Skip to content
Published on

OpenID Connect ディープダイブ — Authorization Code Flow から Discovery まで

Authors

はじめに

2026 年現在、新しく作られるほぼすべてのシステムのログインは OpenID Connect(OIDC)に収斂しました。ソーシャルログインも、社内 SSO も、モバイルアプリも、CLI ツールのログインでさえ、その底には OIDC Authorization Code Flow が流れています。それにもかかわらず、OIDC を「ライブラリの設定数行」としてしか知らないまま運用し、トークン検証の欠落や nonce の未使用といった穴から事故が起きる事例が後を絶ちません。

本記事は OIDC をプロトコルレベルで解剖します。HTTP リクエスト/レスポンスを直接読み、3 種類のトークンの役割を区別し、Discovery と JWKS の動作を理解し、最後に実務用のトークン検証チェックリストとしてまとめます。OAuth 2.1 ドラフトが事実上の基準となった時点ですので、すべての説明は Code + PKCE を前提とします。

OIDC のレイヤー構造 — OAuth 2.0 の上に積んだもの

OIDC は OAuth 2.0 を置き換えません。OAuth 2.0 のフローをそのまま使いながら、その上に認証レイヤーを追加します。

+---------------------------------------------------------------+
| OIDC が追加したもの                                             |
|  - ID Token(署名付き JWT、認証結果の標準フォーマット)          |
|  - scope=openid と標準クレーム(sub, email, name...)           |
|  - Discovery (/.well-known/openid-configuration)                |
|  - JWKS(署名鍵の配布)、UserInfo エンドポイント                |
|  - nonce, max_age, prompt, acr など認証専用パラメータ           |
|  - RP-Initiated / Back-Channel Logout                           |
+---------------------------------------------------------------+
| OAuth 2.0 (RFC 6749) が提供するもの                             |
|  - authorize / token エンドポイントと grant フロー              |
|  - access token, refresh token                                  |
|  - クライアント登録と redirect_uri の検証                       |
+---------------------------------------------------------------+
| 基盤: HTTP + TLS, JWT (RFC 7519), JWS/JWK                       |
+---------------------------------------------------------------+

用語も OAuth の上に重ねて変わります。OAuth の client は OIDC では RP(Relying Party)、authorization server は **OP(OpenID Provider)**になります。

3 つのトークン — 役割を絶対に混ぜないこと

項目ID TokenAccess TokenRefresh Token
受け手(audience)RP(クライアント)Resource Server(API)OP の token エンドポイント
目的「ユーザーが認証された」ことの証明API 呼び出しの権限新しいトークンの再発行
フォーマット必ず JWT自由(JWT または opaque)自由(通常 opaque)
検証の主体RP 自身が署名/クレームを検証API サーバーが検証OP のみが解釈
寿命短く(分単位)短く(5〜15 分推奨)長く(ローテーション必須)
送ってはいけない先API に送らないログインの証拠に使わないRP の外へ絶対に出さない

最も多い 2 つの誤用:

  1. ID Token を API に Bearer として送ること — ID Token の aud は client_id であって API ではありません。API がこれを受け入れるなら audience 検証を放棄したことになります。
  2. Access Token でログイン処理をすること — access token には「誰が認証したか」についての受け手に紐づく保証がありません。ログインの判断は ID Token だけで行います。

ID Token の中身を覗く

ID Token は 3 つの部分(header.payload.signature)からなる JWS です。

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "rsa-key-2026-05"
}
{
  "iss": "https://idp.corp.com/realms/prod",
  "sub": "f9a8b7c6-1234-5678-90ab-cdef12345678",
  "aud": "web-dashboard",
  "exp": 1781258400,
  "iat": 1781258100,
  "auth_time": 1781258095,
  "nonce": "n-0S6_WzA2Mj",
  "acr": "urn:mace:incommon:iap:silver",
  "azp": "web-dashboard",
  "email": "yj.kim@corp.com",
  "email_verified": true,
  "name": "Youngju Kim",
  "preferred_username": "yj.kim"
}
  • iss: 発行者。Discovery ドキュメントの issuer と文字列として正確に一致しなければなりません。
  • sub: ユーザーの不変の識別子。アカウントの突合は email ではなく iss+sub の組み合わせで行うべきです。メールアドレスは変わり、再利用され、未検証の可能性もあります。
  • aud / azp: このトークンの受け手(自分の client_id)。他アプリのトークンを拒否する根拠です。
  • exp / iat / auth_time: 失効、発行、実際の認証時刻。auth_time は max_age の要求とともに再認証ポリシーに使われます。
  • nonce: 認証リクエスト時に送った値のエコー。リプレイ防御の核心です。

Authorization Code Flow + PKCE — HTTP レベルの解剖

全体のシーケンスは次のとおりです。

[ブラウザ]             [RP サーバー]              [OP]
    |                       |                       |
    |-- 1. GET /login ----->|                       |
    |                       |  2. state, nonce,     |
    |                       |     code_verifier 生成 |
    |<- 3. 302 OP /authorize へリダイレクト --------|
    |-- 4. GET /authorize ------------------------->|
    |<- 5. ログイン UI(SSO セッションがあれば省略)|
    |-- 6. 認証(passkey/MFA) -------------------->|
    |<- 7. 302 redirect_uri?code=...&state=... -----|
    |-- 8. GET /callback?code=...&state=... ->|     |
    |                       |  9. state を照合      |
    |                       |-- 10. POST /token --->|
    |                       |    (code+verifier)    |
    |                       |<- 11. トークン応答 ---|
    |                       |  12. id_token 検証、  |
    |                       |      nonce 照合       |
    |<- 13. セッションクッキー --|                  |

ステップ 1: 認証リクエストの作成

RP はリクエスト前に 3 つのランダム値を作ります。

# state: CSRF 防御用(セッションに保存)
state=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43)

# nonce: ID Token のリプレイ防御用(セッションに保存)
nonce=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-43)

# PKCE: code_verifier とその SHA-256 ハッシュである code_challenge
code_verifier=$(openssl rand -base64 64 | tr -d '=+/' | cut -c1-128)
code_challenge=$(printf '%s' "$code_verifier" \
  | openssl dgst -sha256 -binary | openssl base64 -A \
  | tr '+/' '-_' | tr -d '=')

そしてブラウザを authorization エンドポイントへ送ります。

GET /realms/prod/protocol/openid-connect/auth
    ?response_type=code
    &client_id=web-dashboard
    &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
    &scope=openid%20profile%20email
    &state=af0ifjsldkj
    &nonce=n-0S6_WzA2Mj
    &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
    &code_challenge_method=S256 HTTP/1.1
Host: idp.corp.com

ステップ 2: コールバックと code の受信

認証が終わると OP はブラウザを送り返します。

HTTP/1.1 302 Found
Location: https://app.example.com/callback
          ?code=SplxlOBeZQQYbYS6WxSbIA
          &state=af0ifjsldkj
          &iss=https%3A%2F%2Fidp.corp.com%2Frealms%2Fprod

RP が直ちにやるべきこと: セッションの state と戻ってきた state が一致するかを比較します。不一致ならその場で中断します(CSRF の試み)。iss パラメータ(RFC 9207)があれば、期待した発行者かどうかも確認して mix-up 攻撃を遮断します。

ステップ 3: code をトークンに交換(back-channel)

POST /realms/prod/protocol/openid-connect/token HTTP/1.1
Host: idp.corp.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic d2ViLWRhc2hib2FyZDpzM2NyM3Q=

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

OP は code_verifier を SHA-256 でハッシュし、最初に受け取った code_challenge と比較します。コードが窃取されても verifier なしでは交換が不可能 — これが PKCE(RFC 7636)です。

レスポンス:

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 300,
  "refresh_token": "eyJhbGciOiJIUzUxMiIs...",
  "id_token": "eyJhbGciOiJSUzI1NiIs...",
  "scope": "openid profile email"
}

RP は id_token を検証し(下のチェックリスト)、payload の nonce がセッションに保存した値と一致することを確認した後に、はじめてログイン処理を行います。

confidential vs public クライアント

区分confidentialpublic
サーバーレンダリング Web アプリ、BFFSPA、モバイル、CLI
client_secret保有(または private_key_jwt)なし
token リクエストの認証Basic または mTLS/JWTclient_id のみ
PKCEOAuth 2.1 ではそれでも必須必須(唯一の防御線)

OAuth 2.1 基準では PKCE はクライアントの種類に関係なくすべてに適用します。SPA であればトークンをブラウザに直接持たせず、BFF(Backend For Frontend)パターンで confidential クライアント化するのが 2026 年の推奨構成です。

Discovery — 設定をコードに埋め込まない方法

OP は自身のすべての設定を標準パスに公開します。

curl -s https://idp.corp.com/realms/prod/.well-known/openid-configuration
{
  "issuer": "https://idp.corp.com/realms/prod",
  "authorization_endpoint": "https://idp.corp.com/realms/prod/protocol/openid-connect/auth",
  "token_endpoint": "https://idp.corp.com/realms/prod/protocol/openid-connect/token",
  "userinfo_endpoint": "https://idp.corp.com/realms/prod/protocol/openid-connect/userinfo",
  "jwks_uri": "https://idp.corp.com/realms/prod/protocol/openid-connect/certs",
  "end_session_endpoint": "https://idp.corp.com/realms/prod/protocol/openid-connect/logout",
  "scopes_supported": ["openid", "profile", "email", "offline_access"],
  "response_types_supported": ["code"],
  "id_token_signing_alg_values_supported": ["RS256", "ES256", "EdDSA"],
  "code_challenge_methods_supported": ["S256"]
}

RP ライブラリは issuer URL を 1 つ設定すれば、残りはここから読み込みます。重要な検証ルールが 1 つあります。**ドキュメント内の issuer 値が、ドキュメントを取得した URL の issuer 部分と一致しなければなりません。**この確認がなければ、悪意ある OP が他者の設定を装う攻撃が可能になります。

Keycloak 26.6 では EdDSA 署名もサポートされているため、新規構築であれば RS256 に加えて ES256/EdDSA の採用も検討に値します。

JWKS と鍵ローテーション

jwks_uri は ID Token の署名検証に使う公開鍵の一覧(JWK Set)を提供します。

{
  "keys": [
    {
      "kid": "rsa-key-2026-05",
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "n": "0vx7agoebGcQSuuPiLJXZpt...",
      "e": "AQAB"
    },
    {
      "kid": "rsa-key-2026-06",
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "n": "zNf1a9pXkQpW3mLbV2c...",
      "e": "AQAB"
    }
  ]
}

検証の流れ: トークン header の kid → JWKS から同じ kid の鍵を選択 → 署名検証。鍵が 2 つ見えるのは正常です。鍵ローテーション中は新しい鍵と古い鍵が共存するからです。

鍵ローテーションを無停止にする運用ルール:

  1. OP: 新しい鍵を先に JWKS へ公開し、十分な時間(キャッシュ TTL 以上)が経ってから新しい鍵で署名を開始し、古い鍵で署名されたトークンがすべて失効してから古い鍵を削除します。
  2. RP: JWKS をキャッシュしつつ(毎リクエスト照会は禁止)、未知の kid に遭遇したら一度だけ再取得するロジックを入れます。ただし再取得に rate limit をかけて DoS を防ぎます。
  3. kid のないトークン、JWKS にない kid、alg の不一致はすべて拒否します。

UserInfo、クレーム、スコープ

スコープ → クレームのマッピング

scope は「どのクレームの束が欲しいか」の単位です。

scope解放される標準クレーム
openidsub(OIDC リクエストであることの表明、必須)
profilename, family_name, given_name, preferred_username, picture など
emailemail, email_verified
addressaddress
phonephone_number, phone_number_verified
offline_accessrefresh token の発行要求

UserInfo エンドポイント

クレームは ID Token に載せて受け取るか、access token で UserInfo を呼び出して受け取れます。

curl -s https://idp.corp.com/realms/prod/protocol/openid-connect/userinfo \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
{
  "sub": "f9a8b7c6-1234-5678-90ab-cdef12345678",
  "name": "Youngju Kim",
  "email": "yj.kim@corp.com",
  "email_verified": true,
  "groups": ["sso-admins", "developers"]
}

注意: UserInfo 応答の sub が ID Token の sub と一致するかを必ず比較してください。トークン差し替え攻撃をふるい落とす最後のセーフティネットです。ID Token を軽く保ちたければクレームを UserInfo へ後回しにし、ゲートウェイで一度に使いたければ ID Token に含める、というトレードオフを設計します。

state と nonce — 2 つの異なる盾

混同しやすい 2 つのパラメータの役割を明確に区別します。

項目statenonce
防御対象CSRF(コールバックの偽造)ID Token のリプレイ/注入
誰が作るかRPRP
どこから戻るかコールバックのクエリパラメータID Token のクレーム
いつ検証するかコールバック受信の直後ID Token の検証時
ないと何が起きるか攻撃者が自分の code を被害者セッションに注入(セッション固定)窃取された ID Token の再利用

典型的な攻撃シナリオを 1 つ見てみましょう。state がなければ、攻撃者は自分のアカウントでログイン手続きを進め、コールバック URL(自分の code を含む)を抜き出して被害者にクリックさせます。被害者のブラウザがそのコールバックを踏むと、被害者のセッションが攻撃者のアカウントでログインされます。以降、被害者が入力するデータは攻撃者のアカウントに蓄積されます。state の照合 1 行がこのすべてを防ぎます。

トークン検証チェックリスト(実務用)

ID Token 受信時(RP):
[ ] 1. token エンドポイントの TLS 応答から受け取ったか
       (front-channel での受領は禁止)
[ ] 2. JWS header の alg が許可リスト(RS256/ES256/EdDSA)にあるか
       - alg=none を拒否、対称鍵(HS256)の混用を拒否
[ ] 3. kid で JWKS から鍵を見つけて署名検証
[ ] 4. iss == Discovery の issuer(文字列の完全一致)
[ ] 5. aud に自分の client_id が含まれる、複数 audience なら azp == 自分の client_id
[ ] 6. exp 未経過、iat が妥当な範囲(clock skew 60〜120 秒許容)
[ ] 7. nonce == セッションに保存した値(使用後は破棄)
[ ] 8. 再認証を要求した場合は auth_time/acr を確認
[ ] 9. アカウント突合は iss+sub の組み合わせで(email 突合は禁止)

Access Token 受信時(Resource Server):
[ ] 1. 署名検証(JWT の場合)または introspection(opaque の場合)
[ ] 2. iss, exp の検証
[ ] 3. aud または resource indicator が「この API」かを検証
[ ] 4. scope が要求された操作を許可しているか確認
[ ] 5. 必要に応じて発行時刻ベースの失効ポリシーを適用

このうち 4 番(iss)と 5 番(aud)を省略した実装が現実には驚くほど多いのです。「署名さえ有効なら通過」は、どの OP のどのトークンでも通過という意味です。

ログアウト — OIDC の難しい部分

OIDC は 3 つのログアウトメカニズムを標準化しました。

  • RP-Initiated Logout: RP が end_session_endpoint へ送り、OP セッションを終了。
  • Front-Channel Logout: OP が各 RP のログアウト URL を iframe で読み込み。サードパーティクッキーのブロックで信頼性が低下し、廃れつつあります。
  • Back-Channel Logout: OP が各 RP のバックエンドへ logout token(JWT)を直接 POST。2026 年時点の推奨案です。

Back-channel logout を受けるには、RP が「sid(セッション ID)→ アプリセッション」のマッピングを維持する必要があります。ステートレスな JWT だけでセッションを運用するとこの要求を満たせないという点が、「アプリセッションはサーバー側セッション + IdP トークンはバックエンドで保管」という BFF 構成を推すもう 1 つの理由です。

OAuth 2.1 時代の OIDC — 2026 年の整合性まとめ

OAuth 2.1 ドラフトの制約を OIDC 実装に当てはめると次のようになります。

  1. Implicit/Hybrid で id_token を front-channel で受け取っていたパターンの廃止 — response_type=code への一本化。form_post が必要な特殊ケースでない限り、fragment でトークンを受け取りません。
  2. PKCE の全面適用 — OIDC の nonce があっても PKCE は別途必要です。nonce は ID Token の注入を、PKCE は code の窃取を防ぎます。防御対象が異なります。
  3. refresh token のローテーション — public クライアントはローテーション + 再利用検知を有効にします。再利用が検知されたらトークンファミリー全体を失効させます。
  4. redirect_uri の完全一致 — ワイルドカード、部分一致はすべて禁止。
  5. Bearer トークンのクエリ文字列での受け渡し禁止 — ヘッダーまたは POST body のみ。

さらに高セキュリティのドメインであれば FAPI 2.0 Security Profile(Keycloak 26.6 で Final 対応)を、シークレット管理の強化が必要なら client_secret の代わりに private_key_jwt や mTLS クライアント認証を検討します。JWT Authorization Grant や federated client authentication といった Keycloak 26.6 の新機能は、サービス間の信頼連携シナリオでパスワードレスのクライアント認証を可能にします。

トラブルシューティングノート

症状よくある原因確認方法
invalid_grant(token 交換失敗)code の再利用、code の失効(数十秒)、redirect_uri 不一致、verifier 不一致OP のログで拒否理由を確認
署名検証の失敗が間欠的鍵ローテーション直後の JWKS キャッシュが古い未知 kid の再取得ロジックの有無を点検
ログインの無限ループコールバックでセッションクッキーが付かない(SameSite、ドメイン不一致)Set-Cookie 属性とコールバックのドメインを確認
間欠的な exp/iat エラーサーバー間のクロックスキューNTP の状態、skew 許容値の設定
state 不一致エラーの多発マルチタブでのログイン、セッションストアの TTL が短いstate をタブごとに区別して保存
モバイルだけログイン失敗アプリ内ブラウザ(WebView)のブロックポリシーシステムブラウザ(ASWebAuthenticationSession など)を使用

おわりに

OIDC を一文で要約すると「OAuth 2.0 の配管の上に、署名付き JWT(ID Token)とメタデータの自動発見(Discovery/JWKS)を載せて認証を標準化したプロトコル」です。実務の観点から核心を 3 つ残します。

  • **3 つのトークンの役割を混ぜないでください。**ログインは ID Token、API は Access Token、更新は Refresh Token。それぞれ audience と検証の主体が異なります。
  • **検証チェックリストをコードレビューの基準にしてください。**iss/aud/exp/nonce/署名のどれか 1 つでも欠けていれば、その実装は未完成です。
  • **OAuth 2.1 の制約(Code + PKCE への一本化、refresh のローテーション、exact redirect_uri)はすでに現在の常識です。**ドラフトの確定を待つ理由はありません。

SAML が昨日の信頼を支えているとすれば、OIDC は今日と明日 — passkeys 統合、AI エージェントの委譲、FAPI 2.0 — が積み上がる場所です。プロトコルの底を理解しておけば、その上のどんな変化も怖くありません。

参考資料