#はじめに
WindowsのOwinというミドルウェアを使ってOAuthを学習するメモです。
#1からの続きです。
#1ではOwinを使ったクライアント・クレデンシャルズフローを学習しましたが、以下の疑問点が残りました。
- AuthorizationServerは自身が発行していたアクセストークンをどこに保存しているのか?
- AuthorizationServerで生成しているidentityって何?
- ResourceServerでユーザー名をどこからともなくGETしてきているが、どこからGETしているのか?
- ResourceServerはどうやってアクセストークンの有効期限をチェックしているのか?
#アクセストークン
疑問点はアクセストークンがどうなっているのかを理解すれば解決します。
- AuthorizationServerは自身が発行していたアクセストークンをどこに保存しているのか?
- 保存していない。必要な情報はすべてアクセストークンに入れているのでストアする必要がない。
- AuthorizationServerで生成しているidentityって何?
- アクセストークンそのもの。identityオブジェクトを暗号化してアクセストークンにします。
- ResourceServerでユーザー名をどこからともなくGETしてきているが、どこからGETしているのか?
- アクセストークンの中からGETしている。
- ResourceServerはどうやってアクセストークンの有効期限をチェックしているのか?
- アクセストークンの中に有効期限が入っている。
ちなみに、
OAuthではアクセストークンの形式は定義していません。OAuthが定義するフローが実現できればアクセストークンのフォーマットは自由、ということみたいです。
なのでここに書いてあるアクセストークンの仕様はOwin独自のもの(と思われ)ます。
#1で見たようにアクセストークンは普通に見てもこのような情報が含まれているかどうかわかりません。暗号化されているのです。
アクセストークンの暗号化と復号はOwinが内部で勝手にやっているので我々は知る必要がないのですが、気になります。
C:\Users\gebo>
curl
- XPOST http://localhost:11625/OAuth/Token
-H "Authorization:Basic MTIzNDU2OmFiY2RlZg=="
-H "Content-Type: application/x-www-form-urlencoded"
-d "grant_type=client_credentials&scope=gebo foo"
{
"access_token":"npgSf2J_zRD_TiIncJ-9ecw5HnW-YHD3j-EgL9rJEGF8HmP08GXGD90S9r9aZ7ua2yCsZH5aKtiXPh9vQZxjPKzCkb5i3TppLVogSRzOwdDpLJE6xyP1icPzqy6TztDl2P9zzUE-YNinNK4l25S8w5wMGKSpF2P9u_ccGPmoWdwdH0KOYMVe6cqBXMmVmyA0Cpra04jg8czWWuQgdODzUhayOJRyul3r9Q3rtcuazZmfDZRt",
"token_type":"bearer",
"expires_in":1199
}
#アクセストークンの復号
というわけで、復号してみましょう。
以下APIで復号できます。
// Web.configのmachineKeyで指定された暗号化キーでデータを復号する
System.Web.Security.MachineKey.Unprotect()
使い方はこんな感じ
protected void Page_Load(object sender, EventArgs e)
{
// GETしたアクセストークン
var token = "npgSf2J_zRD_TiIncJ-9ecw5HnW-YHD3j-EgL9rJEGF8HmP08GXGD90S9r9aZ7ua2yCsZH5aKtiXPh9vQZxjPKzCkb5i3TppLVogSRzOwdDpLJE6xyP1icPzqy6TztDl2P9zzUE-YNinNK4l25S8w5wMGKSpF2P9u_ccGPmoWdwdH0KOYMVe6cqBXMmVmyA0Cpra04jg8czWWuQgdODzUhayOJRyul3r9Q3rtcuazZmfDZRt";
// Tokenはこれで解析できる
var dec = new TokenDecrypt();
if( dec.Decrypt(token) == false )
{
// Decrypt Error;
return;
}
string result = "Decrypt Success!\r\n\r\n";
result = result + "トークン発行日時 : " + dec.DateTimeIssued.ToString() + "\r\n";
result = result + "トークン失効日時 : " + dec.DateTimeExpires.ToString() + "\r\n";
result = result + "IdentityName : " + dec.IdentityName.ToString() + "\r\n";
result = result + $"Roles({dec.Roles.Count}) : \r\n";
foreach (var r in dec.Roles)
{
result = result + "- " + r + "\r\n";
}
result = result + $"Claims({dec.Claims.Count}) : \r\n";
foreach (var c in dec.Claims)
{
result = result + $"- Type={c.Type} , Value={c.Value}" + "\r\n";
}
TextBox2.Text = result;
}
public class TokenDecrypt
{
public class Claim
{
public string Type;
public string Value;
}
public DateTime DateTimeIssued { get; private set; }
public DateTime DateTimeExpires { get; private set; }
public string IdentityName { get; private set; }
public List<string> Roles { get; private set; }
public List<TokenDecrypt.Claim> Claims { get; private set; }
// OWINで生成したアクセストークンを復号する
public bool Decrypt(string accessToken)
{
// Decrypt
var secureDataFormat = new TicketDataFormat(new MachineKeyProtector());
AuthenticationTicket ticket = secureDataFormat.Unprotect(accessToken);
{
var jstTimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
DateTimeIssued = System.TimeZoneInfo.ConvertTimeFromUtc(ticket.Properties.IssuedUtc.Value.DateTime, jstTimeZoneInfo);
DateTimeExpires = System.TimeZoneInfo.ConvertTimeFromUtc(ticket.Properties.ExpiresUtc.Value.DateTime, jstTimeZoneInfo);
}
// get identity
var identity = ticket.Identity as ClaimsIdentity;
var roleClaims = identity.Claims.Where(x => x.Type == ClaimsIdentity.DefaultRoleClaimType).Select(x => x.Value).ToList();
var nonRoleClaims = identity.Claims.Where(x => x.Type != ClaimsIdentity.DefaultRoleClaimType).Select(x => new { Type = x.Type, Value = x.Value }).ToList();
IdentityName = identity.Name;
Roles = roleClaims;
Claims = new List<Claim>();
foreach ( var c in nonRoleClaims)
{
var setc = new TokenDecrypt.Claim();
setc.Type = c.Type;
setc.Value = c.Value;
Claims.Add(setc);
}
return true;
}
private class MachineKeyProtector : IDataProtector
{
private readonly string[] _purpose =
{
typeof(OAuthAuthorizationServerMiddleware).Namespace,
"Access_Token",
"v1"
};
public byte[] Protect(byte[] userData)
{
throw new NotImplementedException();
}
public byte[] Unprotect(byte[] protectedData)
{
// 復号する
// MachineKey=復号するキー(暗号化するときと同じ値を指定する必要がある)
// Web.config で MachineKey を明示的に指定していない場合には自動生成される
return System.Web.Security.MachineKey.Unprotect(protectedData, _purpose);
}
}
}
web.configにはこんな感じで暗号化キーを指定します
<system.web>
・・・
<machineKey validationKey="6CCD8F2FA7593F3108A607DFAF5A7C579E06F802A9B97BE71FD9DF949ED95CCCB3CAD863ECC07FB2F18BA2A10A241572A65EBB5FCF67AD36669BEFE476AB8F36" decryptionKey="0B3EFF1C0890F814D43FFD56EB88ACEAE8E517FD5D052E8ACE0669CA1D0DE097" validation="SHA1" decryption="AES" />
・・・
</system.web>
暗号化と復号で同じパラメータでないといけません。なので、AuthorizationServerとResourceServerには同じmachineKeyを設定しておく必要があります。
web.configにmachineKeyがない場合は自動的にmachineKeyを生成しているようです。
この場合、どんなタイミングでmachineKeyが変わるのかわからないので、ちゃんと指定したほうがよさそうです。
トークンを復号した結果です
Decrypt Success!
トークン発行日時 : 2019/06/30 16:30:44
トークン失効日時 : 2019/06/30 16:50:44
IdentityName : 123456
Roles(0) :
Claims(3) :
- Type=http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name , Value=123456
- Type=urn:oauth:scope , Value=gebo
- Type=urn:oauth:scope , Value=foo
#machineKeyの生成
こんなわけのわからないキー、コピペするしかないですね。
でも、ちゃんとしたプログラムにするんであれば、自サイト専用の値にしたいです。
とはいえ、適当な値ではダメなようです・・・
調べてみると MachineKey Generator
で検索すると結構ヒットするので、これらのサイトで生成しましょう。
例:ASP.Net MachineKey Generator
https://www.allkeysgenerator.com/Random/ASP-Net-MachineKey-Generator.aspx
#おつかれさまでした
トークンのことがわかり少しスッキリした。
ここで作成したサンプルプログラムはコチラ
https://github.com/gebogebogebo/OwinOAuthSample/tree/master/AccessToken
https://github.com/gebogebogebo/OwinOAuthSample/tree/master/AccessTokenDesktop
#3につづく