Skip to content

アーキテクチャ概観 ​

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-configurationinternal/discoveryOP の実効設定を discovery 文書として返す常時
/jwksinternal/jwks署名鍵セットの公開側常時
/.well-known/oauth-protected-resource(+ パスのサフィックス)internal/protectedresource宣言した保護リソースごとの RFC 9728 メタデータWithProtectedResources
/authinternal/authorizeendpointリクエストの解析、クライアントと redirect_uri の検証、interaction へのリダイレクトauthorization_code grant が有効な場合
/interaction/{uid}internal/authorizeendpoint + op/interaction ドライバログインと同意の手続き。HTML で描画するか、SPA へ JSON で返す/auth と同時
/parinternal/parendpointpushed authorization request を受け取り、一度限りの request_uri を返すfeature.PAR
/tokeninternal/tokenendpointクライアント認証、認可コードとリフレッシュトークンの引き換え、トークン発行常時
/userinfointernal/userinfo提示されたアクセストークンが指す subject の claim常時
/revokeinternal/revokeendpointRFC 7009 によるリフレッシュトークン / アクセストークンの失効feature.Revoke
/introspectinternal/introspectendpointRFC 7662 によるトークン状態を、認可済みのリソースサーバへ返すfeature.Introspect
/device_authorizationinternal/devicecodeendpointRFC 8628 のデバイス認可。device_code と user_code を返すdevice-code grant
/bc-authorizeinternal/cibaendpointCIBA の backchannel authentication リクエスト。auth_req_id を返すCIBA grant
/grant_management/{grant_id}internal/grantmgmtendpointgrant 1 件の照会と失効WithGrantManagement
/end_sessioninternal/endsessionRP 起点のログアウト。バックチャネルの一斉通知もここから/auth と同時(authorization_code grant)
/registerinternal/registrationendpoint動的クライアント登録と、クライアントごとの管理ルートWithDynamicRegistration
/ssf/streams
/ssf/status
/ssf/verify
/ssf/poll/{stream_id}
internal/ssfShared 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ソース役割
CORSinternal/corsdiscovery と /jwks は public CORS。/userinfo、/token、interaction / session の JSON 面、公開済みのプロトコルエンドポイント(/par、/revoke、/introspect、/register、/bc-authorize、/device_authorization、/end_session など)は厳格な許可リスト
信頼プロキシinternal/httpxWithTrustedProxies を元に、X-Forwarded-For / -Proto / -Host から実クライアント IP を解決
Cookieinternal/cookie__Host- プリフィックス、AES-256-GCM、session は SameSite=Lax、互換可能なところは Strict
CSRFinternal/csrfconsent / 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 のライフサイクル ​

最も流量の多いパスです。概略は次のとおりです。

authorize と interaction — OP が組み込み側を呼ぶ箇所
ブラウザが authorize リクエストと interaction を進めます。OP はクライアントを引き、リクエストを検査し、ログインを LoginFlow に渡し、認可コードをストアに保存します。これらの呼び出しはすべて右端のレーン、つまり組み込み側のコードに届きます。authorize リクエストinteractionブラウザuser agentOPgo-oidc-provider組み込み側Store · LoginFlowGET /authClients.GetClientredirect_uri の完全一致リクエストを検査PKCE · scoperesponse_type302 → /interaction/{uid}POST /interaction/{uid}ログインBegin / ContinueStep チェーンResultsubject · AAL · AMR200 同意画面POST /interaction/{uid}同意AuthorizationCodes.Savecode + PKCE チャレンジ
右端のレーンは組み込み側のものです。そこへ向かう矢印はすべてインタフェース呼び出しで、だからこそ中央のプロトコル処理に手を入れずに、保存先とログイン体験を差し替えられます。
認可コードの引き換え — クライアント認証が先
認可コードはブラウザ経由で RP に戻り、RP が /token で直接引き換えます。OP はクライアントを認証し、コードを消費し、トークンの登録と保存をストアに依頼し、トークン応答を返します。コードがブラウザ経由で戻るコードの引き換えRP別アプリOPgo-oidc-providerストア組み込み側302 → redirect_uricode · state · issPOST /tokengrant_type=authorization_codeClients.GetClient先にクライアント認証AuthorizationCodes.Consume続けて PKCE と redirect_uriAccessTokens.RegisterJTIRegistry のみRefreshTokens.Save条件成立時200 · access_token · id_token+ 条件成立時は refresh_token
コードはブラウザを通りますが、引き換えは直接のバックチャネルで行われます。クライアント認証はコードの消費より前、PKCE と redirect_uri の検証はその後です。

/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 は適用されます。

既存セッションで処理する authorize リクエスト
/auth が有効なセッションからコードを発行できる場合、LoginFlow の rule と Decider を、primary step が完了した直後として先に評価する。追加の step を求められなければコードを発行する。step を求められた場合、prompt=none なら interaction_required を返し、それ以外はセッションの subject から chain を実行してから発行する。既存セッションの再利用有効な grant が要求を満たすfirst-party の consent 省略rule と DeciderPrimary 完了直後として扱うセッションの amr が既済の step を示す認可コードを発行step が不要interaction_requiredstep が必要で prompt=nonechain を実行step が必要で prompt=none 以外
risk に基づく rule は chain の内側でだけ発火するため、この経路では動きません。chain はセッションの subject から始まり、すでに確定している subject と別の subject を返す factor は、chain をサーバエラーで終了させます。

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
LoginFlow — 宣言し、一度コンパイルし、リクエストごとに回す
WithLoginFlow に渡すのは仕様です。これは一度 CompiledLoginFlow にコンパイルされ、オーケストレータがリクエストごとにループで実行します。第一要素を開始し、画面を出し、Identity を確定し、Decider と Rules を評価し、発火するルールが無くなるまで繰り返します。宣言一度だけコンパイルリクエストごとに実行LoginFlowPrimary第一要素Rules[]追加要素を求める条件Decider · RiskAAL 方針とシグナルコンパイルCompiledLoginFlowinternal/authnop.New で解決・検証Primary.Begin → Stepチェーン開始時に 1 回画面表示 → ユーザが送信subject · AAL · AMR を確定Decider、続けて Rulesルールが発火 → 次の Step へ発火するルールが無いセッションを発行する
先にコンパイルしておくことで、設定の誤りが実行時ではなく起動時のエラーになります。リクエストが届く頃には解釈すべきものは残っておらず、あとはチェーンを辿るだけです。

各 authorize リクエストでは:

  1. Primary.Begin が interaction.Step(Prompt または Result)を返します。
  2. UI ドライバ(HTML または SPA)が画面を描画し、ユーザが送信します。
  3. Primary.Continue が Result(Identity が確定している)まで進めます。
  4. オーケストレータが LoginContext を組み立てます(subject、scope、完了したステップ、cache 済みのリスクスコア、ACR values)。
  5. Decider が動きます(nil 以外の場合)。Pass 以外の判定はそこで短絡します。すでに確定している subject と別の subject を返した factor は、chain をサーバエラーで終了させます。AAL3 の user-verification 検査は、プロンプトなしで Begin の中で完了する factor にも適用されます。
  6. それ以外は Rules を順に評価します。最初にマッチしたルールの Step.Kind() が CompletedSteps にまだ含まれていなければ発火します。
  7. 発火するルールが無くなるまで繰り返し、その後にセッションを発行します。LoginFlow.Risk は chain ごとに最大 1 回だけ呼ばれ、結果は cache されます。

ExternalStep 経由で自前の factor を差し込む手順は、ユースケース: カスタム認証器 を参照してください。

ストレージの差し込み口 ​

ライブラリは、組み込み側の users テーブルを直接読み書きしません。store.Store interface(小さなサブストアの和集合)越しに会話します。エンドユーザ記録はこのサブストアに含まれません。user store は op.WithUserStore か、ログインフローの op.PrimaryPassword の Store から渡され、既存の users テーブルにマッピングすることが多い部分です(独自の user store を持ち込む を参照):

サブストア何が入るか置き場所の目安
ClientsOAuth クライアントレジストリ通常は永続
AuthorizationCodesone-shot な code(PKCE challenge、scope、認可ごとの認証状態)永続
RefreshTokensリフレッシュトークンの連鎖(それぞれ ChainID を持つ)、ローテーション履歴永続
AccessTokensJWT アクセストークンの jti 単位の登録簿。RevocationStrategyJTIRegistry が使う永続
OpaqueAccessTokensopaque アクセストークンのレコード(発行した chain の ChainID を持つ)永続
Grants(user, client) ごとの consent scope。device-code・CIBA・token exchange の引き換えが記録する grant も含む永続
GrantRevocations失効した grant の tombstone。アクセストークンだけに絞った tombstone も置ける永続
Sessionsブラウザセッションのレコード揮発に置いてもよい
Interactions試行ごとの interaction 状態揮発に置いてもよい
ConsumedJTIsJAR / DPoP jti のリプレイ検出集合永続、または noeviction の Redis
PushedAuthRequestspushed authorization requestPAR が authorization-code transaction に参加する構成では永続
InitialAccessTokens / RegistrationAccessTokensDCR の Initial / Registration Access Token永続
DeviceCodesRFC 8628 のデバイス認可レコード永続
CIBARequestsOpenID Connect CIBA の backchannel authentication レコード永続
MetadataOP 内部の key/value 状態(例: subject_mode マーカー)永続(未対応バックエンドは nil 可)
SSFStreams / SSFEventsShared Signals の stream 設定と送信イベントのキュー。同じストレージ上に置き、任意の store.SharedSignalsStore を通して公開する永続。WithSharedSignals を使うときだけ必須
AuthnLockoutssubject ごとの認証失敗カウンタ。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.OpaqueAccessTokenChainRevokerOpaqueAccessTokens()RevokeByChain: 1 つの refresh chain が発行した opaque アクセストークンだけを失効させる
store.IssuanceRevocationCheckerGrantRevocations()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 と未引き換えのコードは引き換えられる
リフレッシュトークンの再送再送された chaingrant のすべてのアクセストークン不可
認可コードの再送grant のすべての refresh tokengrant のすべてのアクセストークン不可
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 を参照。

次に読む ​