2020年5月20日水曜日

Build速報!FacebookでAzure ADへログインする

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

Buildですね。リモート開催になって日本からでも参加しやすくなって睡眠時間が削られる日々をお過ごしだと思います。

今回も色々とAzure ADに関する新機能の発表がありました。

詳しくはAlexのBlogを参照してもらえれば良いのですが、今回はAzure AD B2Bの直接フェデレーションへのFacebookログインの登場(Public Preview)の話です。

AlexのBlog)
https://techcommunity.microsoft.com/t5/azure-active-directory-identity/build-2020-fostering-a-secure-and-trustworthy-app-ecosystem-for/ba-p/1257360

Azure AD B2Bの直接フェデレーションについてはこれまでも取り上げてきましたが、Googleに対応してから約2年、ここに来てFacebookに対応しました。

Googleとの連携の件
https://idmlab.eidentity.jp/2018/08/azure-ad-b2bgoogleid.html

カスタムドメイン vs 直接フェデレーションの件
https://idmlab.eidentity.jp/2020/05/azure-adbyoid.html



もはやB2Bとは?というしかない状態なのですが、早速触ってみましょう。

外部アイデンティティプロバイダの設定

これまでも紹介したことのある画面です。Azure ADのポータルを開き、External Identitiesメニューから「すべてのIDプロバイダー」を開くと「Google」に加えて新たに「Facebook」が選べるようになっています。


ここでFacebookを追加して、AppIdとAppSecretを追加すれば基本は終わりなのですが、設定前に外部ユーザによるサインアップを許可しておく必要があります。
「外部コラボレーションの設定」メニューを見ると、新しく「ユーザーフローによるゲストセルフサービスサインアップを有効にする」という設定が追加されているので有効にしておきましょう。

後はFacebook Developerコンソールで作成したアプリケーションのAppIdとAppSecretを設定すれば終わりです。

Facebook側の設定方法はAzure AD B2Cのドキュメントに記載がありますので、1点を除いてこちらをそのまま使って大丈夫です。
https://docs.microsoft.com/en-us/azure/active-directory-b2c/identity-provider-facebook

その1点とは、そうですredirect_uriです。
直接フェデレーションの場合にFacebookに設定するredirect_uriはどうなるのか?というと、
https://login.microsoftonline.com/te/{テナントID}/oauth2/authresp
となります。

ここまで設定を進めると、ユーザーフローからAzure ADにアクセスするためのアプリケーション「aad-extensions-app」が自動的に登録されます。


ちなみに、このアプリケーションを削除すると面倒なことになるので、消さないようにしてください。
万一消してしまったらAzure AD B2Cでのb2c-extension-appと同じように回復をしてください。
参考)B2Cでの回復手順
https://docs.microsoft.com/ja-jp/azure/active-directory-b2c/extensions-app


ユーザーフローの登録

外部IDプロバイダの設定が終わったら、次はユーザーフローの登録です。
基本この辺りもAzure AD B2Cと同じです。


ユーザーフローの名称(ポリシー名となります)、利用するIDプロバイダ、連携する属性を設定していきます。
尚、属性は標準で用意されているもの以外に「カスタムのユーザー属性」メニューから自分で好きな属性を追加することもできます。

これでIDプロバイダとユーザーフローの登録はおしまいです。

アプリケーションの登録

次に、先ほど作成したユーザーフローを使ってサインアップやサインインを行う対象となるアプリケーションを登録します。
ポイントは、APIのアクセス許可で「User.Read」スコープに対して管理者による同意をしておくこと、です。現状のユーザーフローでのユーザ作成やログイン時にユーザ自身による同意が上手く取れない?のでこの設定は入れておく必要がありそうです。

ユーザーフローとアプリケーションの紐づけ

アプリケーションを作成したら、先に作成しておいたユーザーフローを使う様に構成をします。
先ほどのExternal Identitiesメニューに戻り、先ほど作成したユーザーフローを開きます。アプリケーションに関する設定項目がありますので、ここで作成したアプリケーションを指定します。

これで一通りの設定は終了です。

動作確認

では早速動かしてみます。

このアプリケーションにアクセスする為には、以下のパラメータをつけてAuthorizationエンドポイント(https://login.microsoftonline.com/{テナントID}/oauth2/authorize)へアクセスする必要があります。(通常のOpenID Connectのアプリケーションと同じです)

  • client_id={登録したアプリのClient Id}
  • response_type=id_token
  • scope=openid
  • nonce={生成したnonceの値。テストなら何でもOK}
  • redirect_uri={登録したアプリのredirect_url}

今回、アプリケーションとしてはhttps://jwt.msを使ったのでこんなURLになります。
https://login.microsoftonline.com/{テナントID}/oauth2/authorize?client_id={クライアントID}&response_type=id_token&scope=openid&nonce=hoge&redirect_uri=https:%2F%2Fjwt.ms

実行してみると、いつものサインイン画面になるので、まずはアカウントを作成します。

アカウントの作成画面に「Facebookアカウントでサインイン」というメニューが出来ています。
ここでFacebookアカウントでサインインすると、Azure ADへのアカウント登録を行う際に追加で登録するアカウントを入力する画面が出てきます。

続行するとアカウントがAzure AD上に登録されます。
アカウントタイプはゲスト、ソースはFacebookになっているのがわかります。


ちなみに登録後、サインインする場合はサインインオプションをクリックするとFacebookログインのメニューが出ていますので、そちらを使ってサインインします。


いかがでしょうか?
基本はAzure AD B2Cの機能の一部を通常のAzure ADの直接フェデレーション向けに開放しただけなので、Azure AD B2Cに慣れている方は直感的に理解できると思います。

しかし、冒頭にも書きましたが、こうなってくると「B2Bとは?」という疑問がやはり出てきますが大人しくしておきましょう。

おまけ

たまたまテストに使ったAzure ADにG-SuiteとのSSO設定があったので、SAMLの属性マッピングの調整をちょこっとして、GmailにAzure ADの直接フェデレーションを使ってFacebookアカウントでログインする、というくだらないものを作ってみたので動画を貼っておきます。

2020年5月16日土曜日

Azure ADを拡張してOpenID Connect for Identity Assuranceに対応させる

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

先日、OpenID ConnectのID保証に関する拡張仕様である「OpenID Connect for Identity Assurance」について紹介しましたが、実際に仕様を理解するためには実装してみるのが一番、ということでAzure Active Directoryを拡張して実装してみようと思います。(尚、現状は完成している訳ではありません。軽く動作を確認してみた程度です)

Azure ADに足りないもの

OpenID Connect for Identity Assuranceを実装する上で、Azure ADに足りないものは最低限でも以下の通りです。

  • Authorizationエンドポイントがclaimsパラメータを受け付ける機能
  • verified_claims等の属性をid_tokenやuserInfoエンドポイントから返す機能

逆に、Azure ADでも以下について当然ですが出来ます。

  • 認証
  • ユーザデータを保持するデータベース
  • アプリケーション(Client)管理

実装に向けた戦略

ということで、以下の戦略で拡張してみることにしました。
  • Azure ADのエンドポイントをラップするWebアプリを用意し、request/responseの整形を行う
  • 認証、データ保持、アプリケーション管理はAzure ADの機能を使う
  • OpenID Connect for Identity Assuranceで拡張された属性(verified_claims関係)はAzure ADのスキーマを拡張する
こんな感じの仕組みになるはずです。


いざ実装

準備1:Azure ADのスキーマを拡張する

Azure ADにはスキーマ拡張を行う機能がありますので、今回はこの機能を使ってAzure AD上にID保証に関する情報、ID保証済みの属性を保存できるようにします。

Azure ADのスキーマ拡張に関するドキュメントはこちら

OpenID Connect for Identity Assuranceでは「verified_claims」というエレメントの定義がされており、配下に「verification」と「claims」というエレメントが存在します。
  • verification : IDの検証をどのように行ったのか?の記録
    • trust_framework : どんなルールによってIDの確認・検証をしたのか。例えば犯罪収益移転防止法や携帯電話不正利用法などの、本人確認を行った際の根拠法やルールを指定します
    • Evidence : ID確認時に利用した証拠(エビデンス)を指定します。免許証などですね。
      • type : エビデンスの種類を指定します。id_document(証明書類)、utility_bill(公共料金の領収書)、qes(欧州限定ですがeIDASの電子証明書)がありますが、ここではid_documentを使いました。
      • method : typeで指定したエビデンスをどのように確認したのか?を指定します。例えば対面で確認した場合は「Physical In-Person Proofing」ということで「pipp」という値を設定します。
      • document : typeにid_documentを指定した場合に、具体的に何の証明書類を使ったのか?を指定します。例えば日本の免許証なら「jp_drivers_license」を指定します。
      • issuer : 証明書を発行した機関に関する情報を指定します。
        • name : 機関名
        • country : 機関の属する国
      • date_of_issuance : 証明書類の発行日を指定します。
      • date_of_expiry : 証明書の有効期限を指定します。
  • claims : 検証された属性(ここは要求された属性を普通に指定します)
    • given_name : 検証済みの名
    • family_name : 検証済みの姓
    • など

この形でAzure ADの属性を拡張出来ればいいのですが、上記ドキュメントにもある通り、拡張できる属性の型はStringやInteger、DateTimeなどに限定され入れ子の構造や配列構造が取れません。本来なら複数のエビデンスを使ったID証明などもあり得ますが、現状はデータをフラットで持つしかありませんので諦めます。

具体的な方法は以下の通りです。
  • Directory.AccessAsUser.Allをスコープに指定してaccess_tokenを取得する
  • https://graph.microsoft.com/v1.0/schemaExtensionsに拡張したいスキーマの構造(JSON)をPOSTする
  • 成功したら他のクライアントからでも参照可能な様にスキーマをアクティベーションする(status属性をPATCHでActiveに更新する)
今回、以下の2つの拡張属性を作りました。
  • verification
  • verifiedClaims

それぞれのPOSTしたデータはこちらです。
- verification
{
    "id":"verification",
    "description": "verification extension for OpenID Connect for Identity Assurance",
    "targetTypes": [
        "User"
    ],
    "properties": [
        {
            "name": "verificationId",
            "type": "String"
        },
        {
            "name": "trustframework",
            "type": "String"
        },
        {
            "name": "evidenceType",
            "type": "String"
        },
        {
            "name": "evidenceMethod",
            "type": "String"
        },
        {
            "name": "evidenceDocumentType",
            "type": "String"
        },
        {
            "name": "evidenceDocumentIssuerName",
            "type": "String"
        },
        {
            "name": "evidenceDocumentIssuerCountry",
            "type": "String"
        },
        {
            "name": "evidenceNumber",
            "type": "String"
        },
        {
            "name": "evidenceDateOfIssurance",
            "type": "DateTime"
        },
        {
            "name": "evidenceDateOfExpiry",
            "type": "DateTime"
        }
    ]
}



- verifiedClaims
{
    "id": "verifiedClaims",
    "description": "verified claims extension for OpenID Connect for Identity Assurance",
    "targetTypes": [
        "User"
    ],
    "properties": [
        {
            "name": "verificationId",
            "type": "String"
        },
        {
            "name": "givenName",
            "type": "String"
        },
        {
            "name": "familyName",
            "type": "String"
        }
    ]
}


ちなみに、拡張スキーマの単位でid(属性名となります)をつけることが出来ますが、.net、.comなど限られたTLDでカスタムドメインをAzure ADに追加している場合は、[カスタムドメイン名]_[拡張属性名]という名前で属性を作ることが出来ますが、カスタムドメインを持っていない場合は「ext][8桁のランダム文字列]_[拡張属性名]という形で属性名が払い出されます。

当然ですが、この拡張属性に値を入れた状態のユーザを用意しておく必要があります。この辺りはGraph APIを使って値のセットをしてください。

準備2:Azure ADをラップするWebサービスを作る

ココが本番です。といってもやることはシンプルです。
(本来はちゃんと実装しないと危険ですので注意してください。本ポストにサンプルコードが出てきますが、かなり手抜きなのであくまで参考として捉えてください


  • Authorizationエンドポイント
    • clientからの要求の中でAzure ADが受け取ってくれないclaimsパラメータをパースして保持します。後からレスポンスを作るときに何が要求されていたのかを判断するために使います。
    • ※テスト実装では決めうち実装なので実際には保持していません
router.get('/authorize', (req, res) => {
    // get claims from request
    var _claims = req.query.claims;
    // redirect to Azure AD
    var target = lib.addQueryTo(conf.AAD_AuthZ, {
            response_type: 'code',
            scope: 'openid User.Read',
            client_id: req.query.client_id,
            state: req.query.state,
            redirect_uri: req.query.redirect_uri });
    res.redirect(target);
});


  • Tokenエンドポイント
    • codeを受け取ってAzure ADのTokenエンドポイントへ渡してaccess_token、id_tokenを受け取ります。
    • id_tokenには当然verified_claimsが含まれていないので、一度id_tokenをほどき、必要に応じてaccess_tokenを使ってMicrosoft Graphで追加の属性を取得、id_tokenを生成してclientへ返却します。
    • ※テスト実装ではPKCEに対応させていませんので、code横取りをして実装しています。本来はclientとPKCEに使う情報を共有しないと横取り出来ないので、ここはちゃんと考えないとダメです。
router.post('/token', async (req,res) => {
    try{
        let response_from_aad_token = await request({
            url: conf.AAD_Token,
            method: "POST",
            form: {
                grant_type: 'authorization_code',
                code: req.body.code,
                client_id: req.body.client_id,
                client_secret: req.body.client_secret,
                redirect_uri: req.body.redirect_uri
            },
            json: true
        })
        console.log(response_from_aad_token);
        // extract response and re-generate id_token with verified claims
        let decoded_jwt = jwt.decode(response_from_aad_token.id_token)

        // get additional claims using graph api
        let response_from_graph = await request({
            url: conf.AAD_Graph + "/v1.0/me?$select=id,userPrincipalName," + conf.AAD_ExtPrefix + "_verifiedClaims," + conf.AAD_ExtPrefix + "_verification",
            method: "GET",
            headers: {
                'Authorization': 'Bearer ' + response_from_aad_token.access_token,
                'Content-Type': 'application/json'
            }
        })
        console.log(response_from_graph);
        var user = JSON.parse(response_from_graph);
        // get properties from user object
        for (var prop in user){
            if(prop.includes('_verification')){
              var _verification = {
                trustframework: user[prop].trustframework,
                evidenceType: user[prop].evidenceType,
                evidenceMethod: user[prop].evidenceMethod,
                evidenceDocumentType: user[prop].evidenceDocumentType,
                evidenceDocumentIssuerName: user[prop].evidenceDocumentIssuerName,
                evidenceDocumentIssuerCountry: user[prop].evidenceDocumentIssuerCountry,
                evidenceDateOfIssurance: user[prop].evidenceDateOfIssurance,
                evidenceDateOfExpiry: user[prop].evidenceDateOfExpiry
              }
            } else if(prop.includes('_verifiedClaims')){
              var _verifiedClaims = {
                familyName: user[prop].familyName,
                givenName: user[prop].givenName
              }
            }
        }
        // create new jwt.
        var privateKey = fs.readFileSync('./private_key.pem', 'utf-8')
        var new_jwt = null;
        if (typeof _verification !== 'undefined') {
            new_jwt = jwt.sign({
                sub: decoded_jwt.sub,
                iss: 'https://' + req.headers.host,
                aud: decoded_jwt.aud,
                iat: decoded_jwt.iat,
                exp: decoded_jwt.exp,
                email: decoded_jwt.email,
                verified_claims: {
                    verification: {
                        trust_framework: _verification.trustframework,
                        evidence: [
                            {
                                type: _verification.evidenceType,
                                method: _verification.evidenceMethod,
                                document: {
 type: _verification.evidenceDocumentType,
 issuer: {
name: _verification.evidenceDocumentIssuerName,
country: _verification.evidenceDocumentIssuerCountry
 },
 number: _verification.evidenceNumber,
 date_of_issuance: _verification.evidenceDateOfIssurance,
 date_of_expiry: _verification.evidenceDateOfExpiry
                                }
                            }
                        ]
                    },
                    claims: {
                        given_name: _verifiedClaims.givenName,
                        first_name: _verifiedClaims.familyName
                    }
                }
            }, privateKey, { algorithm: 'RS256' });    
        } else {
            new_jwt = jwt.sign({
                sub: decoded_jwt.sub,
                iss: 'https://' + req.headers.host,
                aud: decoded_jwt.aud,
                iat: decoded_jwt.iat,
                exp: decoded_jwt.exp,
                email: decoded_jwt.email,
            }, privateKey, { algorithm: 'RS256' });    
        }
        
        res.send({
            token_type: "bearer",
            scope: "openid",
            expires_in: response_from_aad_token.expires_in,
            access_token: response_from_aad_token.access_token,
            id_token: new_jwt
        });
    } catch(e){
        console.log(e);
    }
});


  • userInfoエンドポイント
    • access_tokenを使ってMicrosoft Graphから必要な属性を取得して、整形した上でclientへ返却します
    • ※テスト実装ではここは実装していません。id_tokenにverified_claimsを入れて返却するところまでです。
  • その他
    • .well-known/openid-configurationとかjwks_uriは必要に応じて実装します。id_tokenの署名をAzure ADではなくこのWebサービス側で行うので対応した公開鍵などの情報をclientへ公開してあげる必要があります。

とりあえずこんな感じでnode.jsで実装してみています。


実際に動かす

テストするにしてもclientが必要になるので、phpでちょこっと書いておく必要があります。
やるべきことはclaimsパラメータを認証要求に乗せて検証済み属性を要求する、ということだけであとは普通のOpenID Connectのクライアントです。

こんな感じですね。
    // claims生成
    $verificationArray = array(
        'trust_framework'=>'null'
    );
    $claimsArray = array(
        'given_name'=>'null',
        'family_name'=>'null'
    );
    $verified_claimsArray = array(
        'verification'=>$verificationArray,
        'claims'=>$claimsArray
    );
    $id_tokenArray = array(
        'email'=>'null',
        'verified_claims'=>$verified_claimsArray
    );
    $claimsArray = array(
        'id_token'=>$id_tokenArray
    );
    
    // GETパラメータ関係
    $query = http_build_query(array(
        'client_id'=>$client_id,
        'response_type'=>$response_type,
        'redirect_uri'=> $redirect_uri,
        'scope'=>'openid User.Read',
        'state'=>$state,
        'nonce'=>$nonce,
        'claims'=>json_encode($claimsArray)
    ));
    // リクエスト
    header('Location: ' . $authorization_endpoint . '?' . $query );


動かすとこんな感じになります。
node.jsを動かすのにglitchを使っているので起き上がるまでにちょっと時間がかかってますが。。。


ということで、ここまでのソースはこちらにあります。
くれぐれも真似して使わない様にしてください。色々危ないので。
https://github.com/fujie/aad_oidc4ida

2020年5月2日土曜日

Azure ADの外部コラボレーションとBYOID

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

リモートワークで組織外の方々とのコラボレーションが大きなテーマになっている方々も多いと思いますが、そういう時はAzure ADの外部連携(B2B)の機能ですよね。

そう言えばAzure ADのDirect FederationがGoogle以外にもSAML/ws-federationの外部IdPをサポートした、という会話をfacebookで某安納さんがしていたのを思い出したのと、先日 Alex Simons が色々と新しい機能が出たよ、というもはや個別の機能リリースの紹介だと無理だから一気にまとめて発表、みたいなポストをしていた中に、Azure AD B2CのSAMLサポートが正式リリースされた、という話があったので、もしやこれは繋がる???ということでやってみました。
いわゆるBYOD(Bring Your Own Identity)ってやつです。

当然、Azure ADのカスタムドメインを使った外部ID連携でも良いわけですが、この辺は後で比較をしてきます。


まず関連する記事、ドキュメント類を。


とりあえず完成イメージを

言葉で説明しても伝わりにくいと思うので、どういうことが出来るようになるのか?を動画にしています。
Azure AD B2Bで招待した外部ユーザでログインしようとすると、Azure AD B2Cにリダイレクトされ、LINEでログインすると、Azure ADにゲストユーザとして登録されてアプリケーションが使えるようになる、というシナリオです。


Azure AD B2CのSAMLサポート

先に書いた公式ドキュメント通りに設定していくと、Azure AD B2CがSAML IdPとして動作出来るようになります。(カスタムポリシーを使います。慣れると非常に簡単です)

大きな流れは以下の通りです。

Azure AD B2C側の設定
  • アサーション署名用の証明書(PFX)を作ってポリシーキーとしてアップロードする
  • ClaimsProviderとして、SAML2AssertionIssuerを作成する
    • MetadataにIssuerUriとして設定したものがIdPのEntityIDになります
    • CryptographicKeysに先にアップロードした証明書コンテナを指定するとアサーション署名をしてくれます(暗号化用証明書もアップロードすれば暗号化もできます)
こんな感じの設定になります。
<ClaimsProvider>
  <DisplayName>Token Issuer</DisplayName>
  <TechnicalProfiles>

    <!-- SAML Token Issuer technical profile -->
    <TechnicalProfile Id="Saml2AssertionIssuer_SAMPLE">
      <DisplayName>Token Issuer</DisplayName>
      <Protocol Name="SAML2"/>
      <OutputTokenFormat>SAML2</OutputTokenFormat>
      <Metadata>
        <Item Key="IssuerUri">https://nfpoc.b2clogin.com/nfpoc.onmicrosoft.com/B2C_1A_SI_SAML_SAMPLE</Item>
      </Metadata>
      <CryptographicKeys>
        <Key Id="MetadataSigning" StorageReferenceId="B2C_1A_samlsampleapp"/>
        <Key Id="SamlAssertionSigning" StorageReferenceId="B2C_1A_samlsampleapp"/>
        <Key Id="SamlMessageSigning" StorageReferenceId="B2C_1A_samlsampleapp"/>
      </CryptographicKeys>
      <InputClaims/>
      <OutputClaims/>
      <UseTechnicalProfileForSessionManagement ReferenceId="SM-Saml-issuer"/>
    </TechnicalProfile>

    <!-- Session management technical profile for SAML based tokens -->
    <TechnicalProfile Id="SM-Saml-issuer">
      <DisplayName>Session Management Provider</DisplayName>
      <Protocol Name="Proprietary" Handler="Web.TPEngine.SSO.SamlSSOSessionProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
    </TechnicalProfile>
  </TechnicalProfiles>
</ClaimsProvider>


  • UserJourneyを定義する
    • この辺は通常のカスタムポリシーと同じです
  • RelyingPartyの定義を行う
    • 公式ドキュメントだとSAML SPに関するパラメータはAzure AD B2Cにアプリケーション定義を作ってマニフェストで設定する、ということになっていますが、Azure ADとB2Bとして連携する場合は、ちょっと工夫が必要です(後述します)

Azure AD側の設定

Azure ADの直接フェデレーションの設定を行います。
  • プロトコルはSAMLを選ぶ
  • IdPのドメイン名はSAML IdPの認証エンドポイントと同一ドメインである必要があるため、Azure AD B2Cのドメイン名(~.b2clogin.com)を指定する
  • 上記でAzure AD B2Cの設定が上手くいっていればSAML Metadataのダウンロードが出来るようになっていますので、保存したMetadataをアップロードして解析を行う
直接フェデレーションの設定画面。ここで「新しいSAML/WS-Fed IdP」を選びます。

こんな感じで設定します。



設定としては非常にシンプルです。
ただ、何点かクセがありますので、その部分を重点的に書いておきます。

設定のポイント

Azure AD B2Cのアプリケーション設定
  • ドキュメントを見ると、Azure AD B2Bと直接連携するためには以下の情報を設定する必要があることがわかります。
    • AssertionConsumerService
      • https://login.microsoftonline.com/login.srf
    • Audience(日本語ドキュメントだと「対象ユーザー」となっていますが。。。SAML SPのEntityIDのことです)
      • urn:federation:MicrosoftOnline
  • しかし、現状のAzure AD B2Cのアプリケーション登録ではカスタムスキームのAudience(urn:federation:MicrosoftOnline)をマニフェスト編集をしても登録することが出来ません。
  • ということで、SP(Azure AD)のMetadataを作って適当なところ(今回はbob)にアップロードし、カスタムポリシーから参照する形をとります。
こんな感じです。
<RelyingParty>
  <DefaultUserJourney ReferenceId="SI_SAML_SAMPLE" />
    <TechnicalProfile Id="PolicyProfile">
    <DisplayName>PolicyProfile</DisplayName>
    <Protocol Name="SAML2"/>
    <Metadata>
       <Item Key="PartnerEntity">https://nfpoccontent.blob.core.windows.net/root/aad_sp_meta.xml</Item>

手動で作ったSP Metadataはこんな感じです。単純にEntityIDとエンドポイントさえ書いてあれば最低限はOKです。

<?xml version="1.0"?>
<md:EntityDescriptor
    xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
    entityID="urn:federation:MicrosoftOnline"
    validUntil="2031-12-31T00:00:00.000Z">
  <md:SPSSODescriptor
    protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
    <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://login.microsoftonline.com/login.srf"/>
    <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://login.microsoftonline.com/login.srf" index="0"/>
  </md:SPSSODescriptor>
</md:EntityDescriptor>

Azure AD B2CのSAML Metadata

  • 一応ドキュメントを漁ると出てくるのですが、見落としがちなので書いておきますが、以下のルールでMetadata URLが生成されます。
    • https://ドテナント名.b2clogin.com/テナント名.onmicrosoft.com/B2C_1A_ポリシー名/Samlp/metadata
Azure AD B2CからAzure ADへ渡す属性(SAML AssertionのAttributeStatement)

  • ドキュメントを見るとnameidがpersistentであること、emailaddressを属性として渡すこと、とありますので、それに合わせてAzure AD B2CのRelyingParty設定を行います。具体的にはOutputClaimsの設定です。
  • また、標準的なAzure AD B2CのSAML設定だと結構冗長な感じでAttributeStatementが書かれるので、TechnicalProfileのMetadataにSaml11AttributeEncodingInfo(Saml20~でもOK)を指定することで少しすっきりします。
こんな感じのRelyingParty定義になります。

<RelyingParty>
  <DefaultUserJourney ReferenceId="SI_SAML_SAMPLE" />
  <TechnicalProfile Id="PolicyProfile">
    <DisplayName>PolicyProfile</DisplayName>
      <Protocol Name="SAML2"/>
      <Metadata>
        <Item Key="PartnerEntity">https://nfpoccontent.blob.core.windows.net/root/aad_sp_meta.xml</Item>
        <Item Key="Saml11AttributeEncodingInfo">
          <![CDATA[
            <saml:AttributeStatement xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion">
            <saml:Attribute AttributeName="emailaddress" AttributeNamespace="http://schemas.xmlsoap.org/ws/2005/05/identity/claims">
            <saml:AttributeValue>
            </saml:AttributeValue>
            </saml:AttributeStatement>]]></Item>
      </Metadata>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="objectId"/>
        <OutputClaim ClaimTypeReferenceId="b2cmail" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" />
      </OutputClaims>
      <SubjectNamingInfo ClaimType="objectId" ExcludeAsClaim="true"/>
    </TechnicalProfile>
  </RelyingParty>
</TrustFrameworkPolicy>

ちなみにClaimType「b2cmail」は適当に適宜した属性なので自由に定義してもらえれば良いかと思います。重要なのは、「Azure AD B2Cのドメインと同じドメイン名を持つメールアドレスが設定されていること」です。例えば、hoge.b2clogin.comというAzure AD B2Cドメインを使っている場合は、fuga@hoge.b2clogin.comという値を返す必要があります。
※この辺りは早くAzure AD B2Cがカスタムドメインをサポートしてくれないと実利用するには辛いですね。

いざ、動作確認

設定はこれでおしまいです。
しかし、あくまでこれはAzure AD B2Bにおける「外部ユーザの招待において独自のパスワードを払い出さずに済ませるための機能(これ重要)」なので、あらかじめユーザを招待しておく必要があります。

ここは通常の招待と同じなので詳細は割愛しますが、Azure AD B2Cのドメインと同じドメインのユーザを指定して招待する、というところがポイントです。
つまり、メールは届きません。(少なくともb2clogin.comを使っている限り、一般人はこのドメインでメールは届かないと思います)

ただ、招待されている状態であれば招待元のアプリケーションにアクセスすれば招待の承認と同じことが起きますので、運用でカバーです。



そして、これも公式ドキュメントに記載されていますが、現状B2Bの直接フェデレーションはマルチテナントアプリケーションでのホームレルムディスカバリが使えないので、招待元ディレクトリのテナントIDやドメイン名が明示的に指定されているアプリケーションしか動きません。
例えば、

  • https://myapps.microsoft.comはダメ
  • https://myapps.microsoft.com/?tenantid=xxxxxxxxはOK
という感じです。


ここまで行けば、冒頭に動画で紹介した動きが再現できるはずです。
ちなみに動画内では条件付きアクセスを使ってゲストユーザの初回ログイン時に利用規約に同意させる様にしています。

カスタムドメイン単位でのフェデレーションと何が違うのか?

ここで疑問が出てくるのが、元々Azure ADにはカスタムドメイン単位で外部のSAML/WS-FederationのIdPと連携する機能があります。ちょっと前に多くの企業がAD FSをオンプレにおいてOffice365とSSOをやっていた構成ですね。

もちろんこのケースは社内ユーザを想定した話ですが、技術的に言うとB2Bの直接フェデレーションとほぼ変わりません。

ただ、細かく見るとちょっとずつ違います。
非常に雑な比較ですが、こんな感じです。

カスタムドメインのID連携B2Bの直接フェデレーション
フェデレーション単位ドメイン単位ドメイン単位
ユーザの管理あらかじめ作成が必要(Azure AD Connectでの同期など)
ImmutableIdでのマッチング
あらかじめ招待が必要
メールアドレスでのマッチング
アプリケーションの制限マルチテナントでも可
また、WS-Fedでwindowstranportエンドポイントの整備などの条件を満たせばWindows 10 PCログオンも可(Webサインイン設定不要)
マルチテナントアプリは不可
当然Windows 10 PCログインには使えない




ということで、実用的かどうかはユースケース次第というところですが、日々色々な機能が拡張されてきているな、というところです。