Azure AD B2Cカスタムポリシーでトークンエンドポイント(/token)が失敗する原因と解決策(RestfulProvider/SendClaimsIn=Form)

Postmanでは成功するのに、Azure AD B2CカスタムポリシーのRESTful技術プロファイル(RestfulProvider)から /oauth2/v2.0/token(トークン エンドポイント)を呼ぶと、内部エラーやHTTP 400で失敗する――この症状は「送信形式」と「パラメータの渡し方」がズレたときに起きがちです。この記事では、Postmanと同じリクエストをB2C側で再現するための設定例と、400の切り分け手順を具体的にまとめます。

目次

想定する状況

この記事は、次のような状況で困っている方向けです。

  • Postmanからは、Azure AD B2C のトークン エンドポイント /oauth2/v2.0/token をパラメータ付きで正常に呼び出せる。
  • 同じ内容を Azure AD B2C カスタムポリシーの RestfulProvider で呼び出すと、AADB2C: An exception has occurredBadRequest (400)AADB2C90075 などで失敗する。
  • Application Insights を見ても「結局どのパラメータが足りないのか」が分かりにくい。

結論:Postmanで通ったリクエストと「送信形式」まで一致させる

トークン エンドポイント(OAuth 2.0 の /token)は、多くの場合 application/x-www-form-urlencoded(フォーム形式)でのPOSTを前提にしています。Microsoft Learn の client credentials flow の例でも、Content-Type: application/x-www-form-urlencoded と、Bodyを grant_type=...&client_id=...&... の形式で送ることが明記されています。

一方、B2CのRestfulProviderは、既定では入力クレームをJSONとして送ります。そのままでは「Postmanと別のリクエスト」になり、400(BadRequest)になりやすい、というのがこの問題の核心です。

まずは、「Postmanで成功したもの」を正とし、次の3点を完全一致させるのが最短ルートです。

  • HTTPメソッド(ほとんどの場合 POST
  • URL(テナント、ポリシー、パス/クエリの違い)
  • Bodyの形式(x-www-form-urlencoded か、JSONか)

PostmanとRestfulProviderの違いを一枚で把握する

観点Postmanで成功している状態RestfulProviderの落とし穴
Bodyの形式x-www-form-urlencoded(キー=値を&で連結)SendClaimsIn の既定は Body(JSON送信)なので、そのままだと別物になる
パラメータの渡し方Bodyのkey-valueに必要項目をすべて入れるMetadataに grant_type 等を書いても「Bodyに入る」とは限らない。基本は InputClaims で送る
不足時のエラーレスポンスのJSONに error/error_description が返るB2C側は AADB2C90075 のようなラップされたエラーで見えにくくなる

RestfulProviderのSendClaimsInを正しく選ぶ

RestfulProvider には入力クレームをどこに送るかを決める SendClaimsIn があります。トークン エンドポイントが相手なら、基本は SendClaimsIn="Form" を優先してください。

SendClaimsIn送信場所/形式HTTPメソッドトークン エンドポイント向き?
Body(既定)JSONとしてBodyに送るPOST多くの場合NG(application/json を受けない)
FormBodyにフォーム形式(&区切り)で送るPOST最有力
QueryStringURLのクエリ文字列に付けるGETほぼNG(/token はPOST前提)
Header / Urlヘッダー/URLパスに入れるGETほぼNG

grant_type別の必須パラメータ早見表

400を最短で潰すには、「いま叩いているのはどのフローか」を明確にし、必須パラメータを網羅するのが効果的です。B2Cのドキュメント上も、認可コードフローやclient credentialsフローで必要項目が整理されています。

grant_type主な用途必須になりやすいパラメータ補足
authorization_code認可コードをトークンに交換client_id, grant_type, code, redirect_uriPKCEなら code_verifier が必須になり得る。Webアプリでは client_secret も必要になりやすい
refresh_tokenリフレッシュトークンで更新client_id, grant_type, refresh_token, redirect_uriWebアプリでは client_secret が必要になりやすい
client_credentialsサーバー間(ユーザーなし)grant_type, client_id, client_secret, scopeclient credentials flow はプレビュー扱いと明記されている

「Metadataに書けば送られる」は誤解:任意パラメータはInputClaimsで送る

RestfulProviderのMetadataは、主に以下のような挙動制御のための領域です(ServiceUrl、AuthenticationType、SendClaimsIn、DebugModeなど)。一方、grant_typescope のような業務パラメータは、原則として InputClaims に定義して送ります。

ここを誤ると、Postmanでは送れていた必須パラメータがB2Cからは送られず、結果として400(BadRequest)になります。

実装例:RestfulProviderでB2Cの/tokenにPOST(Form)する

以下は「Postmanのフォーム送信」と同じ形を、B2Cカスタムポリシーで再現するための雛形です。ポイントは SendClaimsIn=”Form” と、パラメータを InputClaims で列挙する点です。

ClaimsSchemaに必要なClaimTypeを用意する

Partner(相手)が期待するパラメータ名(grant_type 等)をそのままClaimType Idにするより、ポリシー内では読みやすい名前にして PartnerClaimType でマッピングするほうが事故が減ります。

<ClaimsSchema>
  <ClaimType Id="oauthGrantType">
    <DisplayName>OAuth2 grant_type</DisplayName>
    <DataType>string</DataType>
  </ClaimType>

  <ClaimType Id="oauthClientId">
    <DisplayName>OAuth2 client_id</DisplayName>
    <DataType>string</DataType>
  </ClaimType>

  <ClaimType Id="oauthClientSecret">
    <DisplayName>OAuth2 client_secret</DisplayName>
    <DataType>string</DataType>
  </ClaimType>

  <ClaimType Id="oauthScope">
    <DisplayName>OAuth2 scope</DisplayName>
    <DataType>string</DataType>
  </ClaimType>

  <ClaimType Id="oauthCode">
    <DisplayName>OAuth2 code</DisplayName>
    <DataType>string</DataType>
  </ClaimType>

  <ClaimType Id="oauthRedirectUri">
    <DisplayName>OAuth2 redirect_uri</DisplayName>
    <DataType>string</DataType>
  </ClaimType>

  <ClaimType Id="oauthCodeVerifier">
    <DisplayName>OAuth2 code_verifier</DisplayName>
    <DataType>string</DataType>
  </ClaimType>

  <ClaimType Id="tokenAccessToken">
    <DisplayName>access_token</DisplayName>
    <DataType>string</DataType>
  </ClaimType>

  <ClaimType Id="tokenRefreshToken">
    <DisplayName>refresh_token</DisplayName>
    <DataType>string</DataType>
  </ClaimType>

  <ClaimType Id="tokenIdToken">
    <DisplayName>id_token</DisplayName>
    <DataType>string</DataType>
  </ClaimType>

  <ClaimType Id="tokenTokenType">
    <DisplayName>token_type</DisplayName>
    <DataType>string</DataType>
  </ClaimType>

  <ClaimType Id="tokenExpiresIn">
    <DisplayName>expires_in</DisplayName>
    <DataType>int</DataType>
  </ClaimType>
</ClaimsSchema>

RESTful技術プロファイル(Form送信)

<ClaimsProvider>
  <DisplayName>Token Endpoint Caller</DisplayName>
  <TechnicalProfiles>


<TechnicalProfile Id="REST-CallTokenEndpoint">
  <DisplayName>Call /token endpoint</DisplayName>
  <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

  <Metadata>
    <Item Key="ServiceUrl">https://{your-tenant}.b2clogin.com/{your-tenant}.onmicrosoft.com/{policy}/oauth2/v2.0/token</Item>
    <Item Key="AuthenticationType">None</Item>
    <Item Key="SendClaimsIn">Form</Item>
    <Item Key="DebugMode">true</Item>
    <!-- 本番(Production)でAuthenticationType=Noneを許可する必要がある場合 -->
    <!-- <Item Key="AllowInsecureAuthInProduction">true</Item> -->
  </Metadata>

  <InputClaims>
    <InputClaim ClaimTypeReferenceId="oauthGrantType"
                PartnerClaimType="grant_type"
                DefaultValue="authorization_code"
                AlwaysUseDefaultValue="true" />

    <InputClaim ClaimTypeReferenceId="oauthClientId"
                PartnerClaimType="client_id" />

    <InputClaim ClaimTypeReferenceId="oauthClientSecret"
                PartnerClaimType="client_secret" />

    <InputClaim ClaimTypeReferenceId="oauthScope"
                PartnerClaimType="scope" />

    <InputClaim ClaimTypeReferenceId="oauthCode"
                PartnerClaimType="code" />

    <InputClaim ClaimTypeReferenceId="oauthRedirectUri"
                PartnerClaimType="redirect_uri" />

    <!-- PKCEを使う場合のみ。Postmanで送っているならB2Cでも送る -->
    <InputClaim ClaimTypeReferenceId="oauthCodeVerifier"
                PartnerClaimType="code_verifier" />
  </InputClaims>

  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="tokenAccessToken" PartnerClaimType="access_token" />
    <OutputClaim ClaimTypeReferenceId="tokenRefreshToken" PartnerClaimType="refresh_token" />
    <OutputClaim ClaimTypeReferenceId="tokenIdToken" PartnerClaimType="id_token" />
    <OutputClaim ClaimTypeReferenceId="tokenTokenType" PartnerClaimType="token_type" />
    <OutputClaim ClaimTypeReferenceId="tokenExpiresIn" PartnerClaimType="expires_in" />
  </OutputClaims>

  <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>



重要: SendClaimsIn="Form" で送ると、InputClaimsは key=value&key=value... の形式でPOSTされます。ここが Postman(x-www-form-urlencoded)と揃うため、最も再現性が高くなります。

固定値と可変値を混ぜるコツ

トークン エンドポイントのパラメータには、毎回変わる値(codecode_verifier)と、基本固定の値(grant_typescope)が混ざります。B2Cでは、固定値は DefaultValueAlwaysUseDefaultValue="true" をセットで使うと安定します。

項目値の性質おすすめの入れ方典型例
grant_typeほぼ固定DefaultValue + AlwaysUseDefaultValueauthorization_code, client_credentials, refresh_token
client_id固定(環境で変わる)入力クレームとして渡す(環境差分管理)アプリ登録のApplication (client) ID
client_secret固定(秘匿)可能なら外部に秘匿し、B2Cからは直接持たないアプリのクライアントシークレット
code毎回変わる/一度きり前段のステップで受け取り、そのままInputClaimsへ認可コード
redirect_uri厳密一致が必要Postmanと同じ値を送る(完全一致)https://example.com/callback
code_verifierPKCE時に必須になり得る使っているなら必ず送る43文字以上のランダム文字列

400(BadRequest)になったときの定番チェック

HTTP 400 は「リクエストが相手の期待と合っていない」ことを意味します。B2Cのエラー表示では AADB2C90075 としてラップされることが多く、まずは “B2Cが受け取ったHTTPステータス”“トークン エンドポイントが返したエラー本文” を取りにいくのが近道です。

AADB2C90075は「REST呼び出しがHTTPエラーだった」という意味

AADB2C90075 は「指定したClaimsExchangeが、HTTPエラー(コード/理由)を返した」ことを示す一般的なエラーです。つまり根本原因は、URL、送信形式、必須パラメータ不足、認証方式の不一致など、REST呼び出し側にあります。

トークン エンドポイントで400になりやすい原因

症状ありがちな原因確認ポイント対処
unsupported_grant_type / AADB2C90086grant_typeが誤り、またはJSON送信で解釈されていないPostmanではx-www-form-urlencodedか? B2CはSendClaimsIn=Formか?SendClaimsIn="Form" に揃え、grant_type をInputClaimsで送る
missing required parameter / AADB2C90083client_id / scope / code などが送れていないInputClaimsに定義があるか、値がその時点で存在するかPartnerClaimTypeで名前を合わせ、DefaultValue/変数の流れを見直す
invalid_client / AADB2C90079confidential clientなのにclient_secretを送っていない、または誤りWebアプリ/サーバーサイドならsecret必須になりやすいclient_secretを送る。secret運用(保管/ローテーション)もセットで設計
invalid_grant / AADB2C90080codeの期限切れ、二重消費、policy違い、redirect_uri不一致コードは一度きり、取得したuser flow/policyと同じものかコード取得〜交換の経路を一貫させ、redirect_uriを完全一致させる
code_verifier関連 / AADB2C90182PKCEを使っているのにcode_verifierが無い/不一致Postmanでcode_verifierを送っているかPKCE利用時はcode_verifierを必ず送る
POST必須 / AADB2C90226誤ってGETで叩いている(QueryString/Url/Header送信)SendClaimsInがForm/BodyになっているかPOSTになる送信方式を使う(通常はForm)

DebugModeとApplication Insightsで原因を見える化する

RestfulProviderには DebugMode があります。REST API側が返す詳細(developerMessage等)をB2C側で扱いやすくするための仕組みで、トラブルシュート中は有効にしておくと切り分けが早くなります(本番では情報過多になり得るため、切り戻し前提で運用してください)。

また、B2Cのエラーメッセージには Correlation ID が出ます。Application Insightsを有効にしている場合、Correlation IDを軸に追うと「どの技術プロファイルが落ちたか」「どこで400になったか」を見つけやすくなります。

(例)Application InsightsでCorrelation IDを追うKustoクエリ

traces
| where message has "Correlation ID"
| where message has "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
| order by timestamp desc

ログにトークン値やシークレットが出ないように、REST側のログ設計も合わせて見直してください。特に access_tokenrefresh_token をレスポンスから受け取る場合、開発時のログ出力がそのまま本番に残ると情報漏えいにつながります。

シークレットをポリシーに直書きしたくない場合の現実的な対処

「client_secretをInputClaimsで送りたいが、ポリシー内にベタ書きしたくない」という悩みはよくあります。Microsoft Q&Aでも、Policy keysはCryptographicKeysからしか参照できず、InputClaimとしては参照できないという回答が出ています。

この制約があるため、次のいずれかで解決するのが現実的です。

方針メリット注意点向いているケース
Basic認証で送る(Authorizationヘッダー)Policy keysで管理しやすい相手のトークンエンドポイントがBasic認証を受け付ける必要がある一般的なOAuth/OIDCサーバーで、RFC通りにBasicに対応している
プロキシ(Azure Functions / API Management等)を挟むB2CからはAPIキー等で呼び、secret注入はプロキシ側で実施できる構成が増える。レイテンシと可用性の設計が必要相手がbodyにclient_secret必須、かつB2C側にsecretを置けない
割り切ってポリシーに直書き(非推奨)最短で動く漏えい・ローテーション・監査の観点で危険検証のみ(短期)

送信形式以外の落とし穴

Form送信に直してもまだ400になる場合は、OAuth特有の“細かい一致条件”を疑います。特に次の3つは頻出です。

redirect_uriの完全一致

redirect_uri は、登録済みの値と完全一致(URLエンコード差分も含む)を求められます。Postmanで成功した値と、B2Cから送っている値が1文字でも違うと失敗します。

認可コード(code)は一度きり

code は基本的に短命で、かつ一度交換すると再利用できません。「Postmanで試した後に、同じcodeをB2Cでも使う」ような手順だと、B2C側が正しくても invalid_grant になります。検証時は、毎回新しいcodeを発行してから交換してください。

PKCE利用時のcode_verifier

PKCE(特にSPAやモバイル)では、code_verifier が必須になるケースがあります。Postmanで送っているのにB2Cで送っていない、または値が一致していないと失敗します。

レスポンスの受け取りはOutputClaimsでマッピングする

/token の成功レスポンスはJSONです。RestfulProviderはOutputClaimsでJSONのプロパティ名(access_token など)を PartnerClaimType に指定することで、そのままクレームに取り込めます。ここは送信側(Form/Body)よりも相性が良く、設定ミスも発見しやすいポイントです。

ただし、受け取ったトークンをセッションに永続化すると、cookieサイズやログ出力の面で事故りやすくなります。上の例のように SM-Noop を使い、必要な場面でだけ後続に渡す設計にしておくと安全です。

最終チェックリスト

  • SendClaimsInはForm になっている(Body/QueryStringにしていない)。
  • 必須パラメータは InputClaims に列挙し、PartnerClaimType で名前を一致させた。
  • 固定値は DefaultValue + AlwaysUseDefaultValue で確実に送る。
  • redirect_uri はPostmanと完全一致。
  • PKCEを使うなら code_verifier を送っている。
  • Correlation IDを起点にApplication Insightsで追える状態にしている。
  • シークレットの保管方針(Basic/プロキシ等)を決めている。

参考リンク

この記事を書いた人

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

コメント

コメントする

目次