Why go-oidc-provider
Why this library exists
This library is a personal project. It distills the author's own pain — accumulated across years of standing up OIDC providers with OSS libraries in other languages — into a Go library so the parts that should be one switch actually are, and the parts that should never be on by default actually aren't.
Things that ought to be easy to embed (two-factor, passkeys, risk-based auth, SPA-driven flows, FAPI 2.0, …) are first-class building blocks, while deprecated, unsafe flows kept "for compatibility" elsewhere (Implicit, ROPC, alg=none, …) are not exposed as public options at all. Details are in the sections below; see Use cases for production-shaped configurations.
You're writing a Go service. You need to be an OpenID Connect Provider — issuing ID tokens / access tokens, hosting /authorize and /token, signing the discovery document. The choices on the market are:
| Choice | What you gain | What you take on |
|---|---|---|
| 1. Roll your own ( go-jose + a JWT lib) | Full control of the surface | Every CVE class is yours — algorithm confusion, redirect URI mismatch, PKCE downgrade, refresh-token reuse, cookie scope, CSRF on the consent post, … |
| 2. Front a heavyweight IdP | Operationally rich | The IdP owns the user table, the templates, the upgrade cadence. Your Go service becomes an embedder of their product. |
3. go-oidc-provider | Library owns the protocol | You bring user accounts, storage, and UI |
This page argues option 3 by walking through the things that hurt when you build options 1 or 2.
Pain → Answer
"I want one switch for FAPI 2.0"
Background — what FAPI 2.0 demands
FAPI 2.0 Baseline mandates PAR (RFC 9126) for authorization requests, PKCE (RFC 7636), sender-constrained tokens via DPoP (RFC 9449) or mTLS (RFC 8705), ES256 signing, and redirect_uri exact match. Message Signing additionally requires JAR (RFC 9101) and JARM for non-repudiation of authorize request/response. Toggling these by hand is a half-dozen options and three places the discovery document needs to agree.
op.WithProfile(profile.FAPI2Baseline) does the whole thing — auto-enables PAR
- JAR + DPoP, intersects
token_endpoint_auth_methods_supportedwith the FAPI allow-list, locks the algorithm list.
Conflicts caught at startup
The constructor refuses to start if the declared profile and the declared options conflict, so partial-FAPI never escapes review.
"I don't want to give up my users table"
op.WithStore(s store.Store) plugs into a tiny set of substore interfaces (store.AuthCodeStore, store.SessionStore, store.UserStore, …). The library never reads or writes your users table directly — your store implementation does.
Reference adapters: inmem, sql (SQLite / MySQL / Postgres), redis (volatile substores), composite (hot/cold splitter). DynamoDB is planned.
"Cookies and CSRF on the consent POST are a minefield"
Easy to get one detail wrong
The __Host- prefix, no Domain, Path=/, Secure, AES-256-GCM, double-submit CSRF, Origin / Referer check, the right SameSite — miss any one and you have a CVE class.
The library bakes in:
__Host-cookie prefix (no Domain, Path=/, Secure)- AES-256-GCM encryption (cookie key supplied via
op.WithCookieKey) - Double-submit CSRF + Origin / Referer check on the consent / logout POST
- SameSite=
Laxfor the session cookie,Strictwhere compatible
You don't write any of this. You generate one 32-byte key, hand it to WithCookieKey, and the cookie scheme is correct.
"I want to drive UI from a SPA"
op.WithSPAUI(...) swaps the default HTML driver for a JSON one — the SPA (React, Vue, Svelte, Angular, …) hits /interaction/{uid}/... for the prompts and posts back signed responses.
SPA-safe error rendering
Error pages emit <div id="op-error" data-code="..." data-description="..."> so the SPA host can document.querySelector('#op-error') without parsing markup, under CSP default-src 'none'; style-src 'unsafe-inline'.
"I need real conformance, not 'we follow the RFC'"
Each release is regressed against the OpenID Foundation conformance suite. Latest baseline (sha ab23d3c):
| Plan | PASSED | REVIEW | SKIPPED | FAILED |
|---|---|---|---|---|
| oidcc-basic-certification-test-plan | 30 | 3 | 2 | 0 |
| fapi2-security-profile-id2-test-plan | 48 | 9 | 1 | 0 |
| fapi2-message-signing-id1-test-plan | 60 | 9 | 2 | 0 |
| Total (3 plans, 164 modules) | 138 | 21 | 5 | 0 |
Reading REVIEW / SKIPPED
REVIEW is OFCS's "human reviewer must look" verdict — the OP error pages that stay there are intentional (details). SKIPPED are modules that exercise things the OP refuses by design (e.g. alg=none request objects).
"I need observable refresh-token rotation"
Refresh tokens rotate by default. Reuse-detection invalidates the entire chain.
op.WithRefreshGracePeriod— widens the rotation window for racing clients.op.WithRefreshTokenOfflineTTL— separates the lifetime ofoffline_accessrefresh tokens (stay-signed-in) from conventional rotation.
The token.issued / token.refreshed audit events carry an offline_access flag in extras so SOC dashboards can split the chains.
"I want metrics, but not a /metrics route I didn't ask for"
op.WithPrometheus(reg) registers a curated counter set on your registry. The library does not mount /metrics itself — that's your router's job.
The same separation holds for tracing (you bring otelhttp) and request duration histograms (your middleware).
What this library is not
Out of scope on purpose
- Not an IdP. It does not store users, hash passwords, or send email. You bring the user model and an
op.Authenticator. There's a TOTP authenticator shipped, but the password check is yours. - Not a generic OAuth2 framework. It targets OpenID Connect Core 1.0 and the FAPI 2.0 family. Pure-OAuth2 builds are supported via
op.WithOpenIDScopeOptional, but the library is opinionated toward OIDC. - Not a UI kit. The default HTML driver exists so the OP boots without configuration; production embedders ship their own templates or a SPA.
Next
- Concepts: OAuth 2.0 / OIDC primer — read this first if "client_credentials" or "authorization_code + PKCE" are unfamiliar.
- Quick Start — get a minimal OP running in 30 lines of Go.
- Use cases — production-shaped examples, each linked to a build-tagged file in
examples/.