OpenID Connectとは?OAuth 2.0との違いと認可コード+PKCEのログイン設計
OIDCのログインで、何を検証すればユーザーを安全に識別できるのか。OAuth 2.0・JWTとの違い、3種類のトークン、state・nonce・PKCE、セッションとログアウトを、Webアプリのログインの流れで解説します。
OpenID Connectとは?OAuth 2.0・JWTとの違い
OpenID Connectは、外部の認証基盤で確認したユーザーの身元を、アプリが標準の手順で受け取るための仕様です。「別のサービスのアカウントでログインする」場面で使います。認証基盤をOpenID Provider(OP、IdPとも呼ぶ)、その結果を信頼するアプリをRelying Party(RP)と呼びます。
| 用語 | 担当すること | 取り違えやすい点 |
|---|---|---|
| OAuth 2.0 | APIへのアクセス権をクライアントに委譲する枠組み | アクセストークンを受け取っただけでは、アプリ向けのログイン結果にならない |
| OpenID Connect | OAuth 2.0にIDトークンなどを加え、ユーザーの認証結果を伝える | どの請求書を読めるかはアプリ側の認可で決める |
| JWT | クレーム(属性)を運ぶトークンの形式 | 形式自体はログイン手順でも認可ルールでもない |
OIDCの要求ではscopeに openid を含めます。IDトークンはJWTですが、アクセストークンはJWTとは限らず、不透明な文字列の場合もあります。形式が同じでも用途が同じとは限りません。署名付きJWTの中身は通常読めるので、署名を暗号化と考えて秘密情報を入れるのも誤りです。
IDトークン・アクセストークン・リフレッシュトークンの役割
架空の請求書Webアプリで、企業のIdPを使ってログインするとします。アプリはログイン結果を受け取り、必要なら別の請求書APIを呼びます。どのトークンを、誰が、何のために受け取るかを先に決めます。
| 種類 | 受け取り・検証する相手 | 用途 |
|---|---|---|
| IDトークン | ログインを開始したRP。audにはそのclient_idを含む | 認証結果を確かめ、アプリのセッションを作る |
| アクセストークン | 対象のリソースサーバー(API) | 許された範囲のAPIを呼ぶ。宛先・権限はAPI側でも検証する |
| リフレッシュトークン | 発行元の認可サーバーのトークンエンドポイント | 再びログイン画面を出さず、アクセストークンなどを更新する。発行は条件次第 |
一般的なOIDC連携では、IDトークンを業務APIのBearerトークンとして流用しません。RP宛ての認証結果をAPIが受け入れると、用途や宛先が違うトークンでアクセスできてしまいます。APIでは、そのAPI向けのアクセストークンを検証し、必要なscopeと対象データへの権限を確かめます。UserInfo用のアクセストークンが、請求書APIにも使えるとは限りません。
API用の宛先とscopeは認可サーバーの設定・要求で別途指定します。APIは合意したトークンの方式で検証し、不透明なトークンならintrospection等で有効性と権限を照会します。JWTなら発行者・宛先・署名・期限に加え、採用したプロファイルの種別も確認します。RFC 9068を使う場合はtypがat+jwt(またはapplication/at+jwt)であることも検証します。IDトークン用の検証器をそのまま共用しません。
認可コード+PKCEでログインする流れ
認可コードフローでは、ブラウザーを経由して受け取るのは短命の認可コードです。BFFがトークンエンドポイントでコードを交換します。PKCE(Proof Key for Code Exchange)は、最初の要求と交換を、その要求のために作った秘密のcode_verifierで結び付けます。RFC 9700ではパブリッククライアントにPKCEを必須とし、機密クライアントにも推奨しています。
- ブラウザー: ログインを開始する
- BFF(RP): 要求ごとのstate・nonce・verifierを開始元に結び付けて保存し、challenge付きでOPへリダイレクト
- IdP(OP): ユーザーを認証し、必要な同意を得て、登録済みのcallbackへcodeとstateを返す
- BFF(RP): 開始元・state・期限を確認し、保存したverifierとクライアント認証でcodeを交換
- IdP(OP): codeとverifierなどを検証し、IDトークンとアクセストークンを返す
- BFF(RP): IDトークンとnonceを検証し、iss+subを自分のユーザーに対応付ける
- ブラウザー: 新しいアプリのセッションCookieを受け取り、ログイン完了
response_type=code
client_id=invoice-web
redirect_uri=https://app.example.com/oidc/callback
scope=openid profile email
state=<transaction-specific random value>
nonce=<a different transaction-specific random value>
code_challenge=<BASE64URL(SHA256(ASCII(code_verifier)))>
code_challenge_method=S256実装ではOIDC対応ライブラリで乱数生成・URLエンコード・応答検証を行います。verifierは要求ごとに生成する43〜128文字の高エントロピーな文字列で、認証要求に送るのはS256で作ったchallengeだけです。redirect_uriはOPに完全一致で登録し、交換時にも同じ値を使います。profile・emailのscopeを指定しても、すべての属性が必ず返るという意味ではありません。
認可を拒否された場合や交換に失敗した場合は、セッションを作らず、その試行を終了します。期限切れ・使用済みの要求も再利用せず、新しくログインを始めます。新規の設計では、アクセストークンを認可レスポンスに直接載せるimplicitフローを選ばず、認可コード+PKCEを使います。
state・nonce・PKCEは何が違う?送る場所と照合先で考える
| 値 | 送る場所と照合先 | この記事の設計での役割 |
|---|---|---|
| state | 認証要求に送り、callbackのstateを保存値と照合 | 要求と応答を結び付け、開始元に結び付けた管理でログインCSRFを防ぐ |
| nonce | 認証要求に送り、検証済みIDトークンのnonceを保存値と照合 | 認証結果をその試行に結び付け、別試行のトークンの再利用を拒む |
| code_verifier | challengeだけを認証要求へ送り、verifier本体はコード交換で送る | 対応するverifierを持たない相手による、横取りしたコードの交換を拒む |
BFFではstate・nonce・verifier・期待するissuer・redirect_uri・期限を、開始元ブラウザーに結び付けた短命のサーバー側レコードに保存します。stateの一致だけでなく、そのブラウザーが開始した、未使用の試行かも確認します。複数タブを許すなら、単一の変数に上書きせず試行ごとに分け、処理開始時に一度だけ消費できるようにします。
IDトークンの検証:署名・iss・aud・期限・nonceを確認する
JWTをBase64URLデコードすると検証前でも属性を読めますが、その値はまだ信頼できません。この構成では署名付きIDトークンを受け入れ、OIDCライブラリに期待するissuer・client_id・nonceと許可するアルゴリズムを渡して検証します。単なるJWTデコーダーをOIDCの検証器として使わないようにします。
| 確認項目 | この構成で確かめること |
|---|---|
| 必須クレームと形式 | iss・sub・aud・exp・iatなど、OIDCで必要な項目と型がそろっている |
| 署名・alg | 許可したアルゴリズムと信頼した発行者の鍵で検証。この構成では署名なしのnoneを拒否 |
| iss | 設定したissuerと完全一致する。複数組織がissuerを共有する場合、許可する組織の条件も別途確認 |
| aud・azp | audが自分のclient_idを含み、追加の宛先も信頼できる。azpは利用する拡張・プロバイダーの条件に従って検証 |
| 時間 | expを過ぎていない。iatの許容範囲や存在するnbfも確認し、時計のずれの許容は小さく定める |
| nonce | その試行で送ったnonceと一致する。欠落・不一致・使用済みの試行なら拒否 |
| 再認証の条件 | max_ageを要求したならauth_timeも確認。MFAが必要な操作ではacr等の意味をOPと合意し、条件を満たさなければ操作を拒否 |
公開鍵は、信頼するissuerのDiscovery文書から得た jwks_uri で取得します。Discoveryのissuer・設定したissuer・IDトークンのissの対応を確認します。トークンに入った任意のURLを追って鍵を取得すると、攻撃者の鍵を信頼したりSSRFを起こしたりします。kidは信頼する鍵集合の中で鍵を選ぶ手掛かりです。
鍵のローテーションに備えてJWKSをキャッシュし、未知のkidなら回数・時間を制限して再取得します。取得不能で既知の有効な鍵もない場合や、再取得後も鍵が見つからない場合は検証を失敗させます。ネットワーク障害時に署名検証を省略する復旧経路を作りません。
ユーザーの識別はiss+sub。emailだけでアカウントを結び付けない
検証したIDトークンの iss と sub の組を、アプリ内部のユーザーIDに対応付けます。emailや表示名には変更や再割り当てがあるため、同一人物を継続して識別する主キーには向きません。メールが同じという理由だけで既存アカウントを自動統合せず、本人確認を伴う明示的な連携手順を設けます。
同じ人でも、プライバシーのためRPごとに異なるsubを返すpairwise識別子があります。複数アプリのユーザーをsubだけで横断結合できると仮定しません。追加属性のためにUserInfoを呼ぶなら、UserInfo向けのアクセストークンを使い、返ったsubがIDトークンのsubと完全一致することも確認します。所属テナントや権限に属性を利用する場合は、その属性を誰が設定できるかと、OPとの契約も確認します。
ログイン後:アプリのセッション・更新・ログアウトを分ける
IDトークンを検証したら、新しいランダムなセッションIDを発行して認証前のIDを入れ替えます。サーバー側にユーザーとの対応と期限を保存し、CookieにはSecure・HttpOnly・適切なSameSiteを設定します。ここで想定する別サイトのOPからのトップレベルGETのcallbackでは、開始元を確認するCookieが届くようSameSite=Laxなどの条件を検証します。POST形式のcallbackはCookieの条件も変わります。
BFFはブラウザーへのトークン露出を減らせますが、Cookieで認証する更新APIにはCSRF対策が必要です。HttpOnlyはJavaScriptによるCookieの読み取りを防ぐだけで、XSS経由の操作を防ぎきるものではありません。セッションの最大期間と無操作期限を決め、各リクエストで期限・アカウント状態・対象データへの権限を確かめます。IDトークンのexpと、アプリのセッションの終了時刻は同じ仕組みではありません。
更新が必要でOPの条件を満たす場合だけ、リフレッシュトークンを取得します。offline_accessを要求するだけで必ず発行されるわけではありません。RFC 9700ではパブリッククライアントのリフレッシュトークンに、送信者への束縛かローテーションを求めます。ここではBFFでもOPが対応するローテーションを採用し、トークンをサーバーに安全に保存します。
ローテーションでは更新のたびに古いリフレッシュトークンを無効にし、再利用を検知したら、そのログインから続くリフレッシュトークンの連鎖(トークンファミリー)を無効にします。複数リクエストの同時更新を1つにまとめないと、正規の処理同士でも再利用検知を起こします。応答が途切れて新しいトークンを保存できなかった場合も、古いものを無条件で再送せず、OPの復旧方針に従い、必要なら再ログインします。
| 止めるもの | 必要な処理 | それだけでは止まらないもの |
|---|---|---|
| アプリのログイン状態 | サーバー側セッションを無効化し、Cookieを削除 | OP側のログイン状態、別に持たれたトークン |
| OP側のログイン状態 | 対応するRP-Initiated Logoutを利用。戻り先は登録済みURLに制限 | 他のRPのセッションや、全発行済みトークンの即時停止は別途確認が必要 |
| 今後のトークン更新 | リフレッシュトークンを取り消す | 自己完結した検証だけをするAPIでの、発行済みアクセストークンの受け入れ |
| APIでの利用 | 短い有効期間と、要件に応じた失効照会・ユーザー状態の確認 | 照会結果のキャッシュ中の古い許可。停止の伝播時間も含めて設計する |
ログアウトでは、まずアプリのセッションを無効にし、必要な取り消しを要求します。トークン取り消しの通信が失敗したときもローカルのログアウトは完了させ、取り消しは未完了として記録し、サーバーから再試行します。OPのログアウトはブラウザーのリダイレクトで要求するため、完了しなければ未確認として表示し、必要に応じてユーザーに再操作を案内します。JWTのローカル検証だけで即時失効が伝わるとは考えず、必要ならintrospection(認可サーバーへの有効性照会)やサーバー側の状態確認を組み込みます。許容できる停止までの時間から、期限とキャッシュを決めます。
実装の確認:ログイン成功より、拒否すべき応答を試す
以下は、このBFF設計を採用する場合の受け入れ条件です。対応するOPとOIDCライブラリを使った結合試験で確認します。この記事の例が製品として実行確認済みという意味ではありません。ログには処理段階と失敗理由を残し、コード・トークン・verifierや不要な個人情報は出しません。
| 試す条件 | 期待する結果 |
|---|---|
| stateが欠落・不一致、または別ブラウザーで開始したcallback | コード交換前に拒否し、セッションを作らない |
| nonceが欠落・不一致、またはcallbackを再送 | IDトークンまたは試行の検証で拒否し、新しいセッションを作らない |
| 間違ったverifierでコード交換 | OPが交換を拒否する。PKCEなしへのフォールバックをしない |
| 同じOPが別client_id向けに発行したIDトークン | 署名が正しくてもaudの不一致で拒否 |
| 別issuer・期限切れ・許可しないalg | セッションを作らない |
| 鍵をローテーション/未知のkidでJWKS取得にも失敗 | 信頼する新しい鍵なら受け入れる。検証可能な鍵がなければ拒否 |
| 複数タブで同時ログイン/並行するトークン更新 | ログイン試行が混ざらず、同じ更新トークンで並行更新しない |
| ログアウト後に同じCookieを再送/OPのログアウトが失敗 | アプリは拒否する。トークン取り消しは再試行し、OPログアウト未完了はユーザーに案内する |
| ログアウト前に得たアクセストークンをAPIへ再送 | 設計した停止時間内に拒否。ローカル検証だけで即時停止するという期待を置かない |
よくある疑問:OIDCならSSOやMFAも自動で付く?
- OIDCを入れればSSOになる? 共通のOPのログイン状態を利用できれば、各RPでパスワードを再入力せずに済む場合があります。ただしRPごとのセッションとログアウト連携は別途設計します。
- ログインできたらMFA済み? 認証方式はOPのポリシー次第です。重要操作で必要な認証強度と再認証の新しさを定め、acr・auth_time等の意味と検証方法をOPと合意します。
- JWTを使っていればOIDC? JWTは形式です。openidの要求、OPとのフロー、IDトークンの検証も含めて、OIDCのログインとして判断します。
仕様と一次資料
2026年10月26日に確認した資料です。OIDC Core/Discoveryはerrata set 2を参照し、認可コードの保護と更新トークンの要件はRFC 9700を併読しています。表の受け入れ条件は、それらをこの記事のBFF構成に適用した設計例です。
- OpenID Connect Core 1.0, errata set 2 — ID Token, validation, UserInfo, subject identifiers
- OpenID Connect Discovery 1.0, errata set 2 — provider metadata and issuer validation
- RFC 7636 — PKCE verifier, S256 challenge, and code exchange
- RFC 9700 — OAuth 2.0 Security Best Current Practice
- RFC 8725 — JWT algorithm verification and preventing token substitution
- RFC 9068 — JWT access token profile and resource server validation
- RFC 7662 — OAuth 2.0 Token Introspection
- RFC 7009 — Token revocation and self-contained access tokens
- OpenID Connect RP-Initiated Logout 1.0 — provider logout and registered return URLs
- OWASP — Session Management Cheat Sheet