Microsoft Entra ID(旧 Azure AD)の OIDC(OpenID Connect)認証で、「認可コードは取れるのに id_token が返らない」「結果としてアプリ側の JWT デコードが The token is null で落ちる」という事象は、設定とリクエストの噛み合わせで起きがちです。この記事では、実際に解消した手順(response_type と acceptMappedClaims)を軸に、再発しないための確認ポイントまで整理します。
現象:/authorize ではコードが取れるが、/token で id_token が返ってこない
今回の状況を、発生事象→影響の形で整理すると以下です。
| フェーズ | 期待 | 実際 | 影響 |
|---|---|---|---|
| /authorize | 認可コード(authorization code)取得 | 認可コードは取得できる | 一見フローは正常に見える |
| /token | access_token と(必要なら)id_token の取得 | access_token は返るが id_token が null / 欠落 | ログイン成立の判断材料が欠ける |
| アプリ側処理 | id_token を JWT として検証・デコード | com.auth0.jwt.exceptions.JWTDecodeException: The token is null | 認証処理が例外で停止 |
ポイントは「アクセストークンが取れている=ログイン成功」ではない点です。アクセストークンは “API を呼ぶための鍵” であり、ログイン(誰としてサインインしたか)の証明は基本的に id_token が担います。
まず押さえる:access_token と id_token の役割の違い
id_token が返らない問題を早く解くには、2種類のトークンの用途を切り分けるのが最短です。
| 項目 | access_token | id_token |
|---|---|---|
| 目的 | API(例:Microsoft Graph / 自社API)にアクセスする | 「このユーザーでサインインした」をアプリが確認する |
| 主な受け手 | API サーバー | クライアント / アプリ(サインイン処理) |
| 中身の例 | 権限(scp / roles)、対象リソース、期限など | ユーザー識別(sub)、発行者(iss)、対象(aud)、nonce など |
| 検証の観点 | API 側で署名・発行者・aud・スコープ/ロールを確認 | アプリ側で署名・発行者・aud・nonce を確認(セッション確立) |
| 「返ってくる/こない」問題の切り分け | API のスコープ要求が通っていれば返りやすい | OIDC として正しく要求していないと返らない/ブロックされる |
つまり、id_token がない状態で “ログイン完了” 扱いにしている実装だと、ちょっとした設定差で一気に崩れます。今回のように、JWT ライブラリが null を受け取って例外を投げるのは典型です。
Entra ID のフロー差分:id_token は「どこで」返るのか
OpenID Connect にはいくつかのバリエーションがあり、id_token が返るタイミングも変わります。混乱しやすいので、まずは “どのフローを採用しているか” を明確にするのが重要です。
| フロー | /authorize | /token | 特徴 | 向いているケース |
|---|---|---|---|---|
| 認可コードフロー(基本) | code | access_token(+通常は id_token も返り得る) | バックチャネルでトークン交換。PKCE と相性が良い | Web/ネイティブ/SPA(PKCE) |
| ハイブリッドフロー | code + id_token(フロントチャネルで id_token も返す) | access_token(+必要なら追加のトークン) | 認可レスポンスで id_token を先に受け取る | 要件上、早期に id_token が必要な構成 |
| (参考)インプリシットフロー | id_token / access_token | (交換なし) | フロントチャネルで直接トークン受領。設計上の注意が多い | 新規採用は慎重に |
今回の解決は、サポート指摘に従って 「/authorize で id_token を明示的に要求する(ハイブリッドフロー)」に切り替えたことが第一歩でした。
切り分けの最短ルート:id_token が返らないときに最初に見るべきチェック表
“設定は見直したのに原因が分からない” 状況では、/authorize と /token のリクエスト内容が実際にどうなっているか(ログ/トレース)を起点に切り分けるのが効果的です。以下は実務で使いやすい確認表です。
| チェック項目 | 見る場所 | OKの目安 | NGだと起きがちなこと |
|---|---|---|---|
| scope に openid が含まれる | /authorize のクエリ | scope=openid ... | OIDC として扱われず id_token が発行されない |
| id_token をどこで受け取る設計か | フロー設計 | (A)/token で受け取る or(B)/authorize で受け取る | 実装とフローがズレて “ないはずの場所” を探し続ける |
| response_type の指定 | /authorize のクエリ | 今回の解決では code id_token | id_token が返らず、アプリ側が null を処理できず落ちる |
| nonce の有無 | /authorize のクエリ | nonce を付与し、検証する | id_token の検証が通らない/リプレイ対策が弱い |
| redirect_uri の一致 | /authorize と /token | 完全一致(末尾スラッシュ等も含む) | トークン交換で失敗、または想定外のレスポンスになる |
| PKCE の整合 | /authorize と /token | code_challenge と code_verifier が対応 | コード交換に失敗、再試行地獄に陥る |
| アプリ登録の「ID トークン」許可 | Azure ポータルの認証設定 | ハイブリッド/インプリシット関連の許可が必要な構成か確認 | /authorize で id_token を要求してもブロックされる |
今回のケースでは、scope 自体は揃っていても、“どの応答で id_token を返してほしいのか” が明確に伝わっていない状態になっていたのが大きなポイントでした。
解決策:/authorize で id_token を明示的に要求する(ハイブリッドフロー)
サポートからの指摘に沿って、/authorize の response_type に id_token を含め、「コードに加えて ID トークンも返す」形(ハイブリッドフロー)に切り替えます。
/authorize リクエスト例(イメージ)
以下は “形” を掴むための例です。値は自環境のものに置き換えてください。
GET https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize
?client_id={client_id}
&response_type=code%20id_token
&redirect_uri={redirect_uri}
&response_mode=form_post
&scope=openid%20profile%20email
&state={random_state}
&nonce={random_nonce}
&code_challenge={S256_code_challenge}
&code_challenge_method=S256
ここでの実務ポイントは次のとおりです。
response_type=code id_tokenにより、認可レスポンスに id_token を含める意図を明示できます。nonceは id_token を受け取る場合の要。付与するだけでなく、アプリ側で検証する前提にします。response_mode=form_postにしておくと、トークンが長いケースでも取り回しが安定します(クエリ長制限の回避)。- SPA/ネイティブでも、可能なら PKCE(
code_challenge/code_verifier)を採用しておくと安全です。
何が変わるのか:レスポンスのイメージ
ハイブリッドにすると、リダイレクト(または form_post)で返るパラメータに id_token が含まれるようになります。
| パラメータ | 役割 | この問題との関係 |
|---|---|---|
| code | /token でアクセストークン等に交換するためのコード | 従来通り取得できていた |
| id_token | ユーザーのサインインを表す ID トークン | ここが null/欠落していたため、JWT デコードで例外が出ていた |
| state | CSRF 対策の相関値 | 必ず検証し、セッションに紐づける |
これにより、少なくとも「アプリがログイン判定に使う id_token」を確実に受け取れる構成へ寄せられます。
追加でハマるポイント:id_token 要求後に AADSTS50146 が出る
id_token を要求できるようにした途端、次の壁として AADSTS50146 が出ることがあります。今回のケースでは、原因がアプリ登録側のクレーム構成にありました。
具体的には、アプリ登録でクレームマッピング(カスタムクレーム)を使っている/使う設定になっている場合、マニフェストの acceptMappedClaims が false のままだと、ID トークンの発行がブロックされることがあります。
解決策:アプリ登録マニフェストで acceptMappedClaims を true にする
手順は次のとおりです(ポータルの文言は表示時期により多少変わります)。
- Azure ポータル → 「アプリ登録」 → 対象アプリを開く
- 左メニューの「マニフェスト」を開く
- JSON 内の
acceptMappedClaimsを探し、次のように変更する
"acceptMappedClaims": true
- 保存して反映させる
この変更後、認可コードおよび ID トークンが正常に取得できる状態になります(今回の事例)。
いつ acceptMappedClaims が効いてくるのか(整理)
「何でもかんでも true にする」のは避けたいので、使いどころをざっくり分けます。
| やりたいこと | 代表的な手段 | acceptMappedClaims が絡みやすい | 補足 |
|---|---|---|---|
| 標準クレームを少し追加/削除したい | トークン構成(Optional Claims) | 低い | UI で完結することが多い |
| 特殊な条件でクレーム値をマッピングしたい | クレームマッピング(カスタム) | 高い | マニフェスト/ポリシー設定が絡む |
| アプリ側の認可(ロール)をトークンに入れたい | App roles / グループクレーム等 | 中 | 発行クレーム量や設計に注意 |
今回のように “id_token を出したい” という目的でも、背後に「クレームをどう扱うか」の設定が入っていると、結果的にトークン発行が止まります。エラーコードが出たら、まずはクレーム周り(マニフェスト/トークン構成/ポリシー)を疑うのが近道です。
実務の確認手順:変更後にどこまでテストすべきか
設定を変えたら、次の3点をセットで確認すると「直ったつもり」を避けられます。
/authorize の戻りで code と id_token を確認
codeが取れているかid_tokenが null ではなく、JWT 形式(xxxxx.yyyyy.zzzzz)になっているかstateが送った値と一致しているか
/token のレスポンスを確認(必要に応じて)
/token 側でも id_token を利用する設計なら、レスポンス JSON のキーを目視で確認します。トークン交換自体の例(イメージ)は以下です。
POST https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id={client_id}
&client_secret={client_secret} (機密クライアントの場合)
&code={authorization_code}
&redirect_uri={redirect_uri}
&code_verifier={code_verifier} (PKCE の場合)
そしてアプリ側ログで、少なくとも次を確認します。
- id_token が “存在する” こと(null でない)
- 署名検証が通ること(鍵/JWKS の取得やキャッシュ含む)
- 検証すべきクレームが想定通りであること(iss/aud/exp/nonce など)
JWT デコード例外を「防ぐ」実装も入れておく
根本原因を解消しても、運用上は一時的な失敗(ネットワーク、再試行、途中キャンセル)で id_token が取れない状況がゼロにはなりません。ライブラリに null を渡して例外で落とすより、先にガードすると障害の形が綺麗になります。
| 観点 | おすすめ | 避けたい |
|---|---|---|
| 入力チェック | id_token が空なら “認証失敗” として扱い、ログに残す | null をそのまま JWT デコーダに渡す |
| ログ | 相関ID(state)やエラーコードをセットで残す | 例外スタックだけで原因追跡する |
| リトライ | ユーザー操作が必要な場合は再ログイン導線を用意 | 無限リトライでフローを壊す |
よくある勘違い:/token に scope=openid を付けたのに id_token が出ない
現場で多いのが、「/token のリクエストで scope=openid profile email を付けたから OIDC のはず」という思い込みです。実際は、認可コードが “どの要求で発行されたか” が重要で、/authorize 側の要求内容が反映されます。
今回の解決がハイブリッドフローだったのは、/authorize の段階で id_token を明示要求し、サーバー側に「この認証で ID トークンが必要」という意図を強く伝えるため、と捉えると理解しやすいです。
再発防止のチェックリスト
最後に、同種の “id_token がない” 問題を潰すための実務チェックリストをまとめます。
| 項目 | やること | 期待できる効果 |
|---|---|---|
| /authorize を最優先で点検 | response_type と scope(特に openid)を確認 | OIDC としての要求不足を最短で潰せる |
| id_token の受け取り場所を明確化 | /authorize で受け取るのか /token で受け取るのかを設計に落とす | 「ない場所を探す」デバッグを回避 |
| nonce/state を必ず運用 | 生成・保存・照合を実装に組み込む | セキュリティと原因追跡が両立する |
| クレーム周りの設定を棚卸し | カスタムクレーム/マッピングがあるなら acceptMappedClaims を確認 | AADSTS50146 のような “発行ブロック” を避けやすい |
| アプリ側のガード | null/空トークン時の分岐とログを用意 | 障害時に落ち方が綺麗になり復旧が早い |
まとめ
Microsoft Entra ID(Azure AD)の OIDC 認証で id_token が返らないときは、アプリ登録の設定だけでなく、/authorize リクエストが “ID トークンを必要としている” ことを正しく伝えているかが最重要ポイントになります。今回のケースでは、次の2点で解消しました。
- /authorize の
response_typeにid_tokenを含める(code id_token) acceptMappedClaimsをtrueにして、クレームマッピング起因の発行ブロックを解除する
同様の症状が出たら、まずは “フロー(どこで id_token を受け取る想定か)” と “クレーム設定(acceptMappedClaims を含む)” をセットで見直すと、最短距離で復旧できます。

コメント