Skip to content

Required options

op.New(...) rejects partial configurations at construction time, so an unsafe OP cannot accidentally take traffic. The four required options are:

OptionWhy it's required
op.WithIssuerDefines the JWT iss claim, the discovery URL, and the cookie scope. Wrong here, every downstream check is wrong.
op.WithStoreWhere authcodes, sessions, refresh chains, JTI replay sets, and clients live. The library is storage-agnostic; without a store it has nowhere to put state.
op.WithKeysetThe signing keys for ID tokens / JWT access tokens / JARM. The library refuses to mint tokens without an ECDSA P-256 crypto.Signer that can produce ES256 signatures.
op.WithCookieKeys32 bytes of random material used as an AES-256-GCM key for cookie payloads. Session and CSRF cookies are encrypted, not just signed.

WithIssuer

go
op.WithIssuer("https://op.example.com")

OIDC Discovery 1.0 §3 / FAPI 2.0 §5.4

The issuer must be https://, must not have a trailing slash, must not carry a query string or fragment. The scheme and host must be all-lowercase, no default port may appear (:443 for https, :80 for http), and the path must be canonical (no .., ., or duplicate slashes). Loopback IP literals (127.0.0.0/8, [::1]) are exempted from the https:// requirement so localhost dev works; the textual host localhost is not in the carve-out (DNS hijack risk per RFC 8252 §7.3).

internal/discovery.ValidateIssuer enforces the shape as defense-in-depth over the option setter. A typo (trailing slash, :443, uppercase host, etc.) will fail op.New, not silently produce a discovery document RPs reject. The strictness is what makes RFC 9207 byte-exact mix-up defence hold end-to-end. See Issuer for the full canonical-form discussion.

WithStore

go
op.WithStore(inmem.New())
// or
op.WithStore(myCompositeStore)

The op.Store interface is the union of small substore interfaces (AuthCodeStore, RefreshTokenStore, SessionStore, ClientStore, …). You usually compose a Store from one of the bundled adapters:

BYO storage

The store interfaces are intentionally tiny so you can implement them against whatever you already run — Cassandra, Spanner, etcd, a Redis cluster with your own conventions. The contract test suite at op/store/contract verifies a store against the same expectations the bundled adapters meet.

WithKeyset

go
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
op.WithKeyset(op.Keyset{
  {KeyID: "k1", Signer: priv},
})

A Keyset is a slice of {KeyID, Signer} records. Signer must implement crypto.Signer and expose an ECDSA P-256 public key. A vault / KMS handle is fine as long as the public key is P-256 and signatures verify as ES256; RSA and Ed25519 OP signing keys are rejected at op.New.

Algorithm allow-list

OP-issued JWTs are signed with ES256 only. Inbound JOSE verification for client assertions, JAR request objects, and DPoP proofs uses the library's closed allow-list (RS256, PS256, ES256, EdDSA) where each protocol permits it. HS* and none are structurally absent — there is no enum value for them and internal/jose.ParseAlgorithm returns ok=false for those strings. This closes RFC 7519 §6 / RFC 8725 §2.1 algorithm-confusion attacks at the type level, not via runtime if-statements.

WithCookieKeys

go
key := make([]byte, 32) // exactly 32 bytes
if _, err := rand.Read(key); err != nil { /* … */ }
op.WithCookieKeys(key)

// Multi-key rotation:
op.WithCookieKeys(currentKey, previousKey)

The 32 bytes seed an AES-256-GCM cipher used to encrypt session and CSRF cookies. Keys rotate with WithCookieKeys: the first key is used to encrypt; subsequent keys are tried for decryption so a rolling key swap doesn't bounce active sessions.

Cookie scheme is non-negotiable

Cookies always use the __Host- prefix (no Domain, Path=/, Secure). SameSite=Lax for the session, double-submit + Origin / Referer check on the consent / logout POST. None of this is configurable — it's the floor.

What's not required (but you almost always want)

  • op.WithStaticClients(...) or op.WithDynamicRegistration(...) — without one of these, no client can authenticate.
  • op.WithAuthenticators(...) — defines how the OP verifies a user. The default is "no authenticator", which means login always fails.
  • op.WithLoginFlow(...) — composes authenticators + rules into the step-up policy (e.g. password → TOTP, password → captcha-after-N-fails).

See the Use cases for production-shaped wirings.