2015年4月8日水曜日

[AD FS/OAuth]Windows Server Technical PreviewでのOAuth対応

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対応についてもある程度試してはいるので、また書きたいと思います、

後は、次のビルドが出たらもう少し試してみたいと思います。

0 件のコメント: