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 occurred や BadRequest (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 を受けない) |
| Form | Bodyにフォーム形式(&区切り)で送る | POST | 最有力 |
| QueryString | URLのクエリ文字列に付ける | 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_uri | PKCEなら code_verifier が必須になり得る。Webアプリでは client_secret も必要になりやすい |
| refresh_token | リフレッシュトークンで更新 | client_id, grant_type, refresh_token, redirect_uri | Webアプリでは client_secret が必要になりやすい |
| client_credentials | サーバー間(ユーザーなし) | grant_type, client_id, client_secret, scope | client credentials flow はプレビュー扱いと明記されている |
「Metadataに書けば送られる」は誤解:任意パラメータはInputClaimsで送る
RestfulProviderのMetadataは、主に以下のような挙動制御のための領域です(ServiceUrl、AuthenticationType、SendClaimsIn、DebugModeなど)。一方、grant_type や scope のような業務パラメータは、原則として 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)と揃うため、最も再現性が高くなります。
固定値と可変値を混ぜるコツ
トークン エンドポイントのパラメータには、毎回変わる値(code や code_verifier)と、基本固定の値(grant_type や scope)が混ざります。B2Cでは、固定値は DefaultValue と AlwaysUseDefaultValue="true" をセットで使うと安定します。
| 項目 | 値の性質 | おすすめの入れ方 | 典型例 |
|---|---|---|---|
| grant_type | ほぼ固定 | DefaultValue + AlwaysUseDefaultValue | authorization_code, client_credentials, refresh_token |
| client_id | 固定(環境で変わる) | 入力クレームとして渡す(環境差分管理) | アプリ登録のApplication (client) ID |
| client_secret | 固定(秘匿) | 可能なら外部に秘匿し、B2Cからは直接持たない | アプリのクライアントシークレット |
| code | 毎回変わる/一度きり | 前段のステップで受け取り、そのままInputClaimsへ | 認可コード |
| redirect_uri | 厳密一致が必要 | Postmanと同じ値を送る(完全一致) | https://example.com/callback 等 |
| code_verifier | PKCE時に必須になり得る | 使っているなら必ず送る | 43文字以上のランダム文字列 |
400(BadRequest)になったときの定番チェック
HTTP 400 は「リクエストが相手の期待と合っていない」ことを意味します。B2Cのエラー表示では AADB2C90075 としてラップされることが多く、まずは “B2Cが受け取ったHTTPステータス” と “トークン エンドポイントが返したエラー本文” を取りにいくのが近道です。
AADB2C90075は「REST呼び出しがHTTPエラーだった」という意味
AADB2C90075 は「指定したClaimsExchangeが、HTTPエラー(コード/理由)を返した」ことを示す一般的なエラーです。つまり根本原因は、URL、送信形式、必須パラメータ不足、認証方式の不一致など、REST呼び出し側にあります。
トークン エンドポイントで400になりやすい原因
| 症状 | ありがちな原因 | 確認ポイント | 対処 |
|---|---|---|---|
| unsupported_grant_type / AADB2C90086 | grant_typeが誤り、またはJSON送信で解釈されていない | Postmanではx-www-form-urlencodedか? B2CはSendClaimsIn=Formか? | SendClaimsIn="Form" に揃え、grant_type をInputClaimsで送る |
| missing required parameter / AADB2C90083 | client_id / scope / code などが送れていない | InputClaimsに定義があるか、値がその時点で存在するか | PartnerClaimTypeで名前を合わせ、DefaultValue/変数の流れを見直す |
| invalid_client / AADB2C90079 | confidential clientなのにclient_secretを送っていない、または誤り | Webアプリ/サーバーサイドならsecret必須になりやすい | client_secretを送る。secret運用(保管/ローテーション)もセットで設計 |
| invalid_grant / AADB2C90080 | codeの期限切れ、二重消費、policy違い、redirect_uri不一致 | コードは一度きり、取得したuser flow/policyと同じものか | コード取得〜交換の経路を一貫させ、redirect_uriを完全一致させる |
| code_verifier関連 / AADB2C90182 | PKCEを使っているのに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_token や refresh_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/プロキシ等)を決めている。

コメント