必須オプション
op.New(...) は構築時に不完全な設定を拒否します。そのため、安全でない OP が誤ってトラフィックを受け取ることはありません。必須オプションが複数欠けている場合は 1 つのエラーにまとめて報告し、各項目のセンチネルエラーには引き続き errors.Is で一致します。3 つのオプションは常に必須です。authorization_code grant を有効にしている場合は、さらに WithCookieKeys と WithLoginFlow の 2 つが必須になります。既定の grant 集合はこの grant を含みます。
op.New が起動時に拒否します。暗号化されていない cookie を発行したり、本番で全サインインがサーバエラーになったりする事態を防ぐためです。| オプション | 必須である理由 |
|---|---|
op.WithIssuer | JWT の iss claim、discovery URL とエンドポイント URL、ログイン・ログアウト画面がリクエストを受け付ける origin を決めます。ここを誤ると下流の検査がすべて誤ります。 |
op.WithStore | 認可コード、セッション、リフレッシュトークンの連鎖、リプレイ検出用の JTI 集合、クライアントの保存先です。ライブラリはストレージに依存しないので、ストアがなければ状態を置く先がありません。 |
op.WithKeyset | ID トークン、JWT アクセストークン、JARM の署名鍵です。OP の署名アルゴリズムに合うアクティブな crypto.Signer(既定の ES256 なら ECDSA P-256、RS256 なら 2048 ビット以上の RSA)がなければ、ライブラリはトークンの発行を拒否します。 |
op.WithCookieKeys | cookie の内容を AES-256-GCM で暗号化するための 32 バイトのランダム素材です。有効な grant に authorization_code が含まれる場合に必須です(既定の grant 集合は authorization_code と refresh_token)。client_credentials だけを提供する OP は、これなしでも起動できます。セッションと CSRF の cookie は署名だけでなく暗号化されます。 |
op.WithLoginFlow | ユーザが本人であることを示す方法を決めます。有効な grant に authorization_code が含まれる場合に必須です。なければサインインできるユーザがいないため、最初の本番リクエストで失敗する代わりに op.New が起動を拒否します。 |
WithIssuer
op.WithIssuer("https://op.example.com")issuer は OP の識別子です。すべてのトークンに iss として載り、discovery URL の前半にもなります。RP はこれをバイト単位で比較するため、ライブラリは受け付ける表記を 1 つに絞っています。
issuer の検証: 仕様の要件とライブラリのポリシー
OIDC Discovery 1.0 §3 は、issuer を query・fragment のない HTTPS URL とし、メタデータの issuer と完全一致させるよう求めます。本ライブラリはさらに、末尾スラッシュ、scheme・host の大文字、既定ポート(HTTPS の :443、HTTP の :80)、正規形でないパス(..、.、重複スラッシュ)を拒否します。ローカル開発では loopback IP リテラル(127.0.0.0/8、[::1])に限って HTTP を許可します。テキストの localhost を使うには、開発用の明示的なオプトインである op.WithAllowLocalhostIssuerForDev() が必要です。HTTP の例外は OIDC Discovery の規定ではなく、ライブラリのポリシーです。
検査は op.New の中(internal/discovery.ValidateIssuer)で行います。綴りの誤りは起動時に失敗し、RP が拒否する discovery 文書が出回ることはありません。この厳しさによって、RFC 9207 の比較に使う issuer の表記がデプロイをまたいで 1 つに揃います。正規形の詳細は Issuer を参照してください。
WithStore
op.WithStore(inmem.New())
// または
op.WithStore(myCompositeStore)store.Store インタフェースは、小さなサブストアのインタフェースの和集合です(AuthorizationCodeStore、RefreshTokenStore、SessionStore、ClientStore …)。ほとんどの構成は、同梱アダプタのどれかを出発点にします。
op/storeadapter/inmem: 全サブストアをメモリに保持します。参考実装、開発、テスト用です。op/storeadapter/sql:database/sql上の永続サブストアです(SQLite、MySQL、Postgres)。op/storeadapter/dynamodb: DynamoDB 用のフルストアです。op/storeadapter/redis:composite経由で使う揮発層です(SessionStore、InteractionStore、ConsumedJTIStore)。op/storeadapter/composite: Hot / Cold 分離です。永続サブストアを一方のバックエンドに、揮発サブストアをもう一方に振り分けます。
既存ストレージへの接続
ストアのインタフェースは意図的に小さくしてあります。Cassandra、Spanner、etcd、独自規約の Redis クラスタなど、すでに運用しているものの上に実装できます。op/store/contract の契約テストスイートで、同梱アダプタと同じ期待を満たすかを検査できます。
一部の挙動は、省略可能なインタフェースと追加のレコードフィールドで有効になります。store.OpaqueAccessTokenChainRevoker と store.IssuanceRevocationChecker を実装し、ChainID と認可ごとのフィールドを保存するストアでは、失効の範囲が 1 つのリフレッシュトークン連鎖に限られます。これらがないストアもそのまま動きますが、失効は grant 単位になり、同じクライアントにおけるユーザの他のセッションにも及ぶことがあります。
WithKeyset
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
op.WithKeyset(op.Keyset{
{KeyID: "k1", Signer: priv},
})Keyset は {KeyID, Signer} レコードのスライスです。Signer は crypto.Signer を実装している必要があります。先頭のエントリがアクティブな鍵で、新しいトークンに署名します。その鍵の種類は OP の署名アルゴリズムと一致させます。既定は ES256 で、ECDSA P-256 の鍵が必要です。Vault や KMS のハンドルでも、公開鍵が P-256 で署名が ES256 として検証できれば使えます。後続のエントリは JWKS に残り、RP が古いトークンを検証できるようにします。鍵のローテーションはこの仕組みで行います。Ed25519 と P-384 の鍵、および 2048 ビット未満の RSA 鍵は op.New が拒否します。
RS256 で署名するときは、2048 ビット以上の RSA 鍵をアクティブな鍵にして、アルゴリズムを明示します。
op.WithSigningAlgorithm(op.AlgRS256)op.LoadSigningKey と op.LoadKeyset は、PKCS#1 / PKCS#8 の RSA PEM と RSA の秘密鍵 JWK を読み込めます。アクティブな鍵がアルゴリズムと合わない場合、および署名鍵の公開鍵が WithEncryptionKeyset にも含まれている場合は、op.New が失敗します。署名鍵と暗号化鍵は別のものにしてください。RS256 は profile.FAPI2Baseline、profile.FAPI2MessageSigning、profile.FAPICIBA とは併用できません。FAPI 2.0 Security Profile §5.4.1 が許可していないためです。稼働中の OP のアルゴリズムを変える手順は 署名アルゴリズムを切り替える を参照してください。
アルゴリズム許可リスト
OP が発行する JWT は、すべて 1 つのアルゴリズムで署名します。既定は ES256 で、指定すれば RS256 です。クライアントごとの選択はありません。client assertion、JAR request object、DPoP proof を検証する側では、各プロトコルが許す範囲で、閉じた許可リスト(RS256、PS256、ES256、EdDSA)を使います。HS* と none は構造的に存在しません。 列挙型に値がなく、internal/jose.ParseAlgorithm はそれらの文字列に ok=false を返します。これにより RFC 7519 §6 と RFC 8725 §2.1 の alg confusion 攻撃を、実行時の if 文ではなく型で閉じています。
WithCookieKeys
key := make([]byte, 32) // ちょうど 32 バイト
if _, err := rand.Read(key); err != nil { /* … */ }
op.WithCookieKeys(key)
// 鍵のローテーション:
op.WithCookieKeys(currentKey, previousKey)32 バイトの鍵が、セッションと CSRF の cookie を暗号化する AES-256-GCM の鍵になります。先頭の鍵で暗号化し、後続の鍵は復号の試行にだけ使うので、稼働中のユーザをサインアウトさせずに鍵を入れ替えられます。op.New がこのオプションを要求するのは、有効な grant に authorization_code が含まれる場合だけです。この grant は既定で有効なため、ほとんどの構成では必須ですが、client_credentials だけに絞った OP は cookie 鍵なしで起動できます。
cookie の方式は固定
cookie は常に __Host- prefix(Domain なし、Path=/、Secure)を付けます。セッション cookie は SameSite=Lax、CSRF cookie は SameSite=Strict で、同意とログアウトの POST には double-submit トークンと Origin / Referer の検査が加わります。いずれも変更できません。ライブラリが動作する最低ラインです。
WithLoginFlow
op.WithLoginFlow(op.LoginFlow{
Primary: op.PrimaryPassword{Store: users}, // 自前の users テーブルに対する store.UserPasswordStore
})LoginFlow は、ユーザのサインイン方法を宣言します。ユーザを特定する Primary のステップが 1 つと、条件を満たしたときに TOTP などのステップを足す Rules(任意)で構成します。PrimaryPassword は store.UserPasswordStore に対してパスワードを検証します。本番の OP では自前のストアを渡します。アプリケーションがすでに持つ users テーブルに対して FindBySubject、FindByUsername、ReadPasswordHash を実装した型です(既存ユーザストアの投影、既存のデータベースに組み込む)。ID トークンと /userinfo の claim も同じストアから供給されます。最初に動かすだけで users テーブルが無い場合は、in-memory アダプタの st.UserPasswords() が最も手軽な代用品です。ユーザがまだ存在しないアプリケーションが永続化するなら、同梱 SQL アダプタの oidc_users テーブルを使います。
動作しない flow は op.New が拒否します。
- 先行のステップが特定したユーザを前提とするステップ(
StepTOTP、StepEmailOTP、StepRecoveryCode)をPrimaryにした場合。PrimaryにはPrimaryPassword、PrimaryPasskey、ExternalStepのいずれかを置き、それ以外はRulesに置きます。 Primary(またはそこに置いたStepOneOf)のPrimaryPasskeyにRequireUserVerificationがない場合。そこではパスワードレスのログインとなり、唯一の認証要素になるためです。- 失敗を数えるステップ(
PrimaryPassword、StepTOTP、StepEmailOTP、StepRecoveryCode、CountFailuresを持つExternalStep)があるのに、ロックアウトストアが解決できない場合。op.Newはop.ErrAuthnLockoutStoreRequiredを返します。AuthnLockouts()を持つWithStoreのバックエンド(in-memory、SQL、DynamoDB、composite は持っています)を使うか、op.WithAuthnLockoutStore(...)を渡してください。 WithLoginFlowを 2 回指定した場合。1 つのプロバイダが持つログインフローは 1 つです。- エンドユーザを解決しうる grant(
client_credentials以外のすべて)があるのに、エンドユーザ記録の供給元がない場合。記録は、このフローのop.PrimaryPasswordのStore(ID トークンと/userinfoの claim もここから出ます)か、op.WithUserStoreから取られ、両方あればop.WithUserStoreが優先されます。PrimaryPasswordを持たないフロー(passkey のみ)や、device-code / CIBA だけを提供する OP はop.WithUserStoreが必須で、同梱アダプタならop.WithUserStore(st.Users())です。op.WithStoreに渡したstore.Storeがユーザの参照元になることはありません。
ライブラリにない認証要素は、自前の op.Authenticator を op.ExternalStep で包んで、Primary か Rules に置きます。多要素認証はこのオプションの上に組み立てます。
ローカルで動かす
既定値は本番を前提にしており、https のみを受け付け、外向きの取得も公開アドレス宛てに限ります。issuer と redirect_uri の検査では http://127.0.0.1 と http://[::1] が除外されているので、ループバックの IP リテラルで動かすローカルの OP にはオプションは要りません。IP リテラルで足りないケースにはそれぞれ専用のオプションがあり、1 つを指定しても他の制限は緩みません。
| 必要なこと | オプション | 本番での使用 |
|---|---|---|
redirect URI に http://localhost を使う | op.WithAllowLocalhostRedirectURIs() | 可。localhost で待ち受けるネイティブクライアントのため。op.New が警告を出します |
issuer のホストを localhost にする(WebAuthn の RP ID がドメインでなければならないローカルの passkey 環境など) | op.WithAllowLocalhostIssuerForDev() | 不可。issuer のホストが localhost でなければ拒否されます |
ループバック上の平文 http の backchannel_logout_uri や Shared Signals の push 先 | op.WithAllowInsecureOutboundForDev(op.OutboundBackchannel)(または op.OutboundSharedSignals) | 不可。issuer が開発用ホスト(ループバックの IP リテラル、localhost、.localhost / .test / .invalid / .example 配下の名前)でなければ拒否されます |
プライベートネットワーク上のクライアント JWKS、JAR の request_uri、sector_identifier_uri、Client ID Metadata Document、バックチャネルの受信先 | op.WithAllowPrivateNetwork(...) に対応する op.Outbound* を指定 | 可。社内向けの配備のため。対象ごとに個別に解除します |
redirect_uri と issuer の除外は、外向きの SSRF ゲートには及びません。ループバックやプライベートアドレス上のクライアント JWKS やバックチャネルの受信先は、その対象を指定するまで拒否されたままです。リライングパーティを別コンテナのアドレスで動かすローカル環境では、使う機能に応じた対象を op.WithAllowPrivateNetwork に指定するのが普通です。private_key_jwt のクライアントなら op.OutboundJWKS が必要です。詳細は Redirect URI、Issuer、ネットワーク関連のオプション にあります。
実運用では必要になるもの
起動できることと、使えることは別です。次のオプションは起動には不要ですが、実際の OP のほとんどが設定します。
op.WithStaticClients(...)またはop.WithDynamicRegistration(...): どちらかがないと、クライアントは認証できません。静的に登録する confidential client がclient_secret_basicまたはclient_secret_postを使う場合は、32 バイト以上の平文SecretかSecretHashが必要です。ないとop.Newが拒否します。op.NewClientSecretが対応する secret とハッシュの組を返します。op.WithScope(...):api:readのような独自の scope を登録します。標準の OIDC scope は登録不要です。op.WithAuditLogger(...)とop.WithLogger(...): 監査イベントとログを自前の基盤に流します。op.WithLoggerを設定しない場合、op.Newが構成について出す起動時の診断(claim をどのユーザストアから読むかを示すclaim_sourceの行 など)はslog.Default()に出ますが、型が違うため外した claim の WARN のようなリクエスト時の記録は破棄されます。最初の起動からop.WithLoggerを設定してください。
本番に近い構成例は ユースケース を参照してください。