2015年1月5日月曜日

[JWT/OAuth]Service Accountを使ってGoogle APIを利用する②

前回の続きです。

前回はGoogle Developer ConsoleおよびGoogleAppsの管理ダッシュボードでGoogle側の設定をするところまでを紹介しましたので、今回は実際にサービスアカウントを使ってAPIを利用するためのアクセストークンを取得してみます。

◆リクエストの仕様
Googleの公式ドキュメントを見るとアクセストークンを取得するためには、JWTを生成してトークンエンドポイントへPOSTする必要があるようです。

リクエストの仕様
エンドポイントhttps://www.googleapis.com/oauth2/v3/token
メソッドPOST
パラメータgrant_typeurn:ietf:params:oauth:grant-type:jwt-bearer
assertion作成したJWT


※ちなみにGoogleが提供している.Net用のクライアントライブラリの通信をキャプチャしてみるとエンドポイントアドレスがドキュメントに記載されているものとは異なり、https://accounts.google.com/o/oauth2/tokenとなっています。(ライブラリ側が古いまま?)

また、POSTするJWTは以下の通りの形である必要があるようです。

JWTの仕様
署名アルゴリズムRSA-SHA256
必要なクレームissサービスアカウントのメールアドレス
scope利用するAPIのスコープ
audhttps://www.googleapis.com/oauth2/v3/token
expトークンの有効期限To
iatトークンの有効期限From


尚、エンタープライズシナリオにおいて特定の管理者がAPIを実行したことを記録していくような場合においては、上記のクレームにsubを追加して管理者メールアドレスを設定することでロギングが出来るようになります。

◆JWTを生成する
・.Net標準のJwtSecurityTokenHandlerを利用する
せっかくマイクロソフト謹製のJWTを扱うライブラリがあり、Azure Active Directory(AzureAD)へのアクセス用のサンプルではほぼ100%このライブラリを使っているので、まずはこちらを使ってJWTを生成してみます。

このライブラリを使うことでコーディング量をかなり削減できるので、うまく使えればかなり楽に開発が出来るはずです。

使うのは、NuGetで提供されている以下のパッケージです。
 名称:JSON Web Token Handler For the Microsoft .Net Framework 4.5
 作成者:Microsoft Open Technologies.
 識別:System.IdentityModel.Tokens.Jwt
 バージョン:4.0.1

JWTを生成するときはシンプルにJwtSecurityTokenに必要なクレーム情報を指定してインスタンスを生成するだけです。
var token = new JwtSecurityToken(
    SERVICE_ACCOUNT, // iss
    TOKEN_ENDPOINT, // aud
    claims, // additional claims
    issuedTime, // iat
    expiresTime, // exp
    credentials); // 署名用のクレデンシャル

固定でiss/aud/iat/exp/credentialを引数で指定するのに加えて、追加のクレームについてはIEmurerable claimsで列挙型の引数を渡す形になるので、必要なクレームは事前にClaim型で定義しておきます。
var claims = new[]
{
    // 「クレーム名,値」のセット
    new Claim("scope", SCOPE)
};

また、署名についてはGoogleからダウンロードした鍵(.p12ファイル)から作成します。
var certificate = new X509Certificate2(
    CERTIFICATE_FILE, // .p12ファイル
    CERTIFICATE_PWD, // 秘密鍵のパスワード(notasecret)
    X509KeyStorageFlags.Exportable);
var credentials = new X509SigningCredentials(certificate);


JWT自体はこれで簡単に生成できるので、ハンドラのインスタンスを生成し、JWTを渡します。
こちらも非常に簡単で、
var handler = new JwtSecurityTokenHandler();
でハンドラは生成でき、
var strJWT = handler.WriteToken(token);
とすればBase64Urlエンコードされた、[ヘッダ].[ペイロード].[署名]が文字列として返ってきます。
この文字列をGoogleのトークンエンドポイントへassertionとしてPOSTしてやれば良いはずです。


出来上がったコードはこんな感じになります。
とてもシンプルです。
        static string CreateJWT()
        {
            // signing certificate
            // pfx(.p12) file which is downloaded from Google Developer Console
            var certificate = new X509Certificate2(
                CERTIFICATE_FILE,
                CERTIFICATE_PWD,
                X509KeyStorageFlags.Exportable);
            var credentials = new X509SigningCredentials(certificate);

            // token lifetime
            var issuedTime = DateTime.UtcNow;
            var expiresTime = issuedTime.AddMinutes(55);

            // additional claims
            var claims = new[]
            {
                // add scope claim
                new Claim("scope", SCOPE)
            };

            // create jwt token
            var token = new JwtSecurityToken(
                SERVICE_ACCOUNT, // iss
                TOKEN_ENDPOINT, // aud
                claims, // additional claims
                issuedTime,
                expiresTime,
                credentials);

            var handler = new JwtSecurityTokenHandler();

            // exception in WriteToken method
            return handler.WriteToken(token);

        }



では、早速実行してみましょう。


はい、怒られました。
Googleの秘密鍵のキー長は1024bitなんですが、このライブラリは最低でも2048bitを要求しているようです。
残念です。
今後のGoogleもしくはMicrosoftに期待しましょう。


・スクラッチでJWTを生成するコードを書く
仕方がないのでスクラッチでコードを書きます。と言っても基本はJSONの取り扱いと署名だけなので、それほど複雑にはなりません。
JSONの扱いは、マイクロソフトのサンプルコードでもよく使われているNewtonsoftのJson.NETパッケージを使います。こちらもNuGetで提供されています。
 名称:Json.NET
 作成者:James Newton-King
 識別:Newtonsoft.Json
 バージョン:6.0.7

◇ヘッダ部
求めるのは、{ alg = "RS256", typ = "JWT" }というJSONなのでまずはJson.NETを使ってバイト配列を作成します。
var header = new { alg = "RS256", typ = "JWT" };
var headerSerialized = JsonConvert.SerializeObject(header,Formatting.None);
var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);

その後、作成したバイト配列をBase64Urlエンコードするのですが、JWTの作者でもあるMicrosoft ResearchのMike Jonesがスペック(ドラフト)のAppendixにC#のサンプルコードを載せてくれているので、そちらを使います。
 参考URL)
 http://self-issued.info/docs/draft-goland-json-web-token-00.html

private static string Base64UrlEncode(byte[] input)
{
    var output = Convert.ToBase64String(input);
    output = output.Split('=')[0]; // Remove any trailing '='s
    output = output.Replace('+', '-'); // 62nd char of encoding
    output = output.Replace('/', '_'); // 63rd char of encoding
    return output;
}

こちらを使って先ほどのバイト配列をエンコードします。
var headerEncoded = Base64UrlEncode(headerBytes);


◇ペイロード部
基本的な考え方はヘッダ部と同じです。必要なJSONは先に挙げた仕様通りなので、以下のようなコードになります。
var payload = new
{
    iss = SERVICE_ACCOUNT, // iss
    scope = SCOPE, // scope
    aud = TOKEN_ENDPOINT, // aud
    exp = exp, // exp
    iat = iat // iat
};
var payloadSerialized = JsonConvert.SerializeObject(payload,Formatting.None);
var payloadBytes = Encoding.UTF8.GetBytes(payloadSerialized);
var payloadEncoded = Base64UrlEncode(payloadBytes);


◇署名部
ここまで作成したヘッダとペイロードを"."(ピリオド)でつないだ文字列を先の秘密鍵を使って署名します。
ポイントはRSACryptoServiceProviderを生成する際のパラメータにキー長をちゃんと設定してあげることです。(certificate.PrivateKey.KeySizeで取得できます。先に説明した通りGoogleの場合、1024bitです)

こちらも同じく作成した署名部分のバイト配列をBase64Urlエンコードして文字列を生成します。
こんなコードになります。

RSAParameters rsaPara = ((RSACryptoServiceProvider)(certificate.PrivateKey)).ExportParameters(true);
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(certificate.PrivateKey.KeySize);
rsa.ImportParameters(rsaPara);
Byte[] target = Encoding.UTF8.GetBytes(headerEncoded + "." + payloadEncoded);
byte[] signBytes = rsa.SignData(target, new SHA256Managed());
var signEncoded = Base64UrlEncode(signBytes);


◇JWTの生成
Googleに渡すJWTはヘッダ、ペイロード、署名を"."(ピリオド)で連携した文字列である必要があるので、単純に文字列を連結します。
var strJWT = headerEncoded + "." + payloadEncoded + "." + signEncoded;


出来上がったコードはこんな感じになります。
先ほどよりはコード量は多いですが、それでもシンプルです。
        static string CreateJWT()
        {
            // signing certificate
            // pfx(.p12) file which is downloaded from Google Developer Console
            var certificate = new X509Certificate2(
                CERTIFICATE_FILE,
                CERTIFICATE_PWD,
                X509KeyStorageFlags.Exportable);

            // 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;


            // header
            var header = new { alg = "RS256", typ = "JWT" };
            var headerSerialized = JsonConvert.SerializeObject(header,Formatting.None);
            var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
            var headerEncoded = Base64UrlEncode(headerBytes);
            
            // payload
            var payload = new
            {
                iss = SERVICE_ACCOUNT,
                scope = SCOPE,
                aud = TOKEN_ENDPOINT, 
                exp = exp,
                iat = iat
            };
            var payloadSerialized = JsonConvert.SerializeObject(payload,Formatting.None);
            var payloadBytes = Encoding.UTF8.GetBytes(payloadSerialized);
            var payloadEncoded = Base64UrlEncode(payloadBytes);

            // signature
            RSAParameters rsaPara = ((RSACryptoServiceProvider)(certificate.PrivateKey)).ExportParameters(true);
            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(certificate.PrivateKey.KeySize);
            rsa.ImportParameters(rsaPara);
            Byte[] target = Encoding.UTF8.GetBytes(headerEncoded + "." + payloadEncoded);
            byte[] signBytes = rsa.SignData(target, new SHA256Managed());
            var signEncoded = Base64UrlEncode(signBytes);

            // concatinate header + payload + signature
            return headerEncoded + "." + payloadEncoded + "." + signEncoded;

        }


実行すると単純に文字列を生成しているだけなので問題なく
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3Mi..
という文字列が取得できます。

後はGoogleにこれをPOSTするだけです。

◆アクセストークンのリクエストを投げる
こちらは単純にHTTP POSTするだけなのでhttpClientを使います。

POSTするためのメソッドを用意しておき、パラメータをセットして実行するだけです。

こんな非同期メソッドを用意しておいて、、、
private static async Task<string> Post(string url, Dictionary<string, string> param)
{
    string result = "";
    try
    {
        HttpClient httpClient = new HttpClient();
        httpClient.MaxResponseContentBufferSize = int.MaxValue;
        HttpContent content = new FormUrlEncodedContent(param);
        var response = await httpClient.PostAsync(url, content);
        String text = await response.Content.ReadAsStringAsync();
        result = text;
    }
    catch (Exception Err)
    {
        result = "ERROR: " + Err.Message;
    }
    return result;
}

こんな感じでリクエストを投げ込みます。
Task task = Task.Factory.StartNew(async () =>
{
    string result = await Post(
        TOKEN_ENDPOINT,
        new Dictionary<string, string>() { 
            { "assertion" , request_jwt }, // 先ほど生成したJWT
            { "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" }});
                Console.WriteLine("Access Token request result :\n{0}", result);
}).Unwrap();
task.Wait();

結果、こんな感じでアクセストークンが取得できます。


後は各APIを利用する際にアクセストークンを提示してあげればOK、という話です。


◆まとめ
サービスアカウントを使ってアクセストークンを取得するところまで解説しました。
どこにもユーザが介在せずにAPIを実行するために必要なトークンが取得できることがわかりましたので、サーバアプリケーションを使う場合は、この方法を使うことになります。
尚、本当は折角なのでマイクロソフト標準のJwtSecurityTokenHandlerを使えればAzureAD用とGoogleApps用でモジュールが簡単に共通化出来るかと思ったのですが、まだまだ発展途上?ということがわかりましたので、今後の対応に期待です。


最後にNGパターンを含めコードを載せておきます。

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace GetAccessToken
{
    class Program
    {

        //
        // Constants
        //
        static string TOKEN_ENDPOINT = "https://www.googleapis.com/oauth2/v3/token";
        static string SERVICE_ACCOUNT = "10.......kvghppo@developer.gserviceaccount.com";
        static string SCOPE = "https://www.googleapis.com/auth/admin.directory.user";
        static string CERTIFICATE_FILE = @"C:\Users\testuser\Downloads\FIMMA-8ce3eefa8ca2.p12";
        static string CERTIFICATE_PWD = "notasecret";

        //
        // Common Library
        //
        private static string Base64UrlEncode(byte[] input)
        {
            var output = Convert.ToBase64String(input);
            output = output.Split('=')[0]; // Remove any trailing '='s
            output = output.Replace('+', '-'); // 62nd char of encoding
            output = output.Replace('/', '_'); // 63rd char of encoding
            return output;
        }

        //
        static string OK_RSA256()
        {
            // signing certificate
            // pfx(.p12) file which is downloaded from Google Developer Console
            var certificate = new X509Certificate2(
                CERTIFICATE_FILE,
                CERTIFICATE_PWD,
                X509KeyStorageFlags.Exportable);

            // 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;


            // header
            var header = new { alg = "RS256", typ = "JWT" };
            var headerSerialized = JsonConvert.SerializeObject(header,Formatting.None);
            var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
            var headerEncoded = Base64UrlEncode(headerBytes);
            
            // payload
            var payload = new
            {
                iss = SERVICE_ACCOUNT,
                scope = SCOPE,
                aud = TOKEN_ENDPOINT, 
                exp = exp,
                iat = iat
            };
            var payloadSerialized = JsonConvert.SerializeObject(payload,Formatting.None);
            var payloadBytes = Encoding.UTF8.GetBytes(payloadSerialized);
            var payloadEncoded = Base64UrlEncode(payloadBytes);

            // signature
            RSAParameters rsaPara = ((RSACryptoServiceProvider)(certificate.PrivateKey)).ExportParameters(true);
            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(certificate.PrivateKey.KeySize);
            rsa.ImportParameters(rsaPara);
            Byte[] target = Encoding.UTF8.GetBytes(headerEncoded + "." + payloadEncoded);
            byte[] signBytes = rsa.SignData(target, new SHA256Managed());
            var signEncoded = Base64UrlEncode(signBytes);

            // concatinate header + payload + signature
            return headerEncoded + "." + payloadEncoded + "." + signEncoded;

        }

        // occur following error.
        //    IDX10630: The 'System.IdentityModel.Tokens.X509AsymmetricSecurityKey' for signing cannot be smaller than '2048' bits.
        // cause: Google's certificate key length is 1024bits, but JwtSecurityTokenHandler/WriteToken requires 2048bits key.
        static string NG_RS256()
        {
            // signing certificate
            // pfx(.p12) file which is downloaded from Google Developer Console
            var certificate = new X509Certificate2(
                CERTIFICATE_FILE,
                CERTIFICATE_PWD,
                X509KeyStorageFlags.Exportable);
            var credentials = new X509SigningCredentials(certificate);

            // token lifetime
            var issuedTime = DateTime.UtcNow;
            var expiresTime = issuedTime.AddMinutes(55);

            // additional claims
            var claims = new[]
            {
                // add scope claim
                new Claim("scope", SCOPE)
            };

            // create jwt token
            var token = new JwtSecurityToken(
                SERVICE_ACCOUNT, // iss
                TOKEN_ENDPOINT, // aud
                claims, // additional claims
                issuedTime,
                expiresTime,
                credentials);

            var handler = new JwtSecurityTokenHandler();

            // exception in WriteToken method
            return handler.WriteToken(token);

        }

        static void Main(string[] args)
        {
            string request_jwt = null;

            // NG : use System.IdentityModel.Tokens.jwt
            // Cause : JwtSecurityTokenHandler only supports 2048bit certificate for signature.
            //request_jwt = NG_RS256();

            // OK : use custom logic
            request_jwt = OK_RSA256();

            Console.WriteLine("Access token request jwt :\n{0}", request_jwt);

            Task task = Task.Factory.StartNew(async () =>
            {
                string result = await Post(
                    TOKEN_ENDPOINT,
                    new Dictionary<string, string>() { 
                        { "assertion" , request_jwt },
                        { "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" }});

                Console.WriteLine("Access Token request result :\n{0}", result);
            }).Unwrap();
            task.Wait();
            Console.WriteLine("End");
            Console.ReadKey();

        }

        private static async Task<string> Post(string url, Dictionary<string, string> param)
        {
            string result = "";
            try
            {
                HttpClient httpClient = new HttpClient();
                httpClient.MaxResponseContentBufferSize = int.MaxValue;
                HttpContent content = new FormUrlEncodedContent(param);
                var response = await httpClient.PostAsync(url, content);
                String text = await response.Content.ReadAsStringAsync();
                result = text;
            }
            catch (Exception Err)
            {
                result = "ERROR: " + Err.Message;
            }
            return result;
        }
    }

}

[JWT/OAuth]Service Accountを使ってGoogle APIを利用する①

今回はサーバアプリケーションからAPIを利用する際の認可の話をGoogleAppsでのOAuth(JWT)の使い方の例をベースに紹介したいと思います。

◆アプリケーションがAPIを利用する際の課題とOAuth2.0
Forefront Identity Manager(FIM)などのサーバアプリケーションからGoogle Admin Directory APIなどを利用する際、サーバアプリケーションに管理者ユーザのIDやパスワードを保持すると様々なデメリットがあります。
例)

  • 管理者のパスワードを変更する際、サーバアプリケーションの設定を変える必要が出てくる
  • サーバアプリケーションに管理者パスワードを保存しておく必要がありセキュリティ上の問題となる可能性がある

※実際、現在codeplex上に公開しているFIM用GoogleApps管理エージェント(Management Agent/MA)ではMA設定にGoogleAppsの管理者ユーザのIDとパスワードを保持しています。

 FIM 2010 GoogleApps MA
 https://fim2010gapps.codeplex.com/


また、サーバアプリケーションに限らずアプリケーションにIDやパスワードを持たせてユーザの代わりにAPIへアクセスするのはよろしくない、ということでOAuth2.0を使った認可(アプリケーションへの権限委譲)が主流になってきています。

(フローの例)
①認可サーバでユーザがAPI(保護対象リソース)へWebアプリケーションがアクセスすることを許可
②認可サーバより発行される認可コードをWebアプリケーションへ渡す
③Webアプリケーションが認可サーバで認可コードとアクセストークンを交換する
④発行されたアクセストークンを使ってAPIを利用


◆サーバアプリケーションがOAuthを使う際の課題と対応
しかし、一般的なWebアプリケーションとは異なりサーバアプリケーションではリソースオーナーであるユーザが一切介在することなくAPIを利用することが求められます。

このようなケースに対応し、Azure Active DirectoryやGoogleApps、Salesforce.comなどの主要クラウドベンダはサーバアプリケーション向けにJWT(JSON Web Token)やJWS(JSON Web Signature)を使ったAPI利用フローを用意しています。

 Azure ADの例(少し古いです)
  http://idmlab.eidentity.jp/2012/09/waad-rest-client-graph-api.html

 GoogleAppsの例(公式ドキュメント)
  https://developers.google.com/accounts/docs/OAuth2ServiceAccount


今回はちょうど先にあげたCodeplexに上げているFIM 2010 GoogleApps MAが使っているGoogleAppsのProvisioning APIの提供が終了するので、代わりに提供されているDirectory APIへ対応させるために改修を行う過程で行ったGoogleApps APIへのアクセス認可の方法を紹介していきます。


◆GoogleAPIへのアクセス用トークンを取得する
さて、実際にGoogleAppsの場合のアクセストークンの取得方法を解説していきます。
先のGoogleのドキュメントをみるとサービスアカウントを使ったAPI利用、というケースが今回のユースケースに該当します。流れとしてはJWTにRSA-SHA256で署名したものをOAuthのトークンエンドポイントへPOSTすればアクセストークが取得できる、とあります。
以下が公式ドキュメントに書いてあるフローです。



【準備】
まずは、JWTを生成する上で必要な設定をGoogle側に入れていきます。

必要なのは、以下の4点です。
①必要なAPIを有効化する
 ⇒GoogleAppsでは最初からすべてのAPIが有効になっているわけではないので、必要なAPIを有効化します。
②クライアントIDを生成する
 ⇒Googleにクライアント情報を登録し、署名に使う証明書を発行してもらう
③GoogleAppsのテナントのAPIアクセスを有効化する
 ⇒初期状態ではAPIアクセスは無効なので有効化する必要があります。
④生成したクライアントから利用できるAPIの範囲(スコープ)を設定する
 ⇒②で生成したクライアントが利用できるAPIをあらかじめ定義しておきます。


順番に見ていきます。①②はGoogle Developer Consoleから、③④はGoogleAppsの管理ダッシュボードのセキュリティメニューから設定します。

①必要なAPIを有効化する
 Google Developer Console(https://console.developers.google.com)にアクセスするとプロジェクトを作成する画面になりますので、まだプロジェクトを作成したことのない人はプロジェクトを作成してください。
 作成が終わったら左ペインの[APIと認証]メニューよりAPIを選択するとAPI一覧の画面になりますので、今回使いたいDirectory APIが含まれるAdmin SDKを探してステータス列の[OFF]のボタンをクリックし有効化を行います。


 確認・同意画面がポップアップするので[同意]をクリックしてAPIを有効化します。


 有効なAPI一覧にAdmin SDKが出てきます。



②クライアントIDを生成する
 次に、同じくDeveloper Consoleの[認証情報]メニューを開き、新しいクライアントIDを作成します。


 作成するクライアントIDの種類を聞かれるので[サービスアカウント]を選択します。


 作成が成功すると、公開鍵/秘密鍵のペアが生成され、秘密鍵の入った証明書ファイル(.p12ファイル)が自動的にダウンロードされます。このファイルは後で使うので大切に保管しておいてください。秘密鍵のパスワードは[notasecret]で固定みたいですが、その値の通り特に秘密事項ではありませんので合言葉として覚えておけばよいです。


 その後、画面上にサービスID情報が表示されます。
 ここで表示されるサービスIDおよびメールアドレス(サービスアカウントのメールアドレス)は後で使いますので、保存しておいてください。



 これでDeveloper Consoleは終わりです。

③GoogleAppsのテナントのAPIアクセスを有効化する
 次はGoogleAppsの管理ダッシュボードの設定です。
 ダッシュボードよりセキュリティメニューを開き、[APIリファレンス]を開くと[APIアクセス]が出てきますので、ここで[APIアクセスを有効にする]にチェックを入れておきます。これでこのGoogleAppsテナントへAPI経由でのアクセスが出来るようになります。


④生成したクライアントから利用できるAPIの範囲(スコープ)を設定する
 同じくセキュリティメニューの[詳細設定]を開きます。ちなみに詳細設定が表示されていない場合は[もっと見る]というリンクがシングルサインオン設定メニューの下にありますので、そちらをクリックしてください。
 サブメニューの中の[APIクライアントアクセスを管理する]をクリックして実際の設定を行います。


 クライアント名に②で生成したクライアントID、APIの範囲に使いたいAPIに対応したスコープを設定します。
 ちなみにDirectory APIの中で今回はユーザを管理したいので、以下を設定します。
  https://www.googleapis.com/auth/admin.directory.user


 スコープの一覧は以下に記載されています。
  https://developers.google.com/admin-sdk/directory/v1/guides/authorizing


ここまでで準備は終わりです。
次回は実際にJWTを生成してアクセストークンを取得するところを解説します。
(.Net標準でSystem.IdentityModel.Tokens.jwtのJwtSecurityTokenHandlerがあるのでせっかくなので使おうとしましたが、結果的にGoogleとはかなり相性が悪く、ほぼスクラッチでコードを書きました...orz。詳しくは次回)

2015年1月2日金曜日

MVP Renewal 6th !!

今年もForefront Identity Manager(FIM)の分野でAwardをいただきました。
なんだかんだで6年目に突入です。


今年はFIMからMicrosoft Identity Manager 2015(MIM)へのメジャーアップデートが控えていることもあり、大きな節目になる年だと思います。

また、私が主に活動しているエンタープライズにおけるアイデンティティの分野においても、これまでのクラウドへのシフトをキードライバーとするハイブリッド・アイデンティティ管理(ID連携、REST APIによるプロビジョニング)が更に発展する形で、以下のキーワードがここ数年の主流となってくると思います。(勝手な予想です、念のため。また、もちろん既に動き始めているキーワードばっかりです)

  • モバイル
    • エンタープライズ・モビリティというキーワードで各ベンダが動き始めている 分野ですが、これまでのPCの延長線上でのブラウザ・ベースでのID連携/シングルサインオンから、よりモバイルの特性を活かしたネイティブ・アプリケーションでのID連携が加速していくと思われます。
  • 多要素認証
    • クラウドとのID連携が加速したことにより、もはや企業のセキュリティの境界がFirewallではなくアイデンティティになりましたが、結果としてこれまで考えられなかった「インターネット上に企業がホストするログイン画面を公開する」という状況が拡大してきています。こうなるとコンシューマ・アイデンティティの分野で長年培われてきた知恵を最大限に活用してセキュリティ対策をしていく必要が出てきます。上記のモバイルの流れと相まって、現状の解としてモバイルデバイスを使った多要素認証(SMSやAuthenticatorアプリ連携)のエンタープライズ分野への適用が進むと思われます。
  • 特権ID管理
    • クラウドやモバイルとは直接は関係しませんが、アプリケーションがより分散管理されていく傾向にあるのは間違いないと思います。また、アプリケーションの分散化に対応する形で、ID管理/ID連携がセキュリティ上の管理ポイント(HUB)となっていく傾向にあるのも同様に間違いないと考えられます。そうなってくると、統制を効かせる意味でも、これまで以上に特権IDに関する管理が複雑化・厳密化する傾向にあると考えています。

また、当然これまでもキーワードとなってきたトラストフレームワークのエンタープライズ活用も、グループ企業内外でのID連携の利用拡大に対応して進んで行く事になると思います。上記特権ID管理などについてはLoA(Level of Assurance)やLoP(Level of Protection)を向上・保証する上で重要なキーワードになってくるものなので、今年は一歩進んでくれるんではないかな?と期待している分野です。

最後になりましたが、本年もよろしくお願いいたします。

2014年12月26日金曜日

[AADSync]オンプレExchangeへのOAuth2認可サポート等

マイクロソフトの製品ライフサイクルに関する考え方がこれまでの一気にバージョンアップ、から継続的にアップデートという方針に変わったのに対応してAADSyncの新機能がちょこちょこリリースされるようになっています。

今回(2014/12/18)のリリース(1.0.475.1202)では以下の機能が追加になっています。

新機能の追加

  • パスワード同期機能が属性ベースでのフィルタリングに対応
    • 属性ベースでパスワード同期するユーザのフィルタリングが出来るようになりました
  • msDS-ExternalDirectoryObjectID属性のオンプレADへの書き戻し対応
    • この属性をオンプレADにも持つことが出来るようになったため、新しいOfficeクライアントからExchangeオンラインに加えてオンプレミスのExchangeへのアクセスについてもOAuth2(JWT)で認可することが可能になりました

  ※OfficeクライアントのOAuth2サポートについては以前のポストを参照してください


その他、もろもろバグ修正

私的に特にうれしいのはsourceAnchorの設定をカスタマイズしてインストールしてもセットアップウィザードを再度実行すると設定が反映されない、というバグ修正でした。(カスタマイズ前提なので)


以下のページからダウンロードできます。
 http://www.microsoft.com/en-us/download/details.aspx?id=44225

2014年12月25日木曜日

[Office365/AzureAD]OpenAMとのID連携③

注意事項)
今回のポストでの構成内容は非サポートの構成を含んでいますので、実環境への適用は避けてください。動作上、ライセンス上の問題が発生しても当方は責任を負いかねます。また、本稿はForefront Identity Manager(FIM)に関する知識をある程度持っている方を対象として書いていますので、細かいFIMの使い方については本blogの他のポストなどを参考にしてください。

ここまでシングルサインオンを中心にOpenAMを使ったOffice365とのID連携(フェデレーション)の解説をしてきましたが、今回はそのバックエンドのプロビジョニングについて解説します。

これまでのポスト
[Office365/AzureAD]OpenAMとのID連携①
[Office365/AzureAD]OpenAMとのID連携②

今回も全体の図の中で枠線で囲っている部分の解説をしていきたいと思います。



AADSyncを使ってオンプレミスのAD DS上のアカウントをAzureADおよびOpenAMのレポジトリであるOpenDJにプロビジョニングしています。
早速構成していきましょう。

◆OpenAMのユーザとOffice365のユーザを紐づけるための準備
最初に大前提として、Office365とのID連携を実現するためには「SAML AsserionとAzureAD上のユーザの以下の属性が一致すること」が必要となります。

属性名属性値
SAML AssertionAzureAD
NameIDImmutableId任意の値(AD FS/AADSync構成の場合の初期値はAD DS上のアカウントのObjectSidをBase64エンコードした値)
IDPEmailUserPrincipalNameメールアドレス形式の値(AD FS/AADSync構成の場合の初期値はAD DS上のアカウントのUserPrincipalName属性の値)


通常AD FS/AADSync(もしくはDirSync)を使うと自動的に上記要件を実現する様に設定が行われますが、今回はAD FSの代わりにOpenAMをIdentity Provider(IdP)として使いますので、OpenAMがSAML Assertionとして発行する値とAzureAD上のユーザの属性を一致させるように、OpenAMのユーザレポジトリであるOpenDJおよびAzureADを構成する必要があります。

IDPEmailについては既に前回OpenAMのリモートサービスプロバイダーを設定する際にIDPEmailとOpenAMのmail属性をマッピングするように設定をしてありますので、追加の設定は不要ですので、NameID/ImmutableIdの値をどうするか考える必要があります。
通常、AD DSアカウントを使うのでObjectSid属性を元にImmutableIdを生成しAzureADへプロビジョニングすることになるのですが、OpenAMのレポジトリであるOpenDJにAD DSのObjectSidを持っていくのもナンセンスなので、今回は簡易的になりますが、AD DS上のsAMAccountName属性とOpenDJ上のuidとAzureAD上のImmutableIdにマッピングすることでOpenAMとAzureADが共通の値を使えるようにします。


◆AzureADとの同期設定
何はともあれAADSyncをダウンロードしてセットアップします。

 ダウンロードページ(2014/12/18に最新版がリリースされています)
 http://www.microsoft.com/en-us/download/details.aspx?id=44225

セットアップを行う際、AzureADとAD DSのアカウントのマッチングに関する設定を行う画面が出てきますので、ここを以下のように設定します。
・sourceAnchor attribute : sAMAccountName
・userPrincipalName attribute : mail

このsourceAnchorがAzureAD上のImmutableIdと紐づく属性となるので、先に述べたようにsAMAccountNameを設定します。また、userPrincipalNameについては.localなどのローカル名前空間でAD DSを構成している環境においてはAzureADのメールアドレスと正しく紐づかないので、別属性(ここではメールアドレス属性)にAzureAD上のメールアドレスの値を設定することにします。



◆OpenDJとの同期設定
まず、AADSyncはAD DSとAzureADの同期しかサポートされませんので、当然のことながらそのままではOpenDJにユーザをプロビジョニングすることはできません。
そこで、非サポートですがForefront Identity Manager(FIM)用のGeneric LDAP Connectorを無理やりAADSyncで使えるように構成します。

 Generic LDAP Connector for Forefront Identity Managerのダウンロードページ
 https://www.microsoft.com/en-us/download/details.aspx?id=41163

無理やり、と言っても基本的にDirSyncもAADSyncもエンジンはFIM Synchronization Serviceなので、コネクタをインストールしてフォルダ構成を合わせれば認識してしまいます。

Generic LDAP Connectorをインストールすると実体のDLLは以下のフォルダに展開されます。
 C:\Program Files\Microsoft Azure AD Sync\Synchronization Service\Extensions

これをAADSyncが利用するコネクタ用のフォルダへコピーします。
 C:\Program Files\Microsoft Azure AD Sync\Extensions

同様にパッケージコネクタの定義ファイルもコピーします。
 元)C:\Program Files\Microsoft Azure AD Sync\Synchronization Service\UISHELL\XMLs\PACKAGEDMAs
 先)C:\Program Files\Microsoft Azure AD Sync\UIShell\XMLs\PackagedMAs

後は、Synchronization Managerから管理エージェントを作成し、Run Profileを定義します。
作成するRun ProfileはとりあえずFull Import/Delta Import/Full Synchronization/Delta Synchronization/Exportで大丈夫です。

諸々の設定が終わると、こんな状態になります。



後はAADSyncのSynchronization Rules Editorを使って属性のマッピングを行うのですが、その前に一つ大事な考慮事項があります。
AzureADとOpenAMのID連携設定を行うと、AzureADからOpenAMへ以下の認証要求がSAMLプロトコルで飛んできます。
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                    ID="_142f225c-9a69-4f38-b585-ba4956b17e32"
                    IssueInstant="2014-12-25T06:49:39Z"
                    Version="2.0"
                    AssertionConsumerServiceIndex="0"
                    >
    <saml:Issuer>urn:federation:MicrosoftOnline</saml:Issuer>
    <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" />
</samlp:AuthnRequest>

重要なのはこの要求の中のNameIDPolicy Formatの部分です。
意味合いとしては「Office365/AzureADにログインするには"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"というフォーマットでNameIDを発行して欲しい」、ということなので、OpenAM側で該当するフォーマットでアサーションを発行してあげるように設定する必要があります。
もちろんOpenAMの全体設定として必ず当該フォーマットでの認証要求があったらNameIDの値に特定の値(ここではuid)を渡す、という設定を行うことも可能ですが、あまりにも汎用性がない構成となってしまいますので、AzureADからの"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"形式での要求があった場合にはuidの値をNameIDとして発行する、という設定を行いたいと思います。

OpenAMでは上記のようなSP毎に発行するNameIDの値を定義するために、ユーザの以下の属性を使います。
※OpenAMの前身であるOpenSSOの更に前身であるSun Access Managerの時代を彷彿とさせる属性名です。
・sun-fm-saml2-nameid-info
・sun-fm-saml2-nameid-infokey

それぞれの属性には以下のような値を設定する必要があるので、AADSyncのExpression Ruleでマッピングを定義します。

属性
sun-fm-saml2-nameid-info"OpenAMのIdP EntityID"|"SP(AzureAD)のEntityID"|"発行する値"|"OpenAMのIdP EntityID"|"NameID Format"|"発行する値"|"SP(AzureAD)のEntityID"|IDPRole|falsehttps://openam.example.com/OpenAM-11.0.0|urn:federation:MicrosoftOnline|kenshinu|https://openam.example.com/OpenAM-11.0.0|urn:oasis:names:tc:SAML:2.0:nameid-format:persistent|kenshinu|urn:federation:MicrosoftOnline|IDPRole|false
sun-fm-saml2-nameid-infokey"OpenAMのIdP EntityID"|"SP(AzureAD)のEntityID"|"発行する値"openam.example.com/OpenAM-11.0.0|urn:federation:MicrosoftOnline|kenshinu



また、上記属性を使うため、OpenDJ上のアカウントのスキーマ(objectClass)設定として以下を設定します。
・inetOrgPerson
・inetUser
・sunFMSAML2NameIdentifier

AADSyncのSynchronization Rules Editorでマルチバリュー属性を設定する場合は面倒ですが、複数のルールを定義し、joinをしていく必要があるので、追加するobjectClassの値ごとにルールを追加します。


ここまでを踏まえて定義したルールが以下です。
①ベースの作成

ページ設定
Description項目
NameOut to OpenDJ - Base Identity
Connected SystemOpenDJ
Connected System Object TypeinetOrgPerson
Metaverse Object Typeperson
Link Typeprovision
Soft Delete Expiry Interval0
Precedence0
Scoping filterAttributeOperatorValue
mailENDSWITH@example.com
TransformationsFlowTypeTarget AttributeSourceApply OnceMerge Type
Expressiondnuid=" & [accountName] & ",ou=people,dc=example,dc=comUpdate
Directsnsn-Update
DirectgivenNamegivenName-Update
Directmailmail-Update
ConstantuserPasswordP@ssw0rdUpdate
Expressionsun-fm-saml2-nameid-infohttps://openam.example.com/OpenAM-11.0.0|urn:federation:MicrosoftOnline|" & [accountName] & "|https://openam.example.com/OpenAM-11.0.0|urn:oasis:names:tc:SAML:2.0:nameid-format:persistent|" & [accountName] & "|urn:federation:MicrosoftOnline|IDPRole|falseMergeCase
Expressionsun-fm-saml2-nameid-infokey"openam.example.com/OpenAM-11.0.0|urn:federation:MicrosoftOnline|" & [accountName]MergeCase
ConstantobjectClassinetOrgPersonMergeCase
Directcncn-Update





尚、objectClass、sun-fm-saml2-nameid-info、sun-fm-saml2-nameid-infokey属性はOpenDJ上でマルチバリュー属性となっているのでMergeTypeはMergeCaseを設定する必要があります。

②objectClassの追加(inetUser)

ページ設定
Description項目
NameOut to OpenDJ - objectClass inetUser
Connected SystemOpenDJ
Connected System Object TypeinetOrgPerson
Metaverse Object Typeperson
Link Typejoin
Precedence0
TransformationsFlowTypeTarget AttributeSourceApply OnceMerge Type
ConstantobjectClassinetUserMergeCase





③objectClassの追加(sunFMSAML2NameIdentifier)

ページ設定
Description項目
NameOut to OpenDJ - objectClass sunFMSAML2NameIdentifier
Connected SystemOpenDJ
Connected System Object TypeinetOrgPerson
Metaverse Object Typeperson
Link Typejoin
Precedence0
TransformationsFlowTypeTarget AttributeSourceApply OnceMerge Type
ConstantobjectClasssunFMSAML2NameIdentifierMergeCase





ここまでで設定が完了するので、AADSyncを使ってAD DS上のユーザをAzureADおよびOpenDJへ実際に同期をしてみます。

◆AD DS上のユーザ作成と同期の実行
今回、ローカルドメイン(.local)としてAD DSを作成したので、userPrincipalNameの代わりにメールアドレスを使う設定を行いましたので、ユーザのメールアドレス属性にAzureADで使うメールアドレスを設定します。




これで準備が整ったので、タスクスケジューラ上のAADSyncスケジュールを有効化して実行します。
尚、OpenDJへの同期についてはAADSyncのスケジューラには組み込まれていませんので手動で実行します。

結果、OpenDJ上に以下のようなユーザが作成され、OpenAMでOffice365/AzureADへシングルサインオン出来るようになります。
dn: uid=kenshinu,ou=people,dc=example,dc=com
objectClass: person
objectClass: organizationalPerson
objectClass: inetorgperson
objectClass: inetUser
objectClass: top
objectClass: sunFMSAML2NameIdentifier
givenName: Kenshin
uid: kenshinu
cn: Kenshin Uesugi
sun-fm-saml2-nameid-info: https://openam.example.com/OpenAM-11.0.0|urn:
 federation:MicrosoftOnline|kenshinu|https://openam.example.com/OpenAM-
 11.0.0|urn:oasis:names:tc:SAML:2.0:nameid-format:persistent|kenshinu|urn:federa
 tion:MicrosoftOnline|IDPRole|false
sn: Uesugi
userPassword: {SSHA}udJ1bXXYdNu2KlpMgXseHxSQnum7i6weFAndjw==
mail: kenshinu@example.com
sun-fm-saml2-nameid-infokey: openam.example.com/OpenAM-11.0.0|urn:fede
 ration:MicrosoftOnline|kenshinu




繰り返しになりますが、今回はAADSyncを使いましたが、Forefront Identity Managerを使えばちゃんとサポートされた構成を作ることが出来ますので、マネはしないでください。
ただ、今後AADSyncでも他のレポジトリとの同期をサポートしていく予定もあるようなので、Synchronization Rules Editorの使い方を中心にAADSyncの使い方をマスターしておくと良いと思います。

参考)Directory Integration Tools(DirSync/AADSync/FIMの機能比較)
 http://msdn.microsoft.com/en-us/library/azure/dn757582.aspx

このページを見るとAADSyncでは様々な機能がCS(Comming Soon)となっており、DirSyncやFIMがAADSyncに統合されていく姿が見えてくると思います。

と、言うことで今後もAADSyncについては目が離せませんね。


尚、今回まででOpenAMを使ったOffice365とのID連携については終わりですが、次回以降で最初の全体構成図の他のパートについても解説をしていこうと思います。

2014年12月22日月曜日

[AD FS]プライバシーに考慮したID連携設定

2か月連続でお世話になりましたCLR/HさんのイベントCLR/H Tokyo vol.7で「ID連携における仮名」の話をしてきました。
(数日前まで登壇の事実を忘れていたので事前準備・告知など全くできず・・・)

当日の資料はこちら



当日行ったデモの内容を含め、少し補足しておきたいと思います。

◆仮名(カメイ)とは
仮名(カメイ/pseudonym)とは、プライバシーに考慮しつつID連携を行いたい、というユースケースに対応した仕組みです。
例えば、企業グループ内で共有しているサービスにおいては、あらかじめ信頼したIdentity Provider(IdP)で認証された、という事実だけがあれば本来個人を特定する情報(氏名やメールアドレス)は不要なはずです。(アプリケーションの動作上、もしくは運用上必要になるケースはありますが)
しかし、実際のID連携のシナリオでは「なんとなく」様々な属性が連携されており、ID連携をしている複数のサービス間でユーザ情報の名寄せが出来てしまったりするケースが散見されます。
エンタープライズシナリオにおいては問題になることはあまりありませんが、複数のService Provider(SP)が存在し、かつ運営主体が別個であるコンシューマシナリオやセンシティブな情報を扱うシナリオ(例えば医療情報などを扱うSPを含むシナリオ)ではユーザ情報の名寄せは問題となることがあります。

そんな時、サービス毎にユニークな名前(ハンドルネームのようなイメージ)をIdPが自動的に発行することで名寄せを防ぎます。このユニークな名前のことを「仮名」と呼びます。
また、仮名には、
・永続的な仮名
・一時的な仮名
があります。

永続的な仮名は同じSPに対しては何度ログインし直しても毎回同じ仮名が発行されますので、SP側でデータを持つ場合でも継続してサービスを使うことが出来ます。
一方で一時的な仮名はSPに対してログインする度に異なる仮名が発行されるので、同じサービス内でのID情報(前回のログイン時に行った振る舞いなど)を紐づけられる心配はありません。


◆ID連携プロトコルにおける仮名
スライドではSAMLを例に紹介しましたが、ws-federationやSAML、OpenID Connectなどの各種ID連携プロトコルにおいて仮名が実装されています。(PPID/Private Personal Identifierとか呼んだりしています)

SAMLにおいてはNameID Formatで表現しており、
・永続的仮名では「urn:oasis:names:tc:SAML:2.0:nameid-format:persistent」
・一時的仮名では「urn:oasis:names:tc:SAML:2.0:nameid-format:transient」
が使われます。


◆AD FSでの仮名のサポート
これまであまり話題に上りませんでしたが、AD FSでも当然仮名を扱うことが出来ます。

ずいぶん前の記事ですが、このあたりでこっそりと紹介されています。

Technet
 When to Use a Custom Claim Rule
 http://technet.microsoft.com/en-us/library/ee913558.aspx
 - Example: How to issue a PPID claim based on an LDAP attribute

MSDN blog
 Name Identifiers in SAML assertions
 http://blogs.msdn.com/b/card/archive/2010/02/17/name-identifiers-in-saml-assertions.aspx


簡単に解説すると、AD FSにはビルトインで「_OpaqueIdStore」というIDストアが定義されており、そこから適切なNameID Format(persistent/transient)をプロパティとしてつけてクレームを発行する、という手順になります。

以下に設定例を紹介します。
(基本はMSDN blogの手順です)


◆永続的仮名を発行する場合
対象のRelying Partyのクレームルール(要求変換規則)に以下の2つのルールを定義します。

1._OpaqueIdStoreからIDの払い出し
 以下のカスタムルールを直接記載します。
c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"]
 => add(store = "_OpaqueIdStore", types = ("http://mycompany/internal/persistentId"), query = "{0};{1};{2}", param = "ppid", param = c.Value, param = c.OriginalIssuer);




2.払い出された値をNameIDとして発行する
 GUIで設定可能です。
 ・[入力方向の要求の種類]に1で発行した際のtypeを指定します
 ・[出力方向の名前IDの形式]に[永続ID]を指定します


実際に払い出されるSAML AssertionをみるとNameID Formatに「urn:oasis:names:tc:SAML:2.0:nameid-format:persistent」が設定されていて、不必要な属性(名前やメールアドレスなど)が発行されていないことがわかります。
<samlp:Response ID="_8b9add32-95c9-47ae-b0f7-fe8719b14207"
                Version="2.0"
                IssueInstant="2014-12-20T07:57:32.137Z"
                Destination="https://sp.example.com/acs"
                Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified"
                InResponseTo="dlmfilinoelgaclpmbbiiknifkjngebcocfoocan"
                xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                >
    <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://idp.example.local/adfs/services/trust</Issuer>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
    </samlp:Status>
    <Assertion ID="_f9880ee1-5236-4a03-911a-dc41d4d6e589"
               IssueInstant="2014-12-20T07:57:32.137Z"
               Version="2.0"
               xmlns="urn:oasis:names:tc:SAML:2.0:assertion"
               >
        <Issuer>http://idp.example.local/adfs/services/trust</Issuer>
        <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
            <ds:SignedInfo>
                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
                <ds:Reference URI="#_f9880ee1-5236-4a03-911a-dc41d4d6e589">
                    <ds:Transforms>
                        <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                        <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                    </ds:Transforms>
                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
                    <ds:DigestValue>qtaUKLYlHQjHAszsestllzzSlwKG/GnfxBTM0LALHmM=</ds:DigestValue>
                </ds:Reference>
            </ds:SignedInfo>
            <ds:SignatureValue>KC2HtMYzjIQ...snip...bhQ3Dg3QBlpcmiA==</ds:SignatureValue>
            <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
                <ds:X509Data>
                    <ds:X509Certificate>MIIC+jCC...snip...E94b3S4cuw==</ds:X509Certificate>
                </ds:X509Data>
            </KeyInfo>
        </ds:Signature>
        <Subject>
            <NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">Eh1as1gTWtagxAk+ECEuTnu/dzUS2fyOnx3ER/NMCeg=</NameID>
            <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <SubjectConfirmationData InResponseTo="dlmfilinoelgaclpmbbiiknifkjngebcocfoocan"
NotOnOrAfter="2014-12-20T08:02:32.137Z"
Recipient="https://sp.example.com/acs"
/>
            </SubjectConfirmation>
        </Subject>
        <Conditions NotBefore="2014-12-20T07:57:32.121Z"
                    NotOnOrAfter="2014-12-20T08:57:32.121Z"
                    >
            <AudienceRestriction>
                <Audience>sp.example.com</Audience>
            </AudienceRestriction>
        </Conditions>
        <AttributeStatement>
            <Attribute Name="http://custom/identity/claims/age">
                <AttributeValue>I am 25 years old.</AttributeValue>
            </Attribute>
        </AttributeStatement>
        <AuthnStatement AuthnInstant="2014-12-20T07:57:31.839Z"
                        SessionIndex="_f9880ee1-5236-4a03-911a-dc41d4d6e589"
                        >
            <AuthnContext>
                <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</AuthnContextClassRef>
            </AuthnContext>
        </AuthnStatement>
    </Assertion>
</samlp:Response>


発行されているのは、
・発行元(Issuer):SPがあらかじめ信頼したIdPのEntityID
・識別子(NameID):仮名
・年齢(age):I am 25 years old.(カスタムスキーマを設定して年齢だけ渡すルールを別途書いています)
・認証情報(AuthnContextClassRef):urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport(パスワードで認証されたことを示します)
といった情報くらいです。
サービスは「あらかじめ信頼したIdP」で「パスワードで認証された」ユーザで「18歳以上(25歳という属性より)」であることがわかるので、ログインを許可しコンテンツを利用させることが出来る、と判断することが出来ます。
また、発行されたNameIDは前回同じユーザがログインした時のものと同じ値なので、サービス側で保持している情報との紐づけも可能となります。


◆一時的仮名を発行する場合
同じく、対象のRelying Partyのクレームルール(要求変換規則)に以下の2つのルールを定義します。

1._OpaqueIdStoreからIDの払い出し
 以下のカスタムルールを直接記載します。
c1:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"]
 && c2:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant"]
 => add(store = "_OpaqueIdStore", types = ("http://mycompany/internal/sessionid"), query = "{0};{1};{2};{3};{4}", param = "useEntropy", param = c1.Value, param = c1.OriginalIssuer, param = "", param = c2.Value);




2.払い出された値をNameIDとして発行する
 GUIで設定可能です。
 ・[入力方向の要求の種類]に1で発行した際のtypeを指定します
 ・[出力方向の名前IDの形式]に[一時ID]を指定します



実際に払い出されるSAML AssertionをみるとNameID Formatに「urn:oasis:names:tc:SAML:2.0:nameid-format:transient」が設定されていて、不必要な属性(名前やメールアドレスなど)が発行されていないことがわかります。(先の永続IDに発行されたAssertionとNameID部分だけが異なります)
<NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">luV8SsaKfGB+vn3IpxhsUL1M/EekpM5E4S20YhWp1Go=</NameID>


こちらも同じく発行されているのは、
・発行元(Issuer):SPがあらかじめ信頼したIdPのEntityID
・識別子(NameID):仮名
・年齢(age):I am 25 years old.(カスタムスキーマを設定して年齢だけ渡すルールを別途書いています)
・認証情報(AuthnContextClassRef):urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport(パスワードで認証されたことを示します)
といった情報くらいです。
サービスは「あらかじめ信頼したIdP」で「パスワードで認証された」ユーザで「18歳以上(25歳という属性より)」であることがわかるので、ログインを許可しコンテンツを利用させることが出来る、と判断することが出来ます。
また、発行されたNameIDは前回同じユーザがログインした時のものとは異なる値なので、サービス側では前回ログインしたユーザと今回ログインしてきたユーザが同じユーザであることが判別できないので、サービス内部でのユーザの紐づけは出来ません。


◆仮名を使う場合の注意点
せっかく仮名を使ったとしても、他にユーザを一意にする属性(メールアドレスなど)をAssertionの中に含めてしまうと仮名を使う意味が全くなくなってしまうので注意が必要です。
(おそらくディレクトリ同期したユーザの情報とAD FSが発行するクレームの紐づけをする必要があるが故にではありますが)悪い例として、Office365のID連携ではNameIDは永続的仮名を使うのですが、別途IDP Emailという属性でメールアドレスを設定する必要があるので、実は仮名の使い方としては意味がありません。

アプリケーションとのID連携を設計する際は、必要とされるプライバシー要件を十分に考慮して利用するNameIDのタイプや連携する属性を検討しましょう。

2014年12月14日日曜日

[Office365/AzureAD]OpenAMとのID連携②

前回のポストの続きです。
今回は予告した通りID連携を、カスタムドメインの作成~SSO設定、OpenAMのIdP/SP/CoT(Circle of Trust)定義の順に実際に設定していきます。

◆OpenAM/IdP(Identity Provider)設定

今回の構成はOpenAM上のユーザでOffice365へログオンしたいので、OpenAMをIdentity Provider(IdP)として設定する必要があります。

尚、実際にはOffice365とOpenAMの間にAzureADが挟まっており、Office365/portal.office.com⇒(ws-federation)⇒AzureAD/login.microsoftonline.com⇒(SAML2.0)⇒OpenAMという流れになります。

前回も掲載した図を厳密に書くと以下のようになります。


早速設定を始めます。
OpenAMのインストールを終了し、管理コンソールの[共通タスク]より[ホストアイデンティティープロバイダの作成]をクリックし、IdP定義を作成します。


ここでの設定項目は署名に使う鍵とトラストサークルの2点だけです。
以下を設定します。
・署名鍵:test(実験なので。実際はちゃんとした証明書を使ってください)
・トラストサークル:新しいトラストサークルに追加、トラストサークル名「例)MSO365」



◆OpenAM/RP(Relying Party)設定

次はOffice365をOpenAMにRPとして設定します。
Office365はSAML2.0のメタデータを公開しているので、それをベースにRP設定をします。

まずは以下のURLにアクセスし、メタデータをダウンロードします。
https://nexus.microsoftonline-p.com/federationmetadata/saml20/federationmetadata.xml
※私はいつもIEで上記URLを開き、ファイルメニューの名前を付けて保存よりXMLファイルを保存しています。

早速ダウンロードしたメタデータをOpenAMにインポートと行きたいのですが、インポート前にXMLファイル内の最初のが余分なので削除しておきます。

以下を削除します。


メタデータの用意が出来たら、OpenAMの管理コンソールの[共通タスク]より[リモートサービスプロバイダを登録]をクリックし、先ほどのメタデータをベースにRP設定を行います。
以下を設定します。
・メタデータ:ファイル⇒先ほどのメタデータファイル(federationmetadata.xml)
・トラストサークル:先ほどIdP作成時に作ったCoT(MSO365)
・属性マッピング:表明内の名前⇒IDPEmail、ローカル属性名⇒mail



これでOpenAM側の設定は完了なので、OpenAMのIdP設定(IdPメタデータ)を出力しておきます。
以下のURLよりメタデータが取得できます。
http://:8080/OpenAM-11.0.0/saml2/jsp/exportmetadata.jsp?realm=/&entityid=http://:8080/OpenAM-11.0.0
※realmやポートなどは環境によって異なります。


次はこのIdPメタデータを使ってOffice365/AzureAD側の設定を行います。


◆Office365/AzureADのカスタムドメインの認証設定を行う

ここではOffice365で使うカスタムドメインの認証をOpenAMとのFederationで行う様に設定を行います。
※ドメインの追加はOffice365の管理ポータルからでもAzureの管理ポータルからでも構いません。
※カスタムドメインを追加する手順は省略します。

具体的には、PowerShellの「Set-MsolDomainAuthentication」コマンドレットに以下のパラメータを付けて設定をします。
※当然ですがコマンドレット実行前に「Connect-MsolService」でAzureADへ接続しておいてください。

パラメータ設定する値設定例
DomainNameカスタムドメイン名example.com
FederationBrandNameブランド名(任意の値)eIdentity
AuthenticationFederated固定Federated
PassiveLogOnUriIdPメタデータ内のタグ内のLocation属性の値
※Binding属性の値が「urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST」のもの
http://openam.example.com:8080/OpenAM-11.0.0/SSOPOST/metaAlias/idp
SigningCertificateIdPメタデータ内のタグの中身MIICQDCCAakCBEeNB0swDQYJ….
IssuerUriIdPメタデータ内のタグ内のentityID属性の値http://openam.example.com:8080/OpenAM-11.0.0
ActiveLogOnUriIdPメタデータ内のタグ内のLocation属性の値
※Binding属性の値が「urn:oasis:names:tc:SAML:2.0:bindings:SOAP」のもの
※httpsでないと設定できないのでhttpsで設定(ブラウザを使ったアクセスではこの設定は使わないので、ダミーのURLでもよいのでとにかくhttpsのURLを設定すればOK)
https://openam.example.com:8080/OpenAM-11.0.0/SSOSoap/metaAlias/idp
LogOffUriIdPメタデータ内のタグ内のLocation属性の値
※Binding属性の値が「urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect」のもの
http://openam.example.com:8080/OpenAM-11.0.0/IDPSloRedirect/metaAlias/idp
PreferredAuthenticationProtocol利用するID連携プロトコル(SAMLP)SAMLP


実際のコマンドは以下のように実行します。
$dom = "example.com"
$url = "http://openam.example.com:8080/OpenAM-11.0.0/SSOPOST/metaAlias/idp"
$cert = "MIICQDCCAakCBEeNB0swDQYJKoZIhvcNAQEEBQAwZzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFDASBgNVBAcTC1NhbnRhIENsYXJhMQwwCgYDVQQKEwNTdW4xEDAOBgNVBAsTB09wZW5TU08xDTALBgNVBAMTBHRlc3QwHhcNMDgwMTE1MTkxOTM5WhcNMTgwMTEyMTkxOTM5WjBnMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEUMBIGA1UEBxMLU2FudGEgQ2xhcmExDDAKBgNVB.............Fcfu2/PeYoAdiDAcGy/F2Zuj8XJJpuQRSE6PtQqBuDEHjjmOQJ0rV/r8mO1ZCtHRhpZ5zYRjhRC9eCbjx9VrFax0JDC/FfwWigmrW0Y0Q=="
$entity = "http://openam.example.com:8080/OpenAM-11.0.0"
$ecp="https://openam.example.com:8080/OpenAM-11.0.0/SSOSoap/metaAlias/idp"
$logout = "http://openam.example.com:8080/OpenAM-11.0.0/IDPSloRedirect/metaAlias/idp"

Set-MsolDomainAuthentication -DomainName $dom -FederationBrandName eIdentity -Authentication Federated -PassiveLogOnUri $url -SigningCertificate $cert -IssuerUri $entity -ActiveLogOnUri $ecp -LogOffUri $logout -PreferredAuthenticationProtocol SAMLP



ちなみに設定結果は「Get-MsolDomainFederationSettings」コマンドレットで確認できます。




今回はここまでです。
次回はAzureAD上のユーザとOpenAM上のユーザの紐づけを行うためのプロビジョニング時の工夫について解説します。(単純に初期状態のディレクトリ同期だとImmutableIdでの紐づけがうまく行かないのでカスタマイズが一部必要になります)