Skip to content

必須オプション ​

op.New(...) は構築時に不完全な設定を拒否します。そのため、安全でない OP が誤ってトラフィックを受け取ることはありません。必須オプションが複数欠けている場合は 1 つのエラーにまとめて報告し、各項目のセンチネルエラーには引き続き errors.Is で一致します。3 つのオプションは常に必須です。authorization_code grant を有効にしている場合は、さらに WithCookieKeys と WithLoginFlow の 2 つが必須になります。既定の grant 集合はこの grant を含みます。

常に必須の 3 つと、認可コードグラントで加わる 2 つ
常に必須なのは issuer・store・keyset の 3 つです。authorization_code グラントを有効にすると、cookie 鍵とログインフローが加えて必須になります。ユーザとブラウザが関わるグラントはこれだけだからです。常に必須op.WithIssuer全トークンに載る iss claimdiscovery と endpoint の URLop.WithStore認可コード · sessionクライアント · 連鎖 · JTIop.WithKeyset署名鍵 · ID トークンJWT アクセストークン · JARMauthorization_code グラントを有効にすると、この 2 つも必須になるop.WithCookieKeyssession と CSRF cookie をAES-256-GCM で暗号化するop.WithLoginFlowユーザのサインイン方法Primary のステップと、要素を足すルール
後ろの 2 つは任意ではなく条件付きの必須です。認可コードグラントを有効にしたままどちらかを省くと、op.New が起動時に拒否します。暗号化されていない cookie を発行したり、本番で全サインインがサーバエラーになったりする事態を防ぐためです。
オプション必須である理由
op.WithIssuerJWT の iss claim、discovery URL とエンドポイント URL、ログイン・ログアウト画面がリクエストを受け付ける origin を決めます。ここを誤ると下流の検査がすべて誤ります。
op.WithStore認可コード、セッション、リフレッシュトークンの連鎖、リプレイ検出用の JTI 集合、クライアントの保存先です。ライブラリはストレージに依存しないので、ストアがなければ状態を置く先がありません。
op.WithKeysetID トークン、JWT アクセストークン、JARM の署名鍵です。OP の署名アルゴリズムに合うアクティブな crypto.Signer(既定の ES256 なら ECDSA P-256、RS256 なら 2048 ビット以上の RSA)がなければ、ライブラリはトークンの発行を拒否します。
op.WithCookieKeyscookie の内容を 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 ​

go
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 ​

go
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 ​

go
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 鍵をアクティブな鍵にして、アルゴリズムを明示します。

go
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 ​

go
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 ​

go
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 を設定してください。

本番に近い構成例は ユースケース を参照してください。