5月に2度目のビルドが公開されるという
噂のWindows Server Technical Previewですが、これまで何回か紹介してきたようにActive Directory Federation Services(AD FS)の機能が大幅に拡張されています。
(参考)これまでのポスト
- [AD FS] Windows Server Technical PreviewのAD FSを試す
http://idmlab.eidentity.jp/2014/10/ad-fs-windows-server-technical.html
- [AD FS]Windows Server Technical Previewで追加された機能~PowerShell編
http://idmlab.eidentity.jp/2015/03/ad-fswindows-server-technical.html
今回はその中でもOAuth2.0への対応について現状をまとめておきます。
(OpenID Connectにも対応しているのですが、そちらは次回にでも)
ポイントは、以下の3点です。
①Confidential Clientの作成が出来るようになった
②Implicit/Client Credentialsに対応した
※ちなみにResource Owner Password Credentialsは未サポートです
③Client AuthenticationにJWTが使えるようになった
■ポイント①:Confidential Clientを作成できるようになった
Windows Server 2012R2まではPublic Clientしか作成することが出来ず、client_secretが必要なフロー(client_credentialsなど)には対応していませんでしたが、Add-AdfsClientコマンドレットの拡張によりConfidential Clientを作成することが出来るようになりました。
こちらはこれまでと同じく、Public Clientの作成です。
PS> Add-AdfsClient -ClientId 6c831710-cd6c-11e4-8830-0800200c9a66 -Name TestPublicClient -RedirectUri http://localhost -ClientType Public
次に、新たに追加されたConfidential Clientの作成です。-ClientTypeオプションに[Confidential]を指定し、-GenerateClientSecretオプションを付けることによりclient_secretを生成できます。
※-ClientTypeオプションの値がPublicだと-GenerateClientSecretオプションは使えません。
PS> Add-AdfsClient -ClientId bf7fb880-cd6f-11e4-8830-0800200c9a66 -Name TestConfidentialClient -RedirectUri http://localhost2 -ClientType Confidential -GenerateClientSecret
RedirectUri : {http://localhost2/}
Name : TestConfidentialClient
Description :
ClientId : bf7fb880-cd6f-11e4-8830-0800200c9a66
BuiltIn : False
Enabled : True
ClientType : Confidential
ADUserPrincipalName :
ClientSecret : buEgBXgYZO5Y7bdk6kjPE9oDLAA1ZRtvSCwm6orc
JWTSigningCertificateRevocationCheck : CheckChainExcludeRoot
JWTSigningCertificate : {}
ちなみに生成されたClientSecretはこの作成結果画面でしか見れませんので、必ずメモしておきましょう。
(もちろん再生成することも出来ます)
■ポイント②:Implicit/Client Credentialsに対応した
Windows Server 2012R2ではCode Flow一択でしたが、今回のビルドからImplicitおよびClientCredentialsにも対応しています。
おまけですが、まずはこれまでの同じくCode Flowです。
※相変わらずResourceパラメータが必要なので、AD FSに登録したRelying PartyのIdentifierを指定します。これは他のgrant_typeでも同様です。
◆Code Flow
①認可コードを要求します。
https://adfsserver.example.com/adfs/oauth2/authorize?response_type=code&client_id=6c831710-cd6c-11e4-8830-0800200c9a66&redirect_uri=http%3A%2F%2Flocalhost&resource=google.com%2Fa%2Fhoge.example.net
②ユーザ認証後、redirect_uriにリダイレクトされ、GETパラメータで認可コードが取得できます。
http://localhost/?code=QAILg...snip...3r6dSQ
③取得した認可コードをtokenエンドポイントへPOSTします。
https://adfsserver.example.com/adfs/oauth2/token
grant_type authorization_code
code
redirect_uri http://localhost
client_id 6c831710-cd6c-11e4-8830-0800200c9a66
④access_tokenが取得できます。
ついでにid_tokenまで返ってきてしまいます。。。
{
access_token: "eyJ0eXAiOiJKV...snip...m1Pu0pRHEqQNr_uilmeMZ_Z1i2lM3hHDFGLmwg"
token_type: "bearer"
expires_in: 3600
id_token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJ...snip...P662gJhkaquYh3vvW9lvxZAqbThO6Oql8hRw"
}
取得できたaccess_token、id_tokenをデコードするとこんな感じです。
- access_token
{
"aud": "microsoft:identityserver:google.com/a/hoge.exmample.net",
"iss": "http://adfsserver.example.com/adfs/services/trust",
"iat": 1426683515,
"exp": 1426687115,
"sub": "admin@example.com",
"apptype": "Public",
"appid": "6c831710-cd6c-11e4-8830-0800200c9a66",
"authmethod": "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
"auth_time": "2015-03-18T12:58:33.642Z",
"ver": "1.0"
}
- id_token
{
"aud": "6c831710-cd6c-11e4-8830-0800200c9a66",
"iss": "http://adfsserver.example.com/adfs/services/trust",
"iat": 1426683515,
"exp": 1426687115,
"auth_time": "2015-03-18T12:58:33.642Z",
"sub": "yMpiFIT0ydzHFXmiKhjSPqiqFDvSHnYlGctCv3NyAas=",
"ver": "1.0"
}
ちなみにSet-AdfsRelyingPartyTrustコマンドレットを使ってResource(Relying Party)の設定のIssueOAuthRefreshTokensToに[AllDevices]を設定することでrefresh_tokenを発行することも出来ます。
{
access_token: "eyJ0eXAiOiJKV1...snip...2Qd0FZ5J_zORnxOyvj1MxQsVCwMMmlNg"
token_type: "bearer"
expires_in: 3600
refresh_token: "NLGBlhSJjs7_zvjTkKtnnGY...snip...OSlKvCA"
id_token: "eyJ0eXAiOiJKV1QiLCJh...snip...0tr3iS_RIxFiNZBxOfvcBlzV9u3HA"
}
これでgrant_typeにrefresh_tokenをセットしてtokenエンドポイントにrefresh_tokenをPOSTすることで再度ユーザ認証を求められることなくaccess_tokenを取得することが出来ます。
◆implicit flow
いよいよ新しくサポートされたImplicit Flowです。
①Authorizationエンドポイントにresponse_type=tokenを付けてaccess_tokenをリクエストします。
https://adfsserver.example.com/adfs/oauth2/authorize?response_type=token&client_id=6c831710-cd6c-11e4-8830-0800200c9a66&redirect_uri=http%3A%2F%2Flocalhost&resource=google.com%2Fa%2Fhoge.example.net
②ユーザ認証が行われるため、ログオンするとredirect_uriにリダイレクトされ、フラグメントにaccess_tokenが返ってきます。
http://localhost/#access_token=eyJ0eXAiO...snip...MP_bQsjj7Jvp81j-qztASSrzzAypA&token_type=bearer&expires_in=3600
CodeFlowと同じく取得したaccess_tokenをデコードするとこんな感じになります。当然同じようなものになりますが。。。
{
"aud": "microsoft:identityserver:google.com/a/hoge.example.net",
"iss": "http://adfsserver.example.com/adfs/services/trust",
"iat": 1426683944,
"exp": 1426687544,
"sub": "admin@example.com",
"apptype": "Public",
"appid": "6c831710-cd6c-11e4-8830-0800200c9a66",
"authmethod": "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
"auth_time": "2015-03-18T13:05:42.250Z",
"ver": "1.0"
}
◆Client Credentials Flow
次にClient Credentialsです。こちらも新しくサポートされました。
①tokenエンドポイントにPOSTします。
その際、client_id/client_secretを一緒にPOSTすることでクライアント認証を行います。
このあたりの認証方法が各社バラバラなのが困りますね。。。
POST https://adfsserver.example.com/adfs/oauth2/token
grant_type client_credentials
resource urn:dummyapi
client_id bf7fb880-cd6f-11e4-8830-0800200c9a66
client_secret bu....snip....rc
・・・。unauthorized_clientエラーが返ってきました。。。
{
error: "unauthorized_client"
error_description: "MSIS9605: The client is not allowed to access the requested resource."
}
Resource(Relying Party)の設定をGet-AdfsRelyingPartyTrustで確認すると、
AllowedClientTypes : Public
となっています。
②今回使うClientはclient_secretを使うのでConfidential Clientなので、Resourceの設定を変更する必要があります。Set-AdfsRelyingPartyTrustでAllowClientTypesにConfidentialを設定して、再度tokenエンドポイントにPOSTします。
今度はちゃんとaccess_tokenが取得できました。
◆Resource Owner Password Credentials Flow
最後に一応Resource Owner Password Credentialsです。
①tokenエンドポイントにgrant_type:passwordでPOSTします。
POST https://adfsserver.example.com/adfs/oauth2/token
grant_type password
username nfujie@example.com
password P@ssw0rd
resource urn:dummyapi
client_id bf7fb880-cd6f-11e4-8830-0800200c9a66
client_secret buEgB...snip..6orc
②残念ながら[unsupport_grant_type]といって怒られてしまいます。
{
error: "unsupported_grant_type"
error_description: "MSIS9611: The authorization server does not support the requested 'grant_type'. The authorization server only supports 'authorization_code' or 'refresh_token' as the grant type."
}
■ポイント③:Client AuthenticationにJWTが使えるようになった
JWT(JSON Web Token) Profile for OAuth 2.0 Client Authentication and Authorization Grants(
http://self-issued.info/docs/draft-ietf-oauth-jwt-bearer-06.html)への対応ですね。
ちなみにまだAuthorization Grantについては試していませんが、多分対応している気がします(マイクたん的に)
先ほどClient Credentials FlowでのClient認証を行うためにclient_idとclient_secretをリクエストに入れましたが、client_secretを毎回通信に入れるのが嫌な場合に秘密鍵で署名したJWTを使ってクライアント認証をする仕組みなので、Set-AdfsClientコマンドレットでクライアントに公開鍵を設定しておき、リクエストのJWT(client_assertion)に対応する秘密鍵で署名します。
①クライアントに公開鍵を設定する
以下の様にしてあらかじめ用意しておいたcerファイルをクライアントに設定します。
PS> $cert=New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("c:\temp\public.cer")
PS> Set-AdfsClient -TargetClientId bf7fb880-cd6f-11e4-8830-0800200c9a66 -JWTSigningCertificate $cert
設定した結果をGet-AdfsClientで確認すると、JWTSigningCertificateパラメータに証明書の情報が表示されます。
PS> Get-AdfsClient -Name TestConfidentialClient
RedirectUri : {http://localhost2/}
Name : TestConfidentialClient
Description :
ClientId : bf7fb880-cd6f-11e4-8830-0800200c9a66
BuiltIn : False
Enabled : True
ClientType : Confidential
ADUserPrincipalName :
ClientSecret : ********
JWTSigningCertificateRevocationCheck : CheckChainExcludeRoot
JWTSigningCertificate : {[Subject]
CN=adfsserver.example.com, OU=hoge, OU=hoge
[Issuer]
CN=hoge, hoge,
L=hoge, S=hoge, C=JP
[Serial Number]
00D6...snip...6116767C
[Not Before]
2/26/2015 12:00:00 AM
[Not After]
2/25/2018 11:59:59 PM
[Thumbprint]
51A5...snip....3F3A23DA
}
②リクエストにセットするclient_assertionを作成し、秘密鍵で署名する
ADALを使えば割と楽に作れますが、今回はロジックをわかりやすくするため、生でコードを書いています。
こんなコードでclient_assertionを取得します。
static string Get_Client_Assertion(){
// 秘密鍵ファイルとパスフレーズをセット
var certificate = new X509Certificate2(
"c:\temp\private.p12",
"secret",
X509KeyStorageFlags.Exportable);
// Credentialの生成
var credentials = new X509SigningCredentials(
certificate,
new SecurityKeyIdentifier(
new NamedKeySecurityKeyIdentifierClause(
"kid",
"51...snip...A23DA(thumbprint)")));
// token lifetime
var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var issueTime = DateTime.Now;
var iat = (long)issueTime.ToUniversalTime().Subtract(utc0).TotalSeconds;
var exp = (long)issueTime.ToUniversalTime().AddMinutes(55).Subtract(utc0).TotalSeconds;
var issuedTime = DateTime.UtcNow;
var expiresTime = issuedTime.AddMinutes(5);
var epoch = new DateTime(1970, 01, 01, 0, 0, 0);
// JWT Headerの生成
var header = new { alg = "RS256", typ = "JWT", x5t = "Ua...snip...9o(thumbprint)" };
var headerSerialized = JsonConvert.SerializeObject(header, Formatting.None);
var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
var headerEncoded = Base64UrlEncode(headerBytes);
// JWT Payloadの生成
var payload = new
{
sub = "bf7fb880-cd6f-11e4-8830-0800200c9a66(client_id)",
iss = "bf7fb880-cd6f-11e4-8830-0800200c9a66(client_id)",
aud = "https://adfsserver.example.com/adfs/oauth2/token",
jti = "admin@example.com",
exp = exp,
iat = iat
};
var payloadSerialized = JsonConvert.SerializeObject(payload,Formatting.None);
var payloadBytes = Encoding.UTF8.GetBytes(payloadSerialized);
var payloadEncoded = Base64UrlEncode(payloadBytes);
// 署名の生成
var x509Key = new X509AsymmetricSecurityKey(certificate);
RSACryptoServiceProvider rsa = x509Key.GetAsymmetricAlgorithm(SecurityAlgorithms.RsaSha256Signature, true) as RSACryptoServiceProvider;
RSACryptoServiceProvider newRsa = null;
newRsa = GetCryptoProviderForSha256(rsa);
using (SHA256Cng sha = new SHA256Cng())
{
return headerEncoded + "." + payloadEncoded + "." + Base64UrlEncode(
newRsa.SignData(Encoding.UTF8.GetBytes(headerEncoded + "." + payloadEncoded), sha));
}
}
③生成したclient_assertionを使ってaccess_tokenを要求する
tokenエンドポイントにclient_assertionを含むパラメータをPOSTします。
POST https://adfsserver.example.com/adfs/oauth2/token
grant_type client_credentials
resource urn:dummyapi
client_assertion_type urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion eyJhbGciO...snip...6r3LZa-H2avPokc4sp4A
client_secretを使わないので少し気分的に楽になります。
④access_tokenが返ってくる
ここは先に解説したClient Credentials Flowの結果と変わりません。
とりあえずここまでです。
他にもOAuth2.0 JWT Bearer TokenフローやOpenID Connect対応についてもある程度試してはいるので、また書きたいと思います、
後は、次のビルドが出たらもう少し試してみたいと思います。