Azure AD B2CをiFrameで埋め込んだ「Embedded sign-in」で、初回は成功するのにサインアウト後の再ログインで400 Bad Requestになる――このパターンは、フレーム制御(X-Frame-Options/CSP)とCookie制約、認証状態(state/nonce)のズレが複合して起きがちです。原因の切り分けと、実務で安定する構成をまとめます。
起きている現象と前提条件を整理する
今回の前提は次のような構成です。
- Azure AD B2C(カスタムポリシー)でサインインを実装
- サインイン選択肢が「ローカルアカウント(メール+パスワード)」と「外部テナントの Microsoft Entra ID(login.microsoftonline.com 系)」
- 通常遷移(トップレベルのリダイレクト)やAzure上でのテストでは問題なし
- iFrame埋め込み(Embedded sign-in)だと、初回は成功するが、サインアウト後の再ログインで 400 Bad Request が発生しやすい
この症状は「B2Cが壊れている」というより、iFrameに入れた瞬間にブラウザのセキュリティ機構(クリックジャッキング対策、サードパーティCookie規制、ストレージ分離など)の影響を強く受けることで、認証フローが前提通りに成立しなくなるケースが多いです。
最重要の前提:Embedded sign-in は「ローカルアカウント中心」で考える
Microsoft Learn の Embedded sign-up/sign-in(カスタムポリシー)では、iFrame埋め込み時の注意点として、以下が明確に書かれています。
- Embedded sign-up/sign-in はローカルアカウントのみをサポートする
- 多くの外部IdP(例:Google/Facebook)は、サインインページをiFrameで表示されることをブロックする
- Safari や Chrome のシークレット等では、iFrame内のB2CセッションCookieがサードパーティCookieとして扱われ、ブロック/削除されることがある
つまり「ローカルアカウントは(条件が揃えば)iFrameで成立し得るが、外部IdPは“そもそもiFrameに入らない”設計が多い」というのが現実です。Microsoftのログイン(login.microsoftonline.com)も、iFrame表示が拒否される代表例です。
| サインイン方式 | iFrame内で完結 | 実務上の注意点 |
|---|---|---|
| Azure AD B2C ローカルアカウント(メール+パスワード) | 条件付きで可能 | JourneyFraming設定、親側CSP、Cookieの扱い(同一サイト化)まで揃えないと不安定になりやすい |
| 外部IdP(Microsoft Entra ID / Google / Facebook など) | 基本的に難しい | ログイン画面側がクリックジャッキング対策でiFrame表示を拒否することが多い |
| 「ローカル+外部IdP」を同じ画面で見せてiFrame内で完結 | 非推奨(破綻しやすい) | 外部IdP選択時に別ドメインへ遷移し、そこでフレームブロックに引っかかる |
なぜ「2回目以降」に400 Bad Requestになりやすいのか
「初回は通るのに、サインアウト後の再ログインで400」という挙動は、次のどれか(または複合)で説明できることが多いです。
| 原因カテゴリ | 起きがちなタイミング | 見え方(兆候) | 確認ポイント | 対処の方向性 |
|---|---|---|---|---|
| フレーム表示ブロック(X-Frame-Options / CSP) | 外部IdPへ遷移した瞬間、または2回目以降の遷移 | コンソールに「Refused to display … in a frame because it set X-Frame-Options to ‘deny’」 | DevTools Console / Networkで対象レスポンスヘッダー確認 | 外部IdPはトップレベル遷移へ切替(iFrame完結を諦める) |
| Cookieが送られない(サードパーティCookie扱い) | Safari / シークレット / トラッキング防止が強い環境 | 初回は通るが、サインアウト→再ログインで状態不整合。B2C側が想定するセッション/相関情報が欠ける | Application → Cookies でB2CドメインのCookieが保存/送信されているか | 可能ならB2Cをカスタムドメイン化し同一サイト化、埋め込み依存を減らす |
| state/tx(StateProperties)など認証フローの「使い捨て状態」を再利用 | サインアウト後にiFrameをリロードせず再ログイン | /SelfAsserted などで 400。リロードすると直る | iFrameのURLが古い authorize / SelfAsserted のまま残っていないか | サインアウト後にiFrameを作り直す(srcを更新、キャッシュ無効化) |
| Cookie肥大(Request header too large) | ブラウザに大量のCookieがある環境 | 400で「Request Header or Cookie Too Long」等が出る | エラーページ文言 / Networkのレスポンス内容 | 対象ドメインのCookie削除、不要Cookieを増やさない設計 |
特にEmbedded sign-inは、B2C側が「Cookieベースのセッションをブラウザに保持する」ことを前提に動作します。iFrame内でそのCookieがブロックされたり、サインアウトのタイミングで想定通りに消えなかったりすると、フロー内部の相関チェック(CSRF対策など)が失敗して、結果として 400 が見えることがあります。
対処の方向性:Embedded sign-in を成立させるための最低ライン
JourneyFraming を正しく有効化する
カスタムポリシーでEmbedded sign-inを行う場合、RelyingParty配下に UserJourneyBehaviors を追加し、JourneyFraming で「どのサイトから埋め込みを許可するか」を明示します。ここが未設定/誤設定だと、フレーム制御の前提が崩れやすいです。
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignIn" />
<UserJourneyBehaviors>
<JourneyFraming Enabled="true"
Sources="https://app.example.com https://portal.example.com" />
</UserJourneyBehaviors>
...
</RelyingParty>
- Sourcesは完全一致のURI指定が必要(ワイルドカード非対応)
- https必須
- トップレベルドメイン(TLD)の文字数制限など、細かな制約がある
親ページ側のCSP(frame-src / child-src)とiFrame属性を確認する
「B2C側が埋め込みを許可していても、親ページ側のCSPが厳しくてブロックされる」ケースが現場ではよくあります。特に次を確認してください。
- 親ページの
Content-Security-Policyにframe-src(またはchild-src)制限があり、B2Cドメインが許可されていない - iFrameに
sandboxを付けていて、リダイレクトやフォーム送信、ポップアップが禁止されている
Embedded sign-inはリダイレクトやフォーム送信を多用します。sandbox を付ける場合は、少なくとも挙動に必要な許可(例:allow-forms, allow-scripts, allow-same-origin など)が足りているかを検証してください(過剰な許可はセキュリティ上のリスクになるため、最小限で)。
サードパーティCookie扱いを避ける(可能なら同一サイト化)
iFrame内のB2CセッションCookieは、ブラウザやモードによってはサードパーティCookieとして扱われ、ブロック/削除されることがあります。Microsoft Learnでもその点が明示されており、回避策として「アプリのドメインとB2Cのドメインを同一サイトに寄せる(カスタムドメインを使う)」方向が示されています。
ここで重要なのは、実装者が「同一オリジン」と「同一サイト(same-site)」を混同しないことです。ブラウザ仕様上、サブドメインが違えば厳密には同一オリジンではありませんが、Cookieのサードパーティ判定は“サイト(eTLD+1)”ベースで行われます。つまり、B2Cのカスタムドメインを自社ドメイン配下に置き、アプリと同じ“サイト”に寄せるのが現実的な改善策になります。
また、サードパーティCookie自体が主要ブラウザで段階的に制限・廃止へ向かっている点も無視できません。埋め込みログインに依存したUXは、将来的に壊れやすい設計になりがちです。
サインアウト後は「iFrameを必ずリセット」する
「初回は通るのに2回目で400」というとき、iFrame内部が“前回の認証フローの残骸”を握り続けているケースがよくあります。B2Cの画面は、内部的に state や tx=StateProperties といった“使い捨て状態”を持っており、古い状態を再利用するとエラーになりやすいです(ページをリロードすると直る現象が出やすい)。
対策としては、サインアウト時に次の2つをセットで行うのが実務で効きます。
- アプリ側セッションの破棄(自社Cookie/セッションを消す)
- iFrameを作り直す(srcを更新し、キャッシュも避ける)
// 親ページ側(例)
// ログアウトボタン押下時に、iFrameを“新規フロー”として再生成する
function resetLoginFrame() {
const iframe = document.getElementById('loginframe');
const ts = Date.now(); // キャッシュバスター
iframe.src = '/account/SignUpSignIn?ts=' + ts;
}
Microsoft Learnのサンプルでも、iFrame内で認証が終わった後はトップをリロードして状態を揃える流れが示されています。Embedded sign-inは「ページ状態の同期」を実装側が意識しないと破綻しやすい設計です。
「MSALをiFrameで動かす」構成は避ける
SPA構成で「iFrame内ページがMSALを動かしてトークン取得」までやろうとすると、さらに不安定になります。Microsoft Learnには、MSAL 2.0 をiFrameで動かすのは現時点でサポートされない旨が明記されています。
Embedded sign-inをやるなら、まずは「Webアプリ(サーバー側)で認可リクエストを作り、iFrameは表示面として使う」方式を優先し、SPA側はトップレベル遷移やポップアップを含めた“ブラウザ制約に強い”認証パターンに寄せるのが安全です。
外部のMicrosoft Entra ID(login.microsoftonline.com)をiFrame内で完結させるのが厳しい理由
結論から言うと、login.microsoftonline.com を別ドメインのままiFrame内に出して、MFAを含む対話型サインインを安定運用するのは現実的ではありません。
理由はシンプルで、Microsoftのログイン画面を含む多くのIdPが、クリックジャッキング対策としてフレーム内表示を拒否するからです。実際に「X-Frame-Options: DENY によりフレーム表示が拒否される」エラーは、Teams/Officeアドイン等のiFrame環境で頻出し、回避策として“ポップアップ等に逃がす”方向が案内されることが多いです。
「Refused to display ‘https://login.microsoftonline.com/…’ in a frame because it set ‘X-Frame-Options’ to ‘deny’.」が見えたら、基本的に“仕様でブロックされている”と考えてよいです。
ここでよく出る誤解が「X-Frame-Options をSAMEORIGINにしたらいけるのでは?」という発想です。しかし、外部IdP側(Microsoft側)のレスポンスヘッダーはあなたのアプリでは変更できません。結果として、外部IdP認証だけはトップレベル遷移(またはポップアップ)に切り替えるのが定番の落としどころになります。
おすすめの落としどころ構成:方式を分けて安定させる
「ローカルアカウントはiFrameでUXを軽く」「外部IdPはトップレベル(全画面)へ遷移して確実に」――この分離が、保守性と安定性のバランスが良い構成です。
| 構成案 | ユーザー体験 | 安定性 | 実装の要点 |
|---|---|---|---|
| ローカルのみiFrame(B2C Embedded sign-in) | 良い(画面遷移が少ない) | 中(ブラウザ制約の影響を受ける) | JourneyFraming、同一サイト化、ログアウト時のiFrameリセット |
| 外部IdPはトップレベル遷移(B2C経由) | 普通(全画面に切り替わる) | 高 | login.microsoftonline.com をiFrameに入れない。必要ならdomain_hintや別ポリシーで分岐 |
| 外部IdPをポップアップで実施 | 良い〜普通(環境差あり) | 中(ポップアップ制限・ユーザー操作要件) | ブラウザのポップアップ規制を考慮し、ユーザー操作起点で開く |
実装イメージは次のように整理すると分かりやすいです。
画面(親ページ)
├─[メールでログイン]→ iFrame表示(B2CローカルアカウントのEmbedded sign-in)
└─[Microsoftでログイン]→ window.top でB2Cポリシーへ遷移(外部Entra IDへはトップレベルで認証)
認証完了後
└─アプリに戻る → 親ページをリロード/状態同期
Embedded sign-inのドキュメントでも、外部IdPを使う場合は「別ポリシーに分ける」または「domain_hint で直接IdPへ飛ばす」などの発想が示されています。これを“iFrame完結”に寄せるより、“外部IdPだけトップレベル”に割り切る方が成功率が高いです。
トラブルシューティング:現場で効く確認手順
どのリクエストが400になっているかを特定する
- /oauth2/v2.0/authorize が400:リクエストが壊れている、Cookie肥大、または途中状態の不整合を疑う
- /SelfAsserted が400:古い tx/state を再利用している、またはセッションCookieが期待通りに扱えない可能性
Consoleの「Refused to display…」は“即、設計変更”のサイン
このメッセージが出る場合、アプリ側の工夫で何とかするより、iFrameをやめてトップレベル遷移/ポップアップへ切り替えるのが最短です。特にlogin.microsoftonline.com系はこのパターンに該当しやすいです。
Cookie・ストレージを確認する
- iFrame内ドメイン(B2C側)のCookieが保存されているか
- Safari/シークレット等でブロックされていないか
- サインアウト後にCookieが残り続けていないか(残っていると“状態ズレ”が起きやすい)
今後の注意点:埋め込みログイン依存はさらに厳しくなる
ブラウザはプライバシー保護の観点から、iFrameなどの埋め込みコンテンツに対するCookie送信をより厳しく扱う方向に進んでいます。埋め込みコンテンツのCookieがサードパーティ扱いになること、そしてサードパーティCookieが主要ブラウザで段階的に非推奨/制限されていることは、認証UXに直撃します。
さらに、Microsoft Learn のEmbedded sign-inページには「Azure AD B2C は新規顧客向けに購入不可になる」旨の注意も含まれています。新規でゼロから設計する場合は、将来のサービス計画も含めて“埋め込みに寄せない認証導線”を最初から選ぶ方が安全です。
まとめ
- Embedded sign-in(iFrame埋め込み)は、ローカルアカウント前提で成立させるべき機能で、外部IdPはブロックされやすい
- 「初回OK・2回目400」は、Cookie制約・古い認証状態の再利用・フレーム制御の不整合が原因になりやすい
- 実務上は「ローカルアカウントのみiFrame」「外部Entra IDはトップレベル遷移(またはポップアップ)」に分離するのが定番で安定する

コメント