MSAL-React + Azure AD B2CでAADSTS9002325(PKCE必須)を解消する原因と対処法

MSAL-ReactでAzure AD B2C(Microsoft Entra ID B2C)にサインインしようとしたら、突然「AADSTS9002325: Proof Key for Code Exchange is required for cross-origin authorization code redemption.」が出て先に進めない。リダイレクトURIは合っているのに…というときは、B2Cの外部IDプロバイダーとして登録しているMicrosoft Entra ID(旧 Azure AD)側アプリ登録の設定ミスが原因であることが多いです。

目次

AADSTS9002325とは何か:エラー文の意味を噛み砕く

「AADSTS9002325」は、Microsoft Entra IDのサインイン処理で使われるOAuth 2.0 / OpenID Connectの認可コードフローにおいて、認可コードの引き換え(token endpointでのcode redemption)の際にPKCE(Proof Key for Code Exchange)が必須なのに付いていない、または“SPAとして扱われているクライアント”がクロスオリジンで認可コードを引き換えようとしている、と判定されたときに出やすいエラーです。

ここで重要なのは、エラーが出たからといって必ずしも「フロント(React)のPKCE実装が悪い」とは限らない点です。特にAzure AD B2Cで外部IDプロバイダーとしてEntra IDを使う構成では、B2C側が“仲介役”としてEntra IDとサーバーサイドでやり取りします。そのため、Entra ID側アプリ登録のプラットフォーム種別がズレていると、フロントのredirectUriをどれだけ直しても改善しない、という現象が起きます。

結論:Azure AD B2CのIDプロバイダーとして使うMicrosoft Entra ID側アプリは、SPAではなくWebアプリとして登録し、https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/oauth2/authrespを正しくリダイレクトURIに設定すると、AADSTS9002325は解消しやすくなります。

今回の前提構成(質問の状況)

  • フロントエンド:MSAL-Reactを使ったSPA(シングルページアプリケーション)
  • SPAのredirectUri:http://localhost:3000/
  • 認証基盤:Azure AD B2C(Microsoft Entra ID B2C)
  • B2Cのユーザーフローで、外部IDプロバイダーとしてMicrosoft Entra ID(旧Azure AD)を使用
  • サインイン時に AADSTS9002325 が発生

まず整理:この構成は「アプリ登録が2種類」出てくる

今回のように「フロントはSPA」「B2Cユーザーフローで、IDプロバイダーとしてEntra IDを使う」場合、よくある落とし穴は“どのアプリ登録の話か”が混ざることです。結論から言うと、MSAL-Reactで使うSPAアプリと、B2CのIDプロバイダーとして使うEntra ID側アプリは、役割が別物です。

登場人物どこに作る役割代表的なリダイレクトURIクライアント種別よくあるミス
B2Cテナント側:SPAアプリ(MSAL-Reactが使う)Microsoft Entra ID B2C テナントReact SPAがB2Cにサインインするためのアプリ登録http://localhost:3000/(開発時)SPA(パブリッククライアント)ここにauthrespを入れようとする/逆にlocalhostを入れ忘れる
Entra IDテナント側:IDプロバイダー用アプリ(B2Cが使う)Microsoft Entra ID(旧Azure AD)テナントB2Cが外部IDプロバイダー(Entra ID)へリダイレクトするためのアプリ登録https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/oauth2/authrespWeb(コンフィデンシャルクライアント)このアプリをSPAで作ってしまい、AADSTS9002325が出る

今回のポイントは後者、つまり「B2CのIDプロバイダーとして使うEntra ID側アプリ」の設定です。フロントのredirectUri: http://localhost:3000/が正しくても、ここがズレていると失敗します。

根本原因:Entra ID側アプリ登録のプラットフォームが「SPA」になっている

Azure AD B2CからEntra IDを外部IDプロバイダーとして使うと、B2Cは「ユーザーをEntra IDへ送る → Entra IDが認証する → B2Cの特定エンドポイントへ戻す」という流れで動きます。この“戻し先”が、いわゆるauthrespのURLです。

このとき、Entra ID側アプリ登録が「シングルページアプリケーション(SPA)」として構成されていると、Entra IDはそのアプリをパブリッククライアントとして扱い、認可コードの引き換えに対してPKCE必須の条件を厳密に適用しやすくなります。一方、B2Cが外部IDプロバイダーと連携するときは、B2Cサービスが“仲介役”としてサーバーサイドでトークン交換する前提(=コンフィデンシャルクライアント前提)であるため、結果としてフローが噛み合わずAADSTS9002325が発生します。

観点SPAとして登録した場合Webとして登録した場合B2CのIDプロバイダー用途
クライアントの扱いパブリッククライアント(秘密情報を安全に保持できない前提)コンフィデンシャルクライアント(クライアントシークレット等を保持できる前提)Webが適合
想定されるトークン交換ブラウザ/SPAが直接トークンエンドポイントへサーバーサイドでトークンエンドポイントへB2Cが仲介してサーバー側で交換
PKCEの位置づけ必須になりやすい(条件が厳しい)推奨だが必須ではないケースが多い設定の噛み合いが重要

解決策:Entra ID側アプリ登録を「Web」プラットフォームで正しく構成する

対処はシンプルで、Entra IDテナント側のアプリ登録を「Web(Webアプリ)」として設定し、B2CのauthrespをリダイレクトURIとして登録することです。ここが整うと、AADSTS9002325は解消するケースが多いです。

手順:Entra ID(旧Azure AD)側での設定

  1. Microsoft Entra 管理センターを開き、アプリ登録から対象アプリを選びます。
  2. 左メニューの認証を開きます。
  3. 「プラットフォーム構成」でプラットフォームを追加を選び、Webを追加します。
  4. WebのリダイレクトURIに、次を登録します(すべて小文字で統一するのが安全です)。
あわせて読みたい
  • your-b2c-tenant-name は実際のB2Cテナント名に置き換えます。
  • 大文字・小文字、末尾スラッシュの有無など、完全一致が前提です。迷ったらB2Cポータルに表示されるコールバックURLをコピーし、同じ表記で登録します。
  • 外部IDプロバイダー用アプリにSPAプラットフォームが残っていても必ずしも致命的ではありませんが、混乱の原因になるため、運用上はWebに寄せて整理するのがおすすめです。

設定を間違えやすいポイント(短時間で見直す用)

項目推奨NG例結果
プラットフォーム種別WebSPAのみAADSTS9002325が出やすい
リダイレクトURI.../oauth2/authresp(B2Cが提示する値)http://localhost:3000/ を登録そもそもB2Cへ戻れない/別エラーになる
大文字・小文字小文字で統一(特にb2clogin.comとテナント名)一部が大文字一致判定で弾かれることがある
クライアントシークレット作成し、期限切れを避ける未作成/期限切れ別の認証エラー・連携失敗に繋がる

クライアントシークレットでつまずかないための実務ポイント

B2Cが外部IDプロバイダー(Entra ID)へ接続する設定では、クライアントシークレットを使うことが一般的です。シークレット関連のミスは、AADSTS9002325とは別のエラーを引き起こすこともありますが、設定のやり直し時に一緒に潰しておくと再発防止になります。

シークレット作成の手順(Entra ID側)

  1. Entra ID側アプリ登録を開き、証明書とシークレットへ進みます。
  2. 新しいクライアント シークレットを作成し、用途が分かる説明と有効期限を設定します。
  3. 作成直後に表示される値(Value)を安全な場所に保存します(後から再表示できないため)。
  4. B2C側のIDプロバイダー設定に、その値を貼り付けます。
落とし穴起きがちなこと回避策
Secret ID と Value の取り違えID(識別子)を貼り付けてしまい、認証が通らないValue(実際の秘密文字列)を保存して使う
期限切れ数か月後に突然ログインできなくなる更新手順を運用に組み込む/期限を管理する
共有しすぎ別環境・別用途で同じシークレットを使い回し、影響範囲が広がる環境・用途ごとに分け、最小権限で管理する

B2C側(ユーザーフロー/IDプロバイダー設定)で確認すること

Entra ID側をWebで整えたら、次はB2C側で「そのアプリ登録を正しく参照できているか」を確認します。B2Cのユーザーフローで、IDプロバイダーとしてMicrosoft Entra IDを設定している箇所が該当します。

  • クライアントID:Entra ID側アプリ登録の「アプリケーション(クライアント)ID」と一致しているか
  • クライアントシークレット:Entra ID側で作成したValueと一致し、期限切れではないか
  • リダイレクトURIの整合:Entra ID側Webに登録したauthrespと、B2Cが利用するコールバックURLが一致しているか
  • テナントの取り違え:B2Cテナントではなく、外部IDプロバイダー(Entra ID)のテナントにアプリ登録しているか

「B2Cテナントに作ったSPAアプリ」と「外部IDプロバイダーとして使うEntra ID側アプリ」を混同すると、クライアントIDやシークレットが噛み合わず、エラーが連鎖します。まずは先ほどの表に立ち返って、どちらのアプリの設定を触っているのかを都度確認するのがおすすめです。

MSAL-React(SPA)側の設定:Implicitではなく、認可コードフロー+PKCEを前提にする

MSAL-React(msal-browser v2系)は、基本的に認可コードフロー(Authorization Code Flow)+PKCEを使います。つまり、古いMSAL.js 1.xのような「暗黙的フロー(Implicit Flow)」を前提に設定をいじると、かえって噛み合わなくなることがあります。

暗黙的フロー(Implicit)のチェックは必要?

利用ライブラリ推奨フロー「暗黙的な許可およびハイブリッド フロー」備考
MSAL-React / msal-browser 2.x認可コード+PKCEIDトークン:オフ、アクセス トークン:オフImplicitに寄せない(コードフローに一本化)
MSAL.js 1.x(レガシー)Implicit(必要時のみ)用途に応じてオン新規構築では非推奨になりやすい

MSAL-Reactの設定例(B2Cユーザーフローをauthorityにする)

以下は、B2Cに対してMSAL-Reactでサインインする際の設定例です。ポイントは、authorityがB2Cのユーザーフロー(ポリシー)を指していることと、knownAuthoritiesにb2clogin.comのドメインを入れること、そしてredirectUriが開発環境ならhttp://localhost:3000/で一致していることです。

export const msalConfig = {
  auth: {
    clientId: "(B2Cテナント側SPAアプリのクライアントID)",
    authority: "https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/B2C_1_signupsignin",
    knownAuthorities: ["{tenant}.b2clogin.com"],
    redirectUri: "http://localhost:3000/",
    postLogoutRedirectUri: "http://localhost:3000/"
  },
  cache: {
    cacheLocation: "localStorage"
  }
};

ここでのclientIdはB2Cテナント側のSPAアプリです。AADSTS9002325の解消で触った「外部IDプロバイダー用のEntra IDアプリ」とは別である点に注意してください。

環境別にズレが出やすいポイント(localhost/検証/本番)

本番に進むほど、リダイレクトURIの登録漏れや表記ブレが起きやすくなります。環境ごとに「どこに何を登録するか」を決め打ちしておくと、原因究明の時間を大きく削れます。

環境SPA側 redirectUri(B2CテナントのSPAアプリ)Entra ID側 redirectUri(IDプロバイダー用Webアプリ)注意点
開発http://localhost:3000/https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/oauth2/authresp末尾スラッシュやポートの違いで失敗しやすい
検証https://stg.example.com/https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/oauth2/authrespSPA側は環境ごとに追加登録が必要
本番https://example.com/https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/oauth2/authrespHTTPS必須、ドメイン移行時は登録更新が必要

ポイントは、外部IDプロバイダー用(Entra ID側)のリダイレクトURIが「SPAのURL」ではなくB2Cのauthrespである点です。環境が増えると、SPA側のURIだけ増える一方で、IDプロバイダー側のURIは基本的にauthrespで固定、という整理になることが多いです。

なぜ「Web」にするだけで直るのか:フローを一度だけ頭の中で通す

原因が腑に落ちないと、再発時に同じ罠にはまります。ここでは、最小限の流れだけ押さえます。

  1. React SPAはB2Cの/authorizeにアクセスし、B2Cユーザーフローを開始する
  2. B2Cは設定された外部IDプロバイダー(Entra ID)へリダイレクトする
  3. ユーザーはEntra IDでサインインし、Entra IDはB2Cのauthrespへ戻す
  4. B2Cは受け取った結果を使って、最終的にSPAへトークン(またはコード)を返す

このとき、Entra ID側アプリ登録が「SPA」だと、Entra IDは“ブラウザでコードを引き換える想定”を強く持ちます。しかし実際にはB2Cが仲介してサーバー側で処理するため、PKCE要件などの条件がぶつかり、AADSTS9002325に繋がりやすくなります。Webとして登録し直すことで、B2Cが想定する「外部連携用(コンフィデンシャル)」の振る舞いと整合が取れます。

似た構成で詰まりやすいチェックリスト

同じAADSTS9002325でも、原因が複合していることがあります。以下の順で確認すると切り分けが速いです。

どのアプリ登録がどの役割かを、先に固定する

  • B2Cテナント:React SPA用アプリ(http://localhost:3000/を登録する側)
  • Entra IDテナント:B2Cの外部IDプロバイダー用アプリ(.../oauth2/authrespを登録する側)

確認ポイントを「場所」ごとに見る

確認場所チェック項目具体例対処の方向性
Entra ID側アプリ登録(IDプロバイダー用)プラットフォームがWebかWebにauthrespが登録されているSPAではなくWebへ寄せる
Entra ID側アプリ登録(IDプロバイダー用)リダイレクトURIが完全一致か大文字混入、別テナント名、末尾の差分B2Cが提示する値をそのままコピー
B2C側:IDプロバイダー設定クライアントID/シークレットの一致別アプリのIDを入れている対象アプリを特定して入れ直す
B2Cテナント側:SPAアプリ登録http://localhost:3000/が登録されているか末尾スラッシュ違いで失敗実際のredirectUriと同じ表記に揃える
MSAL-React側authorityがB2Cユーザーフローになっているかlogin.microsoftonline.comを直叩きしているB2Cのauthorityに統一する

実務で効くデバッグ方法:ブラウザのネットワークで「どこで落ちているか」だけ見る

エラーメッセージの文字面だけ追うと、MSAL側を疑って遠回りしがちです。開発者ツールのNetworkタブで、次の2点だけ見ると原因箇所が絞れます。

  • 最後に成功したリダイレクト先はどこか(B2C → Entra ID → B2C authresp のどこで止まったか)
  • AADSTS9002325が返ってきたのはどのドメインか(Entra ID側のエンドポイントから返っているなら、Entra ID側アプリ設定が濃厚)

今回のケースでは、Entra ID側のアプリ登録がSPA扱いになっていることが主因なので、B2Cへ戻すauthrespのリダイレクトや、認可コードの引き換えタイミングで失敗しているログが見つかりやすいです。逆に、SPAがB2Cへ戻れない(localhostに返ってこない)なら、B2Cテナント側SPAアプリのリダイレクトURI不一致を疑います。

よくある質問

authrespのURLは、なぜ「全部小文字」が推奨なのですか?

B2C連携ではリダイレクトURIが厳密に一致判定されます。環境によっては大文字・小文字の違いが想定外の不一致として扱われることがあるため、テナント名やドメイン部分を小文字で統一しておくと、余計な差分を排除できます。迷ったら、B2C管理画面に表示されるコールバックURLをコピーして同じ表記に揃えるのが確実です。

SPA用アプリと、IDプロバイダー用アプリを1つにまとめられますか?

理屈の上では整理できる場面もありますが、実務では役割が違うため分けた方が事故が少ないことが多いです。SPAは公開クライアントとして扱うのが前提で、リダイレクトURIもlocalhostや本番のフロントURLになります。一方、B2Cの外部IDプロバイダー用途はB2Cのauthrespが中心で、シークレット管理や設定箇所が異なります。混ぜると「どのURIをどこに入れるか」が破綻しやすくなります。

設定を変えたのに直らないときは何をすべき?

設定変更後にブラウザのセッションやMSALのキャッシュが残っていると、古い状態のまま見えることがあります。まずはシークレットの貼り間違いがないかを再確認したうえで、シークレットやリダイレクトURIを変えたタイミングでは、別プロファイルやシークレットウィンドウで試すと切り分けが早くなります。

まとめ:AADSTS9002325を最短で直す要点

  • AADSTS9002325が出たら、まず「どのアプリ登録の話か」を分ける(SPA用と、IDプロバイダー用)
  • B2Cの外部IDプロバイダーとして使うEntra ID側アプリは、SPAではなくWebとして登録する
  • Entra ID側Webプラットフォームに、https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/oauth2/authrespを(表記まで含めて)正確に登録する
  • MSAL-React(msal-browser 2.x)ではImplicitを前提にせず、認可コード+PKCEの設定に寄せる

リダイレクトURIを何度見直しても直らないときほど、原因はフロントのredirectUriではなく、「B2Cが外部IDプロバイダーとして使うEntra ID側アプリ登録がSPAになっている」という取り違えにあることが多いです。設定の役割分担を固定し、Webプラットフォームでauthrespを入れ直すところから見直すと、スムーズに解決できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次