アーキテクチャ概観
op.New(...) は http.ServeMux を内部に持つ http.Handler を返します。本ページでは、リクエスト到着からレスポンスまでの間に OP が何を実行するか、関わるパッケージ、検証の順序、組み込み側が制御するストレージの差し込み口を整理します。
以下のシーケンスで RP は別アプリです。OP 側のアプリケーションが go-oidc-provider を組み込み、Store と LoginFlow の実装を所有します。
パッケージ構成
op/ ← 公開 API(組み込み側はここを import)
op/profile/ ← OAuth 2.1 / OIDC Core / FAPI 2.0 / FAPI-CIBA プロファイル
op/feature/ ← PKCE / PAR / JAR / JARM / DPoP / mTLS / introspect / revoke
op/grant/ ← authorization_code、refresh_token、client_credentials、device_code、CIBA、JWT bearer
op/audit/ ← 監査イベントの定数とイベントカタログ
op/authn/、op/ciba/ ← 組み込み側が実装する契約型(Authenticator、HintResolver など)。op が再エクスポート
op/subject/ ← public / pairwise の subject generator
op/rs/ ← リソースサーバ側: access token の検証器とミドルウェア
op/store/ ← Store interface(サブストアの集合)+ contract test suite(op/store/contract)
op/storeadapter/{inmem,sql,redis,dynamodb,composite}
← ストアアダプタ。patterns はアダプタ実装者向けのヘルパー
op/interaction/ ← ログイン UI 用 HTML / JSON ドライバの差し込み口
op/devicecodekit, op/passkeykit, op/totpkit, op/recoverykit
← 組み込み側が持つ部分のヘルパー(device の確認ページ、factor の登録)
op/testkit/ ← テスト用にそのまま使える provider
cmd/op-client-secret ← 静的クライアント用に client secret と保存用ハッシュを生成
op/storeadapter/sql/cmd/op-schema
← SQL アダプタの migration ファイルを golang-migrate、goose、Flyway 向けに書き出す
internal/ ← 外部からは import 不可(Go の可視性)
authn/ ← LoginFlow オーケストレータ、Authenticator runtime
authorizeendpoint、parendpoint、tokenendpoint、userinfo、
introspectendpoint、revokeendpoint、registrationendpoint、
devicecodeendpoint、cibaendpoint、grantmgmtendpoint、protectedresource、
endsession、backchannel、ssf
jose、jwks、keys ← 署名 / 検証 / 鍵セット
jar、jarm、dpop、mtls、pkce、sessions、cimd、assertiongrant
cookie、csrf、cors、httpx、redact、log、metrics
discovery、scoperegistry、timex、i18n境界は構造的に強制されています。外部コードは internal/ に届きません。組み込み側が制御する差し込み口(オプション、store interface、authenticator、audit subscriber)はすべて op/ 配下にあります。
ルート一覧
op.New は *http.ServeMux を構築し、設定されたパスごとにハンドラを 1 つ登録します。下表のパスは既定値です。プロトコルのエンドポイントはすべて、issuer のパスに mount prefix(WithMountPrefix で変えない限り /oidc)を足した位置に置かれます。たとえば issuer が https://idp.example.com なら、/token は https://idp.example.com/oidc/token です。個別のパスは WithEndpoints で変更できます。discovery と protected-resource metadata だけは例外で、プレフィックスの外の well-known な位置に置かれます。
| パス | ハンドラのパッケージ | 何を返すか | 公開条件 |
|---|---|---|---|
/.well-known/openid-configuration | internal/discovery | OP の実効設定を discovery 文書として返す | 常時 |
/jwks | internal/jwks | 署名鍵セットの公開側 | 常時 |
/.well-known/oauth-protected-resource(+ パスのサフィックス) | internal/protectedresource | 宣言した保護リソースごとの RFC 9728 メタデータ | WithProtectedResources |
/auth | internal/authorizeendpoint | リクエストの解析、クライアントと redirect_uri の検証、interaction へのリダイレクト | authorization_code grant が有効な場合 |
/interaction/{uid} | internal/authorizeendpoint + op/interaction ドライバ | ログインと同意の手続き。HTML で描画するか、SPA へ JSON で返す | /auth と同時 |
/par | internal/parendpoint | pushed authorization request を受け取り、一度限りの request_uri を返す | feature.PAR |
/token | internal/tokenendpoint | クライアント認証、認可コードとリフレッシュトークンの引き換え、トークン発行 | 常時 |
/userinfo | internal/userinfo | 提示されたアクセストークンが指す subject の claim | 常時 |
/revoke | internal/revokeendpoint | RFC 7009 によるリフレッシュトークン / アクセストークンの失効 | feature.Revoke |
/introspect | internal/introspectendpoint | RFC 7662 によるトークン状態を、認可済みのリソースサーバへ返す | feature.Introspect |
/device_authorization | internal/devicecodeendpoint | RFC 8628 のデバイス認可。device_code と user_code を返す | device-code grant |
/bc-authorize | internal/cibaendpoint | CIBA の backchannel authentication リクエスト。auth_req_id を返す | CIBA grant |
/grant_management/{grant_id} | internal/grantmgmtendpoint | grant 1 件の照会と失効 | WithGrantManagement |
/end_session | internal/endsession | RP 起点のログアウト。バックチャネルの一斉通知もここから | /auth と同時(authorization_code grant) |
/register | internal/registrationendpoint | 動的クライアント登録と、クライアントごとの管理ルート | WithDynamicRegistration |
/ssf/streams/ssf/status/ssf/verify/ssf/poll/{stream_id} | internal/ssf | Shared Signals transmitter。stream の設定、状態、検証、ポーリング | WithSharedSignals |
/jwks:既定は ECDSA P-256、RS256なら RSA、切り替え中は両方。設定時はuse=enc鍵も含みます。/interaction/{uid}:op.SPAUIを使うとルートはLoginMountに移ります。/ssf/*:メタデータは prefix の外の/.well-known/ssf-configurationが配信します。
バックチャネルログアウトに行がないのは、公開されるエンドポイントではないからです。/end_session から発火する送信専用の一斉通知であり、OP が各 RP の登録済み backchannel_logout_uri へ logout token を POST します。discovery 文書は実際に公開されたエンドポイントだけを広告するので、feature.Introspect を有効にしていない構成では /introspect を一切告知しません。Provider.Routes() は、現在の構成で provider が登録した http.ServeMux パターンを返します。パス単位で振り分けるルータへ mount するときに使います。op.NewReloadable は、鍵のローテーションを反映するために作り直した provider へ差し替えられる provider を提供します。
クロスカットなミドルウェア
すべてのハンドラは以下にラップされます:
| Layer | ソース | 役割 |
|---|---|---|
| CORS | internal/cors | discovery と /jwks は public CORS。/userinfo、/token、interaction / session の JSON 面、公開済みのプロトコルエンドポイント(/par、/revoke、/introspect、/register、/bc-authorize、/device_authorization、/end_session など)は厳格な許可リスト |
| 信頼プロキシ | internal/httpx | WithTrustedProxies を元に、X-Forwarded-For / -Proto / -Host から実クライアント IP を解決 |
| Cookie | internal/cookie | __Host- プリフィックス、AES-256-GCM、session は SameSite=Lax、互換可能なところは Strict |
| CSRF | internal/csrf | consent / logout の POST に対して double-submit + Origin / Referer チェック |
これらはオプションではありません。組み込み側のオプション設定に関係なく構造的に適用されます。
origin の集合は意図的に分かれています。API の CORS は明示的な WithCORSOrigins でのみ拡張され(API 互換性のため redirect URI の origin も含まれます)、interaction と end-session の手続きの検査は issuer origin と明示的な origin だけを許可します。redirect URI の origin だけで cross-origin interaction / logout POST が許可されることはありません。
Authorize → token のライフサイクル
最も流量の多いパスです。概略は次のとおりです。
/par と /end_session も大筋は同じ形です。上記が標準的な成功経路です。
認可ごとに、認証の状態は別々に持たれます。コード、そこから始まる refresh chain、発行されるアクセストークンには、それを生んだ認可の auth_time・acr・amr・authorization_details・OIDC claims リクエストが記録されます。(subject, client) ごとに 1 つだけあり、後続の認可が毎回書き換える grant を読みにいくことはありません。同じユーザが 2 回目にログインしても、先に発行されたコードが ID Token に載せる内容は変わりません。また、1 つの refresh chain を失効させたりローテーションしたりしても、同じクライアントにおけるユーザの別セッションには影響しません。
JWT アクセストークンに対して AccessTokens の shadow row が保存されるのは、RevocationStrategyJTIRegistry を選択した場合だけです。既定の RevocationStrategyGrantTombstone は失効時に grant tombstone を記録し、RevocationStrategyNone は JWT のサーバ側失効を無効にします。リフレッシュトークンは、OP の grant、クライアント登録、scope の条件をすべて満たした場合だけ保存されます。
既存セッションで処理するリクエスト
/auth は、毎回 chain を実行するわけではありません。キャッシュ済みの grant がリクエストを満たす場合や、first-party の consent 省略が適用される場合は、有効なセッションからそのままコードを発行できます。この経路でも LoginFlow の rule と Decider は適用されます。
panic する Rule の判定関数や Decider は fail-closed になります。Rule.When が panic すると一致したものとして扱われ、step は求められたままです。Decider が panic した場合は拒否します。Decider が nil や未知の形を返した場合も拒否です。
LoginFlow の内部
WithLoginFlow(LoginFlow{...}) は構築時に内部のパイプラインへコンパイルされます:
LoginFlow {Primary, Rules[], Decider, Risk}
│
▼ (compile)
internal/authn/CompiledLoginFlow
├── Primary → Authenticator(Step descriptor → runtime 実装の解決)
├── Rules[] → 順序付き (When, Then) ペア
├── Decider → 任意の short-circuit
└── Risk → chain 開始時に 1 回、score を cache各 authorize リクエストでは:
Primary.Beginがinteraction.Step(Prompt または Result)を返します。- UI ドライバ(HTML または SPA)が画面を描画し、ユーザが送信します。
Primary.ContinueがResult(Identityが確定している)まで進めます。- オーケストレータが
LoginContextを組み立てます(subject、scope、完了したステップ、cache 済みのリスクスコア、ACR values)。 Deciderが動きます(nil 以外の場合)。Pass以外の判定はそこで短絡します。すでに確定している subject と別の subject を返した factor は、chain をサーバエラーで終了させます。AAL3 の user-verification 検査は、プロンプトなしでBeginの中で完了する factor にも適用されます。- それ以外は
Rulesを順に評価します。最初にマッチしたルールのStep.Kind()がCompletedStepsにまだ含まれていなければ発火します。 - 発火するルールが無くなるまで繰り返し、その後にセッションを発行します。
LoginFlow.Riskは chain ごとに最大 1 回だけ呼ばれ、結果は cache されます。
ExternalStep 経由で自前の factor を差し込む手順は、ユースケース: カスタム認証器 を参照してください。
ストレージの差し込み口
ライブラリは、組み込み側の users テーブルを直接読み書きしません。store.Store interface(小さなサブストアの和集合)越しに会話します。エンドユーザ記録はこのサブストアに含まれません。user store は op.WithUserStore か、ログインフローの op.PrimaryPassword の Store から渡され、既存の users テーブルにマッピングすることが多い部分です(独自の user store を持ち込む を参照):
| サブストア | 何が入るか | 置き場所の目安 |
|---|---|---|
Clients | OAuth クライアントレジストリ | 通常は永続 |
AuthorizationCodes | one-shot な code(PKCE challenge、scope、認可ごとの認証状態) | 永続 |
RefreshTokens | リフレッシュトークンの連鎖(それぞれ ChainID を持つ)、ローテーション履歴 | 永続 |
AccessTokens | JWT アクセストークンの jti 単位の登録簿。RevocationStrategyJTIRegistry が使う | 永続 |
OpaqueAccessTokens | opaque アクセストークンのレコード(発行した chain の ChainID を持つ) | 永続 |
Grants | (user, client) ごとの consent scope。device-code・CIBA・token exchange の引き換えが記録する grant も含む | 永続 |
GrantRevocations | 失効した grant の tombstone。アクセストークンだけに絞った tombstone も置ける | 永続 |
Sessions | ブラウザセッションのレコード | 揮発に置いてもよい |
Interactions | 試行ごとの interaction 状態 | 揮発に置いてもよい |
ConsumedJTIs | JAR / DPoP jti のリプレイ検出集合 | 永続、または noeviction の Redis |
PushedAuthRequests | pushed authorization request | PAR が authorization-code transaction に参加する構成では永続 |
InitialAccessTokens / RegistrationAccessTokens | DCR の Initial / Registration Access Token | 永続 |
DeviceCodes | RFC 8628 のデバイス認可レコード | 永続 |
CIBARequests | OpenID Connect CIBA の backchannel authentication レコード | 永続 |
Metadata | OP 内部の key/value 状態(例: subject_mode マーカー) | 永続(未対応バックエンドは nil 可) |
SSFStreams / SSFEvents | Shared Signals の stream 設定と送信イベントのキュー。同じストレージ上に置き、任意の store.SharedSignalsStore を通して公開する | 永続。WithSharedSignals を使うときだけ必須 |
AuthnLockouts | subject ごとの認証失敗カウンタ。store.AuthnLockoutProvider 経由で解決される | 永続 |
「揮発に置いてもよい」サブストアは composite アダプタ越しに Redis 層へ置けます。composite アダプタは構築時に、すべての Kind に振り分け先があることを要求します。ただし Shared Signals 系の kind と AuthnLockouts は、振り分けがなければ WithDefault、それもなければ Clients のバックエンドに従います。composite.TxClusterKinds の 7 要素は、すべて同じ比較可能なバックエンドに解決され、そのバックエンドが store.Transactional を実装していなければなりません。
- 認可コード
- リフレッシュトークン
- grant
- PAR
- JWT アクセストークン登録
- opaque アクセストークン
- grant 失効 tombstone
それ以外の Kind は別のバックエンドに配置できます。無効な Kind、未設定の振り分け先、cluster の分割、比較不能な cluster バックエンド、非トランザクショナルなアンカーは、いずれも構築エラーになります。
失効の範囲と、任意の store interface
grant は、1 人の subject が 1 つのクライアントで持つすべてのセッションで共有されます。そのため grant 単位で失効させる処理は、呼び出し側が意図していないセッションまで終わらせてしまいます。store が失効の範囲をもっと狭く保てるよう、任意の interface が 2 つあります。
| Interface | 実装する先 | 追加されるもの |
|---|---|---|
store.OpaqueAccessTokenChainRevoker | OpaqueAccessTokens() | RevokeByChain: 1 つの refresh chain が発行した opaque アクセストークンだけを失効させる |
store.IssuanceRevocationChecker | GrantRevocations() | IsIssuanceRevoked: アクセストークン限定の tombstone(GrantTombstone.AccessTokensOnly)が、refresh やコードの引き換えを拒否しないようにする |
in-memory、SQL、DynamoDB の adapter は両方を実装しています。どちらも実装しない、または ChainID / AccessTokensOnly を保存しない自前 store は、grant 全体に対する挙動になります。コード・リフレッシュトークン・opaque アクセストークンの AuthorizationRecorded も同様で、認可ごとのフィールドを持たない行は grant を参照する動作に戻ります。各トリガーが及ぶ範囲は次のとおりです。
| トリガー | リフレッシュトークン | アクセストークン | grant への以後の発行 |
|---|---|---|---|
| refresh のローテーション | 旧トークンは失効し、chain は続く | その chain の opaque トークンだけ(chain 単位の失効を持たない store では grant 全体) | 可能 |
リフレッシュトークンへの /revoke | その chain(chain の起点を解決できないときは grant の refresh token すべて) | grant のすべてのアクセストークン | 可能: 別の chain と未引き換えのコードは引き換えられる |
| リフレッシュトークンの再送 | 再送された chain | grant のすべてのアクセストークン | 不可 |
| 認可コードの再送 | grant のすべての refresh token | grant のすべてのアクセストークン | 不可 |
grant の失効(RevokeSubject、Grant Management の revoke、/end_session の後始末) | すべて | すべて | 不可 |
RevokeSubject が失効させるのは grant とトークンだけです。削除したユーザについて、ブラウザセッション、ロックアウトの記録、factor の記録まで OP が持つものをすべて消すには、ユーザストアが subject を解決しなくなってから Provider.ForgetSubject を呼びます。
SQL を使う構成では、アップグレード前に Store.Migrations() の migration ファイルを適用してください(自前の migration ツール向けには op-schema export で書き出せます)。新しい列がないと op.New は起動を拒否します。
MFA factor のストア(EmailOTPStore、TOTPStore、PasskeyStore、RecoveryStore、AuthnLockoutStore)は store.Store のサブストアではありません。LoginFlow を組み立てる際に、対応する login-flow 値(StepEmailOTP.Store、StepTOTP.Store、PrimaryPasskey.Store、StepRecoveryCode.Store、WithAuthnLockoutStore)へ直接渡します。ロックアウトの store は、WithStore の値が AuthnLockouts() を持っていればそこからも取得されます。in-memory、SQL、DynamoDB adapter は同じ名前の accessor でこれらを公開します。examples/27-durable-mfa-store は、同梱 SQL factor store と OP のコアテーブルを 1 つの DB で使う例です。自前バックエンドでは同じ契約を実装します。
詳細は hot/cold ストレージ を参照してください。
Discovery 文書の組み立て
/.well-known/openid-configuration は OP の実効設定から discovery 文書を組み立てます。広告されるフィールドはそのまま OP の実挙動を表します。discovery と挙動の間に乖離はありません。理由は以下のとおりです。
response_types_supportedはWithGrants+ FAPI プロファイルから計算されます。client-credentials だけの OP では空配列となり、browser authorization / logout の surface と flag は広告されません。token_endpoint_auth_methods_supportedは、WithProfile(profile.FAPI2Baseline)またはWithProfile(profile.FAPI2MessageSigning)が有効なときに本ライブラリの FAPI 許可リストと交差します。scopes_supportedは組み込みの scope とWithScopeで登録された scope の和集合です。ui_locales_supportedは runtime locale resolver(seed bundle +WithLocaleBundle追加分)から自動導出されます。WithDiscoveryMetadata(...).UILocalesSupportedに非空の明示リストを渡した場合だけ、それが優先されます。code_challenge_methods_supportedは常に["S256"]です。plainは構造的に存在しません。id_token_signing_alg_values_supported、userinfo_signing_alg_values_supported、authorization_signing_alg_values_supported、introspection_signing_alg_values_supportedは、いずれも OP が発行に使うアルゴリズムだけを列挙します。既定はES256、op.WithSigningAlgorithm(op.AlgRS256)を指定するとRS256です。切り替え中は/jwksに 2 種類の鍵が載ることがありますが、discovery が示すのは使用中のアルゴリズムだけです。request_object_signing_alg_values_supportedは JOSE の許可リスト(RS256、PS256、ES256、EdDSA)です。dpop_signing_alg_values_supportedはそれより狭い集合(ES256、EdDSA、PS256)です。理由は FAQ § DPoP discovery を参照。
次に読む
- Options 索引 — すべての
op.With*を 1 ページに。上のルート一覧へのクロスリンク付き。 - Audit イベントカタログ — 各ハンドラ、各段階で何が発火するか。
- カスタム authenticator — オーケストレータのパイプラインがどこで自前コードを呼ぶか。
- hot / cold ストレージ — サブストアの tier 分けと、揮発 / 永続の境界の関係。