2015年10月9日金曜日

[Azure AD]Azure AD B2Cを利用してソーシャル認証するアプリケーションを開発する

こんにちは、富士榮です。

先日Public Previewが公開されたAzure Active Directory B2C(Azure AD B2C)を使うと、これまでAccess Control Service(ACS)が担ってきたコンシューマ・アイデンティティ・プロバイダを使った認証をサポートするアプリケーションを開発することができます。

参考)
・公式Blogでの発表
 Azure AD B2C and B2B are now in Public Preview!
 http://blogs.technet.com/b/ad/archive/2015/09/16/azure-ad-b2c-and-b2b-are-now-in-public-preview.aspx

・安納さんによる世界一はやいまとめ資料
 https://docs.com/junichia/8538/azure-ad-b2c-preview-v0-6


ASP.NET IdentityやACSでも同じようなことが出来るので、今後どのようにすみ分けていくのかは要注目ですが、まずは手始めにOpenID Connectに対応しているものも、そうでないものも含め各種IdPをAzure AD B2Cを経由することによりOpenID Connect対応のIdPにすることができる、という点でアプリケーション開発者から見ると利点があると思われます。

早速、これまでAzure ADやAD FSのOpenID Connect対応を検証するために書いたコードをどこまで再利用できるのか?を確認してみたいと思います。

参考)過去のポスト
 [Azure AD]App Model v2.0を利用して組織内外共用のアプリケーションを開発する
 http://idmlab.eidentity.jp/2015/08/azure-adapp-model-v20.html

 [AD FS]OpenID Connectに対応した次期AD FSを試す
 http://idmlab.eidentity.jp/2015/08/ad-fsopenid-connectad-fs.html


◆OpenID Connect関連のパラメータを確認する
まずは、Azure AD B2CのOpenID Connect関連パラメータをwell-known/openid-configurationエンドポイントから覗いてみます。
エンドポイントアドレスは、
https://login.microsoftonline.com/{tenant}.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_signin
です。

{
  "issuer": "https://login.microsoftonline.com/xxxxxx/v2.0/",
  "authorization_endpoint": "https://login.microsoftonline.com/xxxx.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_signin",
  "token_endpoint": "https://login.microsoftonline.com/xxxx.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_signin",
  "end_session_endpoint": "https://login.microsoftonline.com/xxxx.onmicrosoft.com/oauth2/v2.0/logout?p=b2c_1_signin",
  "jwks_uri": "https://login.microsoftonline.com/xxxx.onmicrosoft.com/discovery/v2.0/keys?p=b2c_1_signin",
  "response_modes_supported": [
    "query",
    "fragment",
    "form_post"
  ],
  "response_types_supported": [
    "code",
    "id_token",
    "code id_token"
  ],
  "scopes_supported": [
    "openid"
  ],
  "subject_types_supported": [
    "pairwise"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_post"
  ],
  "claims_supported": [
    "emails",
    "sub",
    "idp"
  ]
}



これはマイクロソフトの悪い癖だと思うんですが、エンドポイントのアドレスにクエリパラメータをつけるのはちょっと頂けない感じですね。。。
実際にアプリケーションを開発する時にはConfiguration情報から取得したエンドポイントアドレスに他のパラメータをつけて処理を行うわけなので、クエリパラメータが2重についてエラーが起きてしまいます。。。
ここはAzure AD B2Cを使ったアプリケーション開発をする際の注意点ですね。


◆クライアント登録を行う
Azure AD B2Cの管理は新しいAzureポータルから行います。
このあたりの細かい設定方法は別の機会に紹介したいと思いますが、といあえずクライアント(アプリケーション)を登録してclient_idとclient_secretを取得しないと始まりませんので、まずは登録します。



◆アプリケーションを開発する
既存のOpenID Connect/RP(アプリケーション)のコードの再利用性を確認するのが目的なので、以前のポストで使ったPHPアプリケーションを再利用します。

完全互換であれば、
・authorization_endpoint
・token_endpoint
・client_id
・client_secret
・redirect_uri
を今回の環境に合わせて修正すればそのまま動くはずです。

早速修正してみました。


これで実行すると、、、404エラー来ました・・・。
先ほどのエンドポイントアドレスにクエリパラメータが入っているので変なエンドポイントへアクセスしてしまっています。
仕方がないので、認可エンドポイントへのGETリクエストのパラメータを'?'ではなく、'&'で連結します。



基本はこれで動くようになりましたが、id_tokenのフォーマットが微妙に違うようです。
具体的にはemailsクレームがマルチバリューで返ってくるので、jsonのパースをちゃんとしてあげないといけません。
例えばGoogle+だと以下のようなid_tokenがかえってきます。
{
  "exp": 1444322056,
  "nbf": 1444318456,
  "ver": "1.0",
  "iss": "https://login.microsoftonline.com/xxxx/v2.0/",
  "acr": "b2c_1_signin",
  "sub": "Not supported currently. Use oid claim.",
  "aud": "60a26a60-1c47-497c-b4c3-5cea0f1bc293",
  "iat": 1444318456,
  "auth_time": 1444318456,
  "idp": "google.com",
  "emails": [
    "xxx@gmail.com"
  ]
}



◆実行してみる
今回はGoogle+およびFacebookを使ってサインインする様にAzure AD B2Cを構成してみました。
それぞれの実行結果を確認してみます。

まずはアプリケーションへアクセスするとAzure AD B2Cのログイン画面へ遷移します。
デフォルト画面なので恐ろしくシンプルな画面です。
ここにあらかじめ設定しておいたアイデンティティ・プロバイダが出てきます。



初回アクセス時は認可の確認が求められますので許可すると、クレームが取得できます。
(ちなみに本当の初回はソーシャルIDでサインアップする必要があります。こちらの手順は別途紹介します)

Google+でサインインした場合



Facebookでサインインした場合




どちらの場合もsubが取得できないんですね。。。
今後にいろいろと期待です。


参考:今回利用したコード)
<?php

// パラメータ類
$authorization_endpoint = 'https://login.microsoftonline.com/xxxx.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1_signin';
$token_endpoint = 'https://login.microsoftonline.com/xxxx.onmicrosoft.com/oauth2/v2.0/token?p=b2c_1_signin';
$client_id = '60a26a60-1c47-497c-b4c3-5cea0f1bc293';
$client_secret = 'xxxx';
$redirect_uri = 'https://xxxx.azurewebsites.net/index.php';
$response_type = 'code';
$state =  'hogehoge'; // 手抜き
$nonce = 'fogafoga'; // 手抜き

// codeの取得(codeがパラメータについてなければ初回アクセスとしてみなしています。手抜きです)
$req_code = $_GET['code'];
if(!$req_code){
    // 初回アクセスなのでログインプロセス開始
    // GETパラメータ関係
    $query = http_build_query(array(
        'client_id'=>$client_id,
        'response_type'=>$response_type,
        'redirect_uri'=> $redirect_uri,
        'scope'=>'openid',
        'state'=>$state,
        'nonce'=>$nonce
    ));
    // リクエスト
    header('Location: ' . $authorization_endpoint . '&' . $query );
    exit();
}

// POSTデータの作成
$postdata = array(
    'grant_type'=>'authorization_code',
    'client_id'=>$client_id,
    'code'=>$req_code,
    'client_secret'=>$client_secret,
    'redirect_uri'=>$redirect_uri
);

// TokenエンドポイントへPOST
$ch = curl_init($token_endpoint);
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query($postdata));
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
$response = json_decode(curl_exec($ch));
curl_close($ch);

// id_tokenの取り出しとdecode
$id_token = explode('.', $response->id_token);
$payload = base64_decode(str_pad(strtr($id_token[1], '-_', '+/'), strlen($id_token[1]) % 4, '=', STR_PAD_RIGHT));
$payload_json = json_decode($payload, true);

// 整形と表示
print<<<EOF
    <html>
    <head>
    <meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
    <title>Obtained claims</title>
    </head>
    <body>
    <table border=1>
    <tr><th>Claim</th><th>Value</th></tr>
EOF;
    foreach($payload_json as $key => $value){
        if($key == "emails"){
            foreach($value as $mail_key => $mail_value){
                print('<tr><td>'.$key.'</td><td>'.$mail_value.'</td></tr>');                    
            }
        }else{
            print('<tr><td>'.$key.'</td><td>'.$value.'</td></tr>');            
        }
    }
print<<<EOF
    </table>
    </body>
    </html>
EOF;

?>

0 件のコメント: