Azure AD B2C で SMS と TOTP を 1 つのカスタムポリシーに統合しようとしたとき、「サインインボタンを押してもポップアップが出ない」「画面が無反応に見える」という相談はとても多いです。本記事では、MSAL 側の設定から Azure AD B2C のアプリ登録、カスタムポリシー XML のポイントまでを一気通貫で整理し、どこから切り分ければよいかを具体的に解説します。
Azure AD B2C カスタムポリシーで SMS と TOTP を統合したい
要件としてよくあるのが、次のようなケースです。
- SMS(電話番号)によるワンタイムコード認証
- 認証アプリ(Microsoft Authenticator / Google Authenticator など)による TOTP 認証
- これら 2 種類の MFA を「別ポリシー」ではなく「1 つのカスタムポリシー」に統合し、ユーザーに選択させたい
そして、その実装を進めている途中で、Medium やブログ記事を参考にして Proxy / Identity の 2 アプリを作成し、ポリシーキーも用意したものの、いざフロントエンドからサインインボタンを押しても「何も起きない(ポップアップが出ない)」という問題に直面することがあります。
この症状は、MSAL 側の設定不備・ポリシーの参照ミス・アプリ登録の許可周りなど、複数の要因が絡むため、闇雲に触ると原因が見えづらくなります。そこで本記事では、次の観点で順番に整理します。
- アーキテクチャの前提整理
- フロントエンド(MSAL)側でまず確認すべきポイント
- Azure AD B2C のアプリ登録( IdentityExperienceFramework / ProxyIdentityExperienceFramework )設定
- カスタムポリシー(XML)の構造と SMS / TOTP 統合の考え方
- 「ポップアップが出ない」状況を最短で切り分ける手順
- 実践的な設計ポイントと最短チェックリスト
症状の整理:サインインを押してもポップアップが出ない
今回の相談の典型的な状況を整理すると、次のようになります。
- Proxy / Identity の 2 アプリは作成済み
- 対象アカウント種別は「この組織のディレクトリ内のアカウントのみ」
- ID トークンは有効化済み
- ポリシーキー(Token Signing / Encryption)は 2 つ作成済み
- フロントエンドから [サインイン] ボタンを押すが、認証ポップアップが出ない(フローが開始している気配がない)
見た目は「ボタンを押しても無反応」というだけですが、裏では「MSAL が authority にアクセスできていない」「authorize リクエストが 401 で落ちている」「JS が例外を投げている」など、いろいろなパターンが隠れています。
まずは、構成全体をざっくり整理しておきましょう。
構成の前提:どのアプリが何をしているのか
Azure AD B2C カスタムポリシー + SPA 構成では、主に次の 3 種類のアプリ登録が登場します。
| 種類 | 代表的な名前 | 用途 |
|---|---|---|
| フロントエンド SPA / Web アプリ | (任意、例:my-b2c-spa) | MSAL から B2C のポリシーを呼び出すクライアント |
| IdentityExperienceFramework | IdentityExperienceFramework | カスタムポリシーで実行される「バックエンド API」として振る舞う |
| ProxyIdentityExperienceFramework | ProxyIdentityExperienceFramework | Authentication Service として IEF を呼び出すためのプロキシアプリ |
フロントエンド(MSAL)が使う clientId は、基本的には「SPA / Web アプリ」のアプリ登録です。
多くのトラブルでは、この clientId や authority で IEF 系のアプリ ID を使ってしまったり、ドメイン名・ポリシー名の組み立てを誤っていることが原因になります。
フロントエンド(MSAL)側でまず確認すべきポイント
authority / knownAuthorities の設定
Azure AD B2C を MSAL から呼び出す場合、authority は次の形式で指定します。
https://<tenant>.b2clogin.com/<tenant>.onmicrosoft.com/B2C_1A_<PolicyName>
- <tenant> はテナント名(例:contoso)
- 末尾には
B2C_1A_で始まるカスタムポリシー名をそのまま書く
さらに、MSAL の設定で knownAuthorities を指定しておきます。
knownAuthorities: ["<tenant>.b2clogin.com"]
ここが login.microsoftonline.com になっていたり、テナント名が異なっていると、MSAL 側でエラーになり、結果として「ボタンを押しても何も起きない」状態になりがちです。
clientId / redirectUri の一致
MSAL 側で指定する clientId は、必ず「フロントエンド SPA / Web アプリ」のアプリ登録のものを使用します。
IdentityExperienceFramework / ProxyIdentityExperienceFramework のアプリ ID を指定してしまうと、そもそもフロントエンドとしてのフローが成立しません。
合わせて、redirectUri はアプリ登録に設定したリダイレクト URI と完全一致している必要があります(末尾のスラッシュ有無も含めて一致)。
| 項目 | よくあるミス | 確認ポイント |
|---|---|---|
| clientId | IEF / Proxy のアプリ ID を指定 | SPA 用に作成したアプリ ID になっているか |
| redirectUri | アプリ登録にない URL や末尾スラッシュ違い | ポータルに登録済みの URL と完全一致しているか |
msal-browser v2 を前提にしたフロー選択
新規実装であれば、msal-browser v2(認可コード + PKCE) の利用が推奨されます。古いサンプルの中には暗黙的フロー(Implicit Flow)を前提にしたものもありますが、セキュリティと将来性を考えると、可能な限りコードフローに寄せた方がよいでしょう。
msal-browser v2 の最小構成イメージは次のようになります。
const msalConfig = {
auth: {
clientId: "<SPAのクライアントID>",
authority: "https://<tenant>.b2clogin.com/<tenant>.onmicrosoft.com/B2C_1A_<policyName>",
knownAuthorities: ["<tenant>.b2clogin.com"],
redirectUri: "http://localhost:3000"
},
cache: {
cacheLocation: "sessionStorage"
}
};
const msalInstance = new msal.PublicClientApplication(msalConfig);
// 切り分けのため、まずは redirect で挙動確認するのがおすすめ
msalInstance.loginRedirect({
scopes: ["openid", "offline_access"]
});
既存で MSAL v1(msal.js)を利用している場合は、アプリ登録側で 暗黙的フロー(ID トークンなど)を有効化 しておく必要がありますが、新規であれば v2 を強くおすすめします。
loginPopup と loginRedirect の使い分け
「ポップアップが出ない」という切り分けでは、一時的に loginRedirect() に切り替えてみる のがとても有効です。
| メソッド | 特徴 | 切り分け上の利点 |
|---|---|---|
| loginPopup() | 小ウィンドウや新タブでログイン画面を表示 | UX は良いが、ポップアップブロックに引っかかることがある |
| loginRedirect() | 現在のタブを B2C ログイン画面にリダイレクト | ブラウザのポップアップ制限の影響を受けにくい |
まずは loginRedirect() で「フロー自体が動いているか」を確認し、それが動くようであれば、UX のために loginPopup() に戻す、という流れにするのがおすすめです。
なお、loginPopup() / loginRedirect() はどちらも ユーザー操作イベント内(onClick など)で明示的に呼ぶ 必要があります。レンダリング時や非同期コールバックの中から勝手に呼び出すと、ブラウザ側で弾かれる場合があります。
ブラウザのポップアップ / Cookie 制限
開発環境やセキュリティポリシーによっては、以下の設定が影響することがあります。
- ポップアップブロック(特に企業管理端末のブラウザ)
- サードパーティ Cookie 制限
- トラッキング防止機能(Intelligent Tracking Prevention など)
これらが原因でウィンドウが開けないケースもあるため、切り分け時には一度緩めたプロファイル(または別ブラウザ)で挙動を確認してみるとよいでしょう。
Azure AD B2C 側アプリ登録(IEF / Proxy)の必須設定
IdentityExperienceFramework / ProxyIdentityExperienceFramework の役割
カスタムポリシーの公式構成では、次の 2 つのアプリ登録を作成します。
| アプリ名 | 一般的な種別 | 役割 |
|---|---|---|
| IdentityExperienceFramework | Web API / Web アプリ | カスタムポリシーを実行するバックエンドとして機能 |
| ProxyIdentityExperienceFramework | Public client / ネイティブ | ユーザーからの認証要求を IEF に中継するプロキシ |
ポイントは、ProxyIdentityExperienceFramework → IdentityExperienceFramework への委任許可(API アクセス) を正しく設定することです。
API の公開と委任許可(スコープ)の設定
- IdentityExperienceFramework アプリで API を公開
- 「API の公開」でスコープ(例:
user_impersonation)を作成 api://<IEFアプリID>/user_impersonationのような形になる
- 「API の公開」でスコープ(例:
- ProxyIdentityExperienceFramework アプリで上記スコープを委任
- 「API のアクセス許可」で IEF の API を追加
- 作成した
user_impersonationを選択
- 管理者同意を付与
- テナント管理者が「管理者による同意」を実行
- これがないと B2C のバックエンドで 401 が発生し、結果としてフロントから見ると「何も起きない」ように見えることがあります
リダイレクト URI と Public client フロー
ProxyIdentityExperienceFramework は通常、Public client(ネイティブ)アプリ として構成します。この際、
- 「モバイルおよびデスクトップ アプリケーション」または「パブリック クライアント フローを許可する」を有効化
- 必要に応じて
urn:ietf:wg:oauth:2.0:oob等のリダイレクト URI を設定(古いパターンを使う場合)
一方、フロントエンドの SPA アプリでは、「シングルページ アプリケーション」プラットフォームで http://localhost:3000 などのリダイレクト URI を登録します。
最近は SPA をカスタムポリシーと組み合わせる構成が一般的なので、ここを混同しないように整理しておきましょう。
ID トークン / 認可コードフローの有効化
msal-browser v2 でコードフローを利用する場合、アプリ登録側で「ID トークン」「アクセス トークン」を許可するだけでなく、認可コードフローが利用できる構成になっているか も確認しておきます。
特に、古いチュートリアルを参考にしている場合は、暗黙的フローの設定が混ざっていることがあるため、現在の実装方針と整合が取れているかを一度見直すと安心です。
カスタムポリシー XML の整合性チェック
ポリシーファイル構成とバリデーション
カスタムポリシーは、通常次のようなファイル構成になっています。
- TrustFrameworkBase.xml
- TrustFrameworkExtensions.xml
- SignUpOrSignIn.xml(RelyingParty)
- 他、PasswordReset 等
SMS / TOTP の統合を行う場合、多くは Extensions に定義を追加し、RelyingParty ポリシーでその UserJourney を参照します。
XML の整合性で特に重要なのは、
RelyingParty/DefaultUserJourneyで参照している UserJourney が、Extensions / Base の<UserJourneys>内に存在するか- 各
<OrchestrationStep>のOrderが連番になっているか TechnicalProfileReferenceIdで指定した TechnicalProfile が定義済みか- 必要な
ClaimTypeが事前に定義されているか
公式の IEF ポリシーバリデーターを使えば、これらの構文レベルのミスは機械的に検出できます。
「ポップアップが出ない」という現象の裏側で 500 エラーが起きている場合もあるため、XML のバリデーションは必ず一度通しておくことをおすすめします。
UserJourney と TechnicalProfile の分岐設計
SMS / TOTP を 1 つのポリシーで実現する場合、よくある構成は次のような流れです。
- ユーザーのサインイン(ローカルアカウント / 外部 IdP)
- MFA の種類を選択する画面(SMS / TOTP)
- 選択結果に応じた分岐
- TOTP 選択時:TOTP が未登録なら登録フロー → 検証フロー
- SMS 選択時:電話番号の登録 / 認証コード送信 → 検証フロー
- ID トークン発行(
amrなどに認証手段を入れる)
カスタムポリシーでは、これを OrchestrationStep と TechnicalProfile の組み合わせで表現します。イメージとしては、次のような UserJourney になります(抜粋)。
<UserJourney Id="SignUpOrSignInWithMfa">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp">
<TechnicalProfiles>
<TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
</TechnicalProfile>
</TechnicalProfiles>
</OrchestrationStep>
<!-- MFA 選択画面 -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="MfaSelection" TechnicalProfileReferenceId="SelfAsserted-Mfa-Selection" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- 選択結果に応じて SMS / TOTP に分岐 -->
<OrchestrationStep Order="3" Type="InvokeSubJourney">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>mfaMethod</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<SubJourneys>
<SubJourney Id="MfaWithSms" />
<SubJourney Id="MfaWithTotp" />
</SubJourneys>
</OrchestrationStep>
...
</OrchestrationSteps>
</UserJourney>
実際には Precondition で mfaMethod の値をチェックし、SMS の場合だけ SubJourney A を呼ぶ、TOTP の場合は SubJourney B を呼ぶ、という形にします。
このとき、参考にしているブログ記事の TechnicalProfile 名や SubJourney 名と、自分の XML で使っている名前が一致しているかをしっかり確認してください。
ポリシーキー(TokenSigning / TokenEncryption)の確認
カスタムポリシーでは通常、次の 2 つのキーを使用します。
B2C_1A_TokenSigningKeyContainerB2C_1A_TokenEncryptionKeyContainer
KeyContainer の名前をカスタムしている場合や、Extensions と RelyingParty で参照名がずれている場合、トークン発行の段階でエラーとなり、結果としてフロント側が「沈黙」して見えることがあります。
XML 内の KeyContainerReferenceId と、Azure ポータルの「ポリシーキー」の名前を突き合わせて確認しておきましょう。
TOTP シークレットの保持と再登録制御
TOTP の場合、ユーザーごとにシークレット(シード値)をどこかに保存しておく必要があります。一般的には、B2C の「ユーザー属性拡張(extension_〜)」を 1 つ用意し、そこに TOTP シークレットを格納します。
- 例:
extension_mfaTotpKey
カスタムポリシーでは、
- この属性が空の場合:TOTP を新規登録するフローに遷移
- 値が入っている場合:登録済みとして検証フローに遷移
という分岐を設けておくと、「端末紛失による再登録」「強制再 enrolment」なども実現しやすくなります。
「ポップアップが出ない」状況の切り分け手順
ここからは、実際にどこから確認していくと効率的かをステップ順にまとめます。
Azure ポータルから「今すぐ実行」でテストする
最初におすすめなのが、フロントエンドを介さず、ポータルから直接ポリシーを実行してみる ことです。
- Azure ポータル > Azure AD B2C > カスタムポリシー
- 対象の RelyingParty ポリシー(例:B2C_1A_signup_signin_mfa)を選択
- 「今すぐ実行」をクリックして、認証画面が出るか確認
ここで認証画面が表示される場合は、ポリシー自体は動いている ので、問題の主戦場はフロントエンド(MSAL)側だと絞り込めます。
逆に、ここでエラーや空白画面になる場合は、カスタムポリシーの XML か、IEF / Proxy のアプリ登録に問題がある可能性が高くなります。
| 結果 | 疑うべき箇所 |
|---|---|
| ポータルから正常にログイン画面が表示される | MSAL の authority / clientId / redirectUri / ブラウザ設定 |
| ポータルからもエラーになる / 画面が出ない | カスタムポリシー XML、IEF・Proxy アプリ登録、ポリシーキー |
ブラウザ開発者ツールで Console / Network を確認
次に、フロントエンドから実行したときの挙動をブラウザの開発者ツールで確認します。
- Console タブ:MSAL のエラー(authority 不一致、構成ミス)が出ていないか
- Network タブ:
/oauth2/v2.0/authorizeへのリクエストが送られているか - レスポンスコード:302 / 200 / 401 / 500 など
たとえば authorize にそもそもリクエストが飛んでいない場合は、ボタンの onClick が発火していない、または MSAL 呼び出し前で例外が出ている可能性があります。
一方、authorize までは飛んでいるが 401 になっている場合は、Proxy → IEF への許可や管理者同意周りを再確認する必要があります。
Application Insights で IEF のトレースを見る(推奨)
本格的にカスタムポリシーを運用する場合は、Azure Application Insights に IEF のログを送る設定をしておくと非常に便利です。Correlation ID でトレースを追うことで、
- どの TechnicalProfile で止まっているか
- 外部 REST API 連携でエラーが起きていないか
- クレームの受け渡しが想定通りになっているか
といった情報が可視化できます。「ポップアップが出ない」問題の裏には、実は REST API 連携先での 500 エラーが隠れていた……ということもあるため、運用を見据えるならぜひ有効化しておきたいポイントです。
アプリ登録の再点検チェックリスト
最後に、アプリ登録周りで特に見落としやすいポイントをまとめます。
- SPA アプリのリダイレクト URI が 実際の URL と完全一致 しているか
- ProxyIdentityExperienceFramework に IEF API の 委任許可 + 管理者同意 が付与されているか
- Proxy アプリの「パブリック クライアント フロー」が有効化されているか
- 使用している MSAL のフロー(コード / 暗黙的)とアプリ登録の設定が合っているか
SMS / TOTP を 1 ポリシーで実装する際の設計ポイント
最後に、設計面の観点から「こうしておくと後々楽になる」というポイントをまとめます。
ユーザー選択 → 分岐 → 検証 → トークン発行の最小パターン
シンプルかつ拡張しやすい構成としては、次のような流れがベースになります。
- サインイン(ローカル / 外部 IdP)
- MFA 選択画面(SMS / TOTP)
- 選択に応じて SubJourney で分岐
- SubJourney_MfaWithSms
- SubJourney_MfaWithTotp
- 成功したら ID トークンを発行(
amrやmfaMethodなどをクレームに格納)
このとき、TOTP / SMS それぞれの SubJourney で「登録」と「検証」のステップを分けておくと、後から「登録済みユーザーは検証だけ」「特定条件で再登録を強制」といったロジックを組み立てやすくなります。
amr(Authentication Method Reference)で認証手段をトークンに載せる
下流アプリケーションで「このユーザーは SMS で認証したのか、TOTP で認証したのか」を知りたい場合は、カスタムポリシー側で amr クレーム(または独自の mfaMethod クレーム)を発行しておくと便利です。
- SMS の場合:
amr = ["mfa", "sms"] - TOTP の場合:
amr = ["mfa", "totp"]
こうしておけば、API 側で「TOTP で認証しているユーザーだけ追加の操作を許可」といったきめ細かな制御も可能になります。
UX の観点からの小さな工夫
- 初回ログイン時:MFA 種類の選択と同時に、TOTP の登録や電話番号の登録を一気に済ませる
- 2 回目以降:基本は前回と同じ認証手段に誘導しつつ、「別の方法でサインイン」リンクから切り替え可能にする
- 端末紛失時:SMS のみでリカバリーできるよう、TOTP のみ必須にはしない
ポリシー設計の自由度が高いぶん、UX を意識しないと「ユーザーには分かりづらいが管理者は満足」という状態になりがちです。フローを紙に書き出し、「ユーザーが何回クリックすれば終わるか」を数えながら設計すると、無駄なステップやループに気づきやすくなります。
まず試したい「最短チェックリスト」
最後に、今回の「ポップアップが出ない」問題に対して、最短で試せるチェックリストをまとめます。迷ったときは、この順番で確認してみてください。
- フロント(MSAL)の設定確認
- authority / knownAuthorities / clientId / redirectUri を整理
- いったん
loginRedirect()でフロー自体が動くか確認
- アプリ登録(IEF / Proxy)の許可確認
- IdentityExperienceFramework で API を公開(例:
user_impersonation) - ProxyIdentityExperienceFramework から上記スコープを委任
- 管理者同意が付与されているか再確認
- Proxy の Public client フローが有効か
- IdentityExperienceFramework で API を公開(例:
- ポータルの「今すぐ実行」でポリシー単体を実行
- これで画面が出るなら、問題はほぼ MSAL 側に絞り込める
- 出ない場合は、ポリシー XML / ポリシーキー / アプリ登録を重点確認
- ポリシー XML を IEF バリデーターで検証
- RelyingParty → DefaultUserJourney → OrchestrationStep → TechnicalProfile の紐づきに矛盾がないか
- 必要な ClaimType / ポリシーキーが定義されているか
- SMS / TOTP の TechnicalProfile 名がサンプルとズレていないか
- Application Insights でトレース(可能なら)
- どのステップでエラーになっているかを Correlation ID で追跡
- REST API 連携や外部システムとのやり取りも確認
この順にチェックしていけば、「サインインボタンを押してもポップアップが出ない」問題の原因は、かなりの確率で特定できます。そして一度この流れを掴んでおけば、SMS / TOTP の組み合わせに限らず、他のカスタムポリシーのトラブルシュートにも応用できるはずです。
まとめ
Azure AD B2C カスタムポリシーで SMS と TOTP を 1 ポリシーに統合する実装は、初見ではハードルが高く感じられますが、
- MSAL(フロントエンド)の設定
- IEF / Proxy のアプリ登録と権限
- カスタムポリシー XML(UserJourney / TechnicalProfile / ポリシーキー)
という 3 つのレイヤーに分解して考えると、一気に整理しやすくなります。
「ポップアップが出ない」という一見単純に見える症状の裏には、多様な原因が隠れていますが、本記事のチェックリストに沿って順番に潰していけば、必ずどこかで手がかりが見つかるはずです。
まずはポータルの「今すぐ実行」でポリシー単体を動かし、そのうえで MSAL の設定やアプリ登録を見直してみてください。
Azure AD B2C のカスタムポリシーは学習コストこそ高いものの、一度流れを掴めば非常に強力な認証基盤になります。SMS / TOTP の統合をきっかけに、自社にフィットした認証 UX を設計していきましょう。

コメント