OAuth2 と OpenID Connect

このページの目次

OAuth 2.0 は、アプリケーションがリソースへのアクセスを認可される方法を定義します。OpenID Connect(OIDC)は、その上にユーザー認証結果の表現と検証を定義します。アプリケーションは API 認可のみを要求することも、OIDC ログインを行ってから独自のセッションを確立することもできます。JWT はトークンの表現形式の一つであり、OAuth の同義語でもなく、「一切の状態管理を不要にする」という約束でもありません。

ロール例主要な責任
Resource Ownerドキュメントを所有するユーザーアクセスを認可する;すべてのフローにユーザーが関与するわけではない
Clientドキュメントへのアクセス権限を要求するアプリケーション認可の開始、トランザクションの保存、トークンの保持と使用
Authorization Server組織のアイデンティティと認可サービス認証、認可判断、トークンの発行と管理
Resource Serverドキュメント APIトークンの検証、リソースレベルの権限の適用

認可サーバーとリソースサーバーは、同じ製品に属することもあれば、独立してデプロイされることもあります。ブラウザは主にユーザーインタラクションとリダイレクトを担当します。サーバーサイドWebモードでは、認可コードを交換し、トークンを保存するクライアントは通常、アプリケーションのバックエンドです。RFC 6749

1回のログインで Authorization Code + PKCE を理解する

認可コードログイン:フロントチャネルで code を受け取り、クライアントが交換してローカルセッションを構築 ブラウザ アプリケーションクライアント 認可サーバー ① トランザクションを保存し、リダイレクト ② authorize:challenge、state、nonce ③ ログイン後のコールバック:code、state ④ ローカルトランザクションに関連付け ⑤ token:code、verifier、クライアント認証 ⑥ ID Token を検証し、アプリケーションセッションを作成 図はサーバーサイドWebクライアントを使用しています。SPAやネイティブアプリでは、トークンの保存とクライアント認証の機能が異なります。

アプリケーションはログイン開始前に短期間のトランザクションを作成し、期待される issuer、コールバックアドレス、ランダムな state、nonce、および PKCE verifier を記録し、ログインを起動したブラウザセッションにバインドします。異なるタブで並列にログインできるため、「最後の state」を単一グローバル変数で保存することはできません。

ステップ重要な内容完了条件
認可の開始response_type=code、client_id、redirect_uri、scopeパラメータが登録情報とローカルトランザクションと一致していること
PKCE バインディングcode_challenge、code_challenge_method=S256verifier はクライアント側に残し、challenge は認可エンドポイントに送信
ユーザーインタラクションログインおよび必要な同意既存のセッションがある場合は再度資格情報を入力する必要がない場合があり、毎回ポップアップが表示されるわけではない
コールバックの処理code または error、および返された state起動したトランザクションを見つけて照合し、期限切れまたは無関係な応答を拒否
交換code、verifier、必要な redirect_uri およびクライアント認証認可サーバーがバインディング関係を検証し、認可コードは再利用不可
アプリケーションセッションの構築OIDC ID Token の検証、ローカルアカウントへのマッピングその後、アプリケーション独自のセッションと権限コンテキストを作成

クライアントは、エラーコールバックも元のトランザクションに関連付ける必要があります。error=access_denied が返されたからといって、関連付けチェックをスキープしてはいけません。本番環境の実装では、単純に同じ認可コードを再生成するのではなく、コールバックの重複、コードの交換応答の喪失、トランザクションの期限切れを処理する必要があります。

OAuth のセキュリティベストプラクティスでは、認可サーバーが PKCE をサポートし、パブリッククライアントは PKCE を使用することが求められます。シークレットクライアントも使用すべきです。インプリシットフローはすべてのシステムから消えたわけではありませんが、認可応答で直接 access token を返す方法は、新しい実装の標準的な選択肢ではありません。パスワードグラントは明確に使用すべきではありません。マシンアイデンティティ、デバイスインタラクションなどには独自の認可方法があり、code flow を「すべてのシナリオにおける唯一のフロー」と呼ぶことはできません。RFC 9700 §2

PKCE の値はどのように計算されるか

S256 の計算は BASE64URL(SHA256(ASCII(code_verifier))) であり、Base64 の末尾の = を削除します。verifier の長さは 43〜128 文字の許可された文字で構成され、十分なエントロピーを持つ乱数によって生成される必要があります。challenge は SHA-256 の 16 進文字列ではありません。

import base64
import hashlib
import secrets

def challenge(verifier):
    return base64.urlsafe_b64encode(
        hashlib.sha256(verifier.encode('ascii')).digest()
    ).rstrip(b'=').decode('ascii')

# RFC 7636 付録 B の公開ベクトル。ランタイムで使用する秘密ではない。
verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
expected = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'
assert challenge(verifier) == expected
assert challenge(verifier + 'x') != expected
fresh = secrets.token_urlsafe(32)
assert len(fresh) == 43 and len(challenge(fresh)) == 43
print('PKCE 公開ベクトルおよび変更拒否が通過')

code を傍受した側が対応する verifier を持っていなければ、code のみでこの交換を完了することはできません。PKCE はクライアントの長期アイデンティティ認証ではありません。公開配布される SPA やネイティブアプリは、内蔵された client_secret で秘密を保持することはできません。秘密を保護できるサーバーサイドクライアントは、登録された方法で自身を認証します。RFC 7636

state、nonce、PKCE はそれぞれ何をバインドするか

値関連する場所照合すべき関係
state認可リクエストとブラウザのコールバックこのコールバックがこのブラウザで起動したトランザクションに属しているか
nonceOIDC リクエストと ID Tokenトークン内の認証結果が期待されるトランザクションにバインドされているか
PKCE認可リクエストとトークン交換交換者が、当時生成された verifier を保持しているか

これら3つの防護機能には重複がありますが、「そのいずれかが欠けていると、すべてのプロトコルモードで必ず違反する」と単純に言うことはできません。本文の例では、関連する関係を個別に検証しやすくするために、これら3つを明示的に使用しています。特に複数の issuer シナリオでは、A の応答を B のトークンエンドポイントに渡さないようにする必要があります。期待される issuer は、信頼できるトランザクションと設定から決定すべきであり、コールバックから与えられた任意のアドレスだけを信じてはいけません。

なぜ challenge は SHA256 の 16 進数ではないのか

これらの2つの出力は同一の 32 バイトのダイジェストから生成されますが、Base64url 出力のみがプロトコルで要求される challenge です。許可された文字の1つを変更すると、バインディング結果が変化します。verifier を 43 文字未満に短縮すると、構文要件に違反し、任意の文字列のハッシュを合法な PKCE として扱うことはできません。

図を準備しています
なぜ challenge は SHA256 の 16 進数ではないのか?

ダイジェストは 32 バイトであり、hex と Base64url は異なるエンコーディングです。S256 ルールに従ってバインディング challenge を取得して初めて、この交換チェックを満たします。

ID Token、Access Token、Refresh Token は交換不可

3種類のトークンの受信者と用途 ID Token → クライアント 1回の認証結果を証明;aud には通常 client_id が含まれる Access Token → リソースサーバー API へのアクセス権限を付与;JWT である場合もあれば、不透明文字列である場合もある Refresh Token → 認可サーバー 後続のトークンを取得;ビジネス API には送信されず、必ずしも発行されるとは限らない 形状が似ているからといって交換可能ではありません。署名が有効であっても、受信者、用途、時間、認可の制約を満たす必要があります。

ID Token は OIDC クライアント向けであり、ユーザーの認証結果を主張します。その有効期限は、アプリケーションセッションの強制有効期限とは等しくなく、毎回の API の権限判断を代替するものではありません。Access Token はリソースサーバー向けであり、⁠ユーザー情報を含めることができるほか、インタラクションのないクライアントを表すこともあります。ユーザークレームを含むかどうかは、トークンの形式と認可シナリオに依存し、名前だけで推測することはできません。

OIDC の scope=openid は、OIDC 認証を要求することを示します。profile、email などはクレームの要求であり、すべてのフィールドが表示されることを保証するものではありません。email は変更される可能性があり、常にグローバルに一意であるとは限りません。アプリケーションの外部アイデンティティキーは、信頼できる iss と sub の組み合わせを基盤として、ローカルアカウントのマッピングを構築すべきです。UserInfo を呼び出す際は、その sub がすでに検証された ID Token と一致していることを確認する必要があります。OIDC Core

offline_access を要求しても、サーバーが無条件で refresh token を返すわけではありません。発行はフロー、ユーザーの同意、プロバイダーのポリシーに依存します。1回のトークン応答に refresh token が含まれていないからといって、必ずしもプロトコルのエラーではありません。

JWT 検証:まず誰を信頼するかを決定し、次にキーを選択する

一般的な署名付き JWT は、JWS compact 形式(Base64url(header)、Base64url(payload)、signature)の3部構成を使用します。エンコーディングは暗号化ではなく、このようなトークンを取得した人は通常、クレームを読み取ることができます。暗号化された JWT は異なる構造を使用しており、「3つのドットセグメントを数える」ことですべての JWT を概括することはできません。

kid は、信頼できるキーセットから候補キーを検索するためのヒントであり、証明書でもなければ、任意のキー取得 URL でもありません。クライアントは、信頼できる issuer のディスカバリメタデータから jwks_uri を取得し、メタデータ内の issuer を照合する必要があります。JWKS のパスは /.well-known/jwks.json に固定されていません。検証されていないトークンに基づいて、任意の issuer や jku にアクセスしないでください。OIDC Discovery

検証レイヤー判断内容
暗号とフォーマット許可されたアルゴリズム、署名、キーの使用目的、解析および重複フィールドのポリシー
信頼の源泉issuer は期待される設定と厳密に一致する必要があります
受信者ID Token の aud には本クライアントが含まれる;Access Token は現在の API 向け
時間exp、適用される nbf、許容される時計のずれ、および認証の新鮮さ
トランザクションバインディングnonce が要求された場合はその値を照合;azp などの関連ルールを処理
トークンの用途ID Token と Access Token で異なる検証ルールを使用
ビジネス認可scope、主体、テナント、リソースの帰属およびアクション

キーローテーション時には、未知の kid に対してキャッシュを限定的に更新できますが、各見知らぬ kid に対して無限の外部リクエストをトリガーしないようにする必要があります。古い公開鍵の保持期間は、まだ受け入れられるべき古いトークンをカバーする必要があります。検証キーの削除とトークンの自然な期限切れは、同じことではありません。JWT BCP

RFC 9068 に準拠した JWT Access Token は at+jwt タイプを使用し、関連するクレームと検証要件を定義しています。すべての履歴またはプライベートな access token が自動的にこのプロファイルに従うことを要求することはできず、「署名が正しい任意の JWT」を API 資格情報として受け入れることもできません。RFC 9068

以下は、⁠信頼できるトークンが署名検証済みであることを前提としたビジネス認可のみを示しており、JSON ディクショナリを完全なトークン検証器として意図的に使用していません。scope が読み取りを許可していても、ドキュメントが現在のテナントに属しているかどうかを確認する必要があります。

def may_read(claims, document, now):
    return (
        claims['iss'] == 'https://identity.example'
        and claims['aud'] == 'documents-api'
        and now < claims['exp']
        and 'documents:read' in claims['scope'].split()
        and claims['tenant_id'] == document['tenant_id']
    )

c = dict(iss='https://identity.example', aud='documents-api',
         exp=2000, scope='documents:read', tenant_id='tenant-a')
assert may_read(c, {'tenant_id': 'tenant-a'}, 1999)
assert not may_read(c, {'tenant_id': 'tenant-b'}, 1999)
assert not may_read(c, {'tenant_id': 'tenant-a'}, 2000)
assert not may_read(dict(c, aud='web-client'), {'tenant_id': 'tenant-a'}, 1999)
print('テナント間、期限切れ、および誤った受信者はすべて拒否')

リフレッシュ、取り消し、ログアウトは別の3つの操作

Access Token が期限切れになると、クライアントは認可サーバーに対して refresh token を使用して新しいトークンを要求できます。ローテーションを採用している場合、各リフレッシュで新しい refresh token が返され、古い値は廃棄されます。クライアントは新しい値をアトミックに保存し、並列リフレッシュを調整する必要があります。2つのリクエストが同時に古いトークンを使用すると、そのうちの1つが再利用と見なされ、結果としてトークンファミリー全体が取り消される可能性があります。

再利用の検出には、「サーバーサイドでローテーションが完了したが、応答がネットワークで消失した」という曖昧さもあります。古い値を無限に再試行することはできません。プロバイダーの回復ウィンドウ、再利用ポリシー、およびクライアントの再認証パスは一致している必要があります。パブリッククライアントの refresh token は、送信者制約またはローテーションなどの規定されたリプレイ保護を採用すべきであり、単にトークンをクライアントストレージに配置するだけでは、このメカニズムではありません。RFC 9700 §4.14

操作直接変更される状態自動的に保証されないこと
アプリケーションセッションの削除本アプリケーションのログイン状態すべての API トークンが無効化されること
refresh token の取り消し後続のリフレッシュ能力およびプロバイダー関連の認可状態発行済みの JWT が各 API で即座に拒否されること
認可サーバーのログアウトアイデンティティプロバイダーのセッション各アプリケーションのローカルセッションが同期して消失すること
access token の取り消し認可サーバー内の有効性オフライン署名検証のみの API がリアルタイムで通知されること

API が JWT の署名と期限切れ時間のみをチェックする場合、取り消し後でも期限切れまで受け入れる可能性があります。より迅速な収束が必要な場合は、イントロスペクション、取り消し状態の伝播、またはより短い有効期限などの明示的なソリューションを採用できます。イントロスペクションが返す active もそのコンテキストに依存しており、キャッシュには可視性の遅延が発生します。Token Introspection、Token Revocation

Bearer token の保持者はそれを使用して操作できます。DPoP はトークンを証明キーにバインドし、リクエストに対して証明を生成することで、この使用条件を変更します。しかし、これは scope やリソース認可を代替するものではなく、完全に制御されたクライアントが自動的に信頼回復できるわけではありません。DPoP

アプリケーションの形態がトークンの保存場所を決定する

形態アイデンティティと保存方法処理が必要な問題
サーバーサイド Web / BFFバックエンドで交換・保存し、ブラウザはアプリケーションセッションクッキーを保持クッキー属性、CSRF、バックエンドセッションのライフサイクル
ブラウザ SPAパブリッククライアント、秘密を保守的に内蔵できないXSS、リフレッシュの調整、ページリロード後の回復
ネイティブアプリパブリッククライアント、システム認可インタラクションと PKCE を使用リダイレクトの受信、システムストレージ、プロセスのライフサイクル
バックグラウンドサービス適切な場合に client credentials を使用してサービスアイデンティティを表現資格情報の保護、scope、ローテーション、およびワークロードの帰属

client credentials にはエンドユーザーのログイン手順がなく、ある従業員のアイデンティティを使ってその sub を解釈すべきではありません。基本仕様では、このフローで refresh token を発行すべきではありません。サービスは通常、自身の資格情報を使用して access token を再取得します。アプリケーション内蔵シークレット、OAuth 認可、およびユーザー認証の3つの問題は、それぞれ個別に設計する必要があります。RFC 6749 §4.4

統合時のトラブルシューティングでは、まずステージを特定します。認可リクエストが拒否された場合は登録とパラメータを確認;invalid_grant の場合は code が期限切れ、既に使用済み、またはバインディングが不一致かどうかを確認;署名検証に失敗した場合は信頼できる issuer とキーを確認;ログインは成功したが API が拒否された場合はトークンの種類、audience、scope、およびリソース権限を確認。「JWT をデコードできる」ことをログイン統合完了の基準として使用しないでください。