Skip to content

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:

ChoiceWhat you gainWhat you take on
1. Roll your own
(go-jose + a JWT lib)
Full control of the surfaceEvery 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 IdPOperationally richThe IdP owns the user table, the templates, the upgrade cadence. Your Go service becomes an embedder of their product.
3. go-oidc-providerLibrary owns the protocolYou 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_supported with 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=Lax for the session cookie, Strict where 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):

PlanPASSEDREVIEWSKIPPEDFAILED
oidcc-basic-certification-test-plan30320
fapi2-security-profile-id2-test-plan48910
fapi2-message-signing-id1-test-plan60920
Total (3 plans, 164 modules)1382150

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 of offline_access refresh 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/.