Skip to content

go-oidc-provider

OpenID Connect Provider you mount on any Go http.Handler.

Works with net/http, chi, gin, or any router. PAR · JAR · DPoP · mTLS built in, PKCE required by default, and unsafe configurations fail at op.New. Targets FAPI 2.0 Final Baseline & Message Signing.

FAPI 2.0 FINAL · OFCS 201 PASSED / 259 MODULES · 9 PLANS · APACHE-2.0 · GO ≥ 1.26

The provider is a handler inside your application
Clients call the OpenID Provider over HTTPS. The provider is an http.Handler that your Go application mounts under /oidc on its own router. It always serves discovery, JWKS, authorization, token, userinfo and logout endpoints, adds PAR, introspection, revocation, registration, device and CIBA endpoints when those features are enabled, and reaches storage, signing keys and the login flow through the options you pass to op.New.clientsWeb app (RP)confidential clientauthorization code + PKCESPA · mobile · CLIpublic clientPKCE · device codeResource serververifies access tokensJWKS · /introspectyour Go applicationoptions you passop.New(…)http.Handler mounted at /oidcalways/auth/jwks/token/end_session/userinfo/.well-known/openid-configurationwhen enabled/par/introspect/revoke/register/device_authorization/bc-authorizebuilt inPKCE · PAR · JAR · JARM · DPoP · mTLS · FAPI 2.0 profileStoreWithStoreinmem · SQL · DynamoDBSigning keysWithKeysetES256 · rotationLogin flowWithLoginFlowpassword · TOTP · passkey
Nothing runs as a separate server — the handler mounts on net/http, chi, gin or any other router. Storage, keys and sign-in are parts of your application that the provider calls. Logging and metrics hook in the same way, through WithLogger and WithPrometheus.

Standard use cases ​

Five typical configurations. Each one links to a runnable example in the upstream repository.

1. Stand up the smallest possible OP ​

go
package main

import (
  "log"
  "log/slog"
  "net/http"

  "github.com/libraz/go-oidc-provider/op"
  "github.com/libraz/go-oidc-provider/op/storeadapter/inmem"
)

func main() {
  st := inmem.New()
  handler, err := op.New(
    op.WithIssuer("https://op.example.com"),
    op.WithLogger(slog.Default()), // without it, request-time warnings are discarded
    op.WithStore(st),
    op.WithKeyset(myKeyset),    // see Quick Start: ephemeral key generation
    op.WithCookieKeys(cookieKey), // 32 bytes — AES-256-GCM
    op.WithLoginFlow(op.LoginFlow{
      Primary: op.PrimaryPassword{Store: st.UserPasswords()},
    }),
  )
  if err != nil {
    log.Fatal(err)
  }
  log.Fatal(http.ListenAndServe(":8080", handler))
}

See examples/01-minimal and Quick Start.

2. Run the library's FAPI 2.0 Baseline profile ​

go
st := inmem.New()
clientJWKS, _ := op.LoadPublicJWKS("conformance/keys/fapi-client.jwks.json")
handler, _ := op.New(
  op.WithIssuer("https://op.example.com"),
  op.WithStore(st),
  op.WithKeyset(myKeyset),
  op.WithCookieKeys(cookieKey),
  op.WithLoginFlow(op.LoginFlow{
    Primary: op.PrimaryPassword{Store: st.UserPasswords()},
  }),
  op.WithProfile(profile.FAPI2Baseline), // PAR, PKCE, DPoP unless feature.MTLS is enabled
  op.WithStaticClients(op.PrivateKeyJWTClient{
    ID: "demo-fapi",
    JWKS: clientJWKS,
    RedirectURIs: []string{"https://app.example.com/callback"},
    Scopes: []string{"openid", "profile"},
  }),
)

Why one switch is enough

op.WithProfile(profile.FAPI2Baseline) applies the FAPI 2.0 Security Profile (Final) to the whole OP.

  • Every authorization request goes through PAR, with PKCE.
  • Access tokens are sender-constrained: DPoP by default, mTLS as well once you enable feature.MTLS.
  • Clients authenticate with private_key_jwt or mTLS, and a client assertion's aud is the issuer.
  • Refresh tokens do not rotate, and access tokens live at most 10 minutes.

Signed request objects are accepted but not required; profile.FAPI2MessageSigning requires them. See Use case: FAPI 2.0 Baseline.

3. Issue tokens to backend services (no end user) ​

go
handler, _ := op.New(
  op.WithIssuer("https://op.example.com"),
  op.WithStore(inmem.New()),
  op.WithKeyset(myKeyset),
  op.WithGrants(grant.ClientCredentials),
  op.WithStaticClients(op.ConfidentialClient{
    ID: "backend-service",
    Secret: "cc-demo-secret-rotate-me-0123456789abcdef",
    AuthMethod: op.AuthClientSecretBasic,
    GrantTypes: []string{"client_credentials"},
    Scopes: []string{"api:read"},
  }),
  op.WithScope(op.PublicScope("api:read", "Read API resources")),
)

See examples/05-client-credentials and Use case: client_credentials.

go
st := inmem.New()
handler, _ := op.New(
  op.WithIssuer("https://op.example.com"),
  op.WithStore(st),
  op.WithKeyset(myKeyset),
  op.WithCookieKeys(cookieKey),
  op.WithLoginFlow(op.LoginFlow{
    Primary: op.PrimaryPassword{Store: st.UserPasswords()},
  }),
  op.WithUI(op.SPAUI{
    LoginMount: "/login",
    StaticDir:  "../internal/webui/static",
  }),
)

UI ownership options

op.WithUI takes one of three modes: op.SPAUI (OP-mounted SPA shell), op.HTMLUI (the built-in pages with your own login, consent or account chooser templates), or op.DriverUI (your own interaction.Driver). interaction.JSONDriver is still the lower-level route when you want your own router to serve the shell. See examples/10-react-login, Use case: SPA, and Custom consent UI.

5. Persist on a real database, split hot from cold ​

go
import (
  "context"

  "github.com/libraz/go-oidc-provider/op"
  "github.com/libraz/go-oidc-provider/op/storeadapter/composite"
  oidcredis "github.com/libraz/go-oidc-provider/op/storeadapter/redis"
  oidcsql "github.com/libraz/go-oidc-provider/op/storeadapter/sql"
)

durable, _  := oidcsql.New(db, oidcsql.MySQL())
volatile, _ := oidcredis.New(context.Background(),
  oidcredis.WithDSN("rediss://redis:6380/0"),
  oidcredis.WithRedisAuth(redisUser, redisPassword),
)
combined, _ := composite.New(
  composite.WithDefault(durable), // everything else, consumed-JTI markers included
  composite.With(composite.Sessions, volatile),
  composite.With(composite.Interactions, volatile),
)

loginFlow := op.LoginFlow{Primary: op.PrimaryPassword{Store: durable.UserPasswords()}}
handler, _ := op.New(
  op.WithIssuer("https://op.example.com"),
  op.WithStore(combined),
  op.WithKeyset(myKeyset),
  op.WithCookieKeys(cookieKey),
  op.WithLoginFlow(loginFlow),
)

See examples/09-redis-volatile and Use case: hot/cold split.


Where to start ​

You areStart with
New to OAuth 2.0 and OIDCOAuth 2.0 / OIDC primer, then Minimal OP
Ready to run the libraryInstall, Required options, Mount on your router
Deciding whether to adopt itWhy go-oidc-provider, Security posture, FAQ
Building something specificUse cases

Install ​

sh
go get github.com/libraz/[email protected]

Upgrading

v1.5.0 changes the stable surface.

  • An OP without op.WithProfile requires PKCE. op.WithProfile(profile.OIDCCore) admits clients that cannot send it.
  • The user store is passed through the login flow or op.WithUserStore.
  • The interaction UI is chosen through one op.WithUI.
  • Options that did the same job under several names are folded into one.
  • op.New refuses configurations that would start but not be safe, such as a static client secret shorter than 32 bytes.
  • The login and interaction surface (op.LoginFlow, op.WithUI, op/interaction) is covered by the compatibility promise from this release on.

The checklist at the top of the v1.5.0 entry in CHANGELOG.md maps each old name to its replacement. Symbols marked Experimental: in godoc may change in any minor release.

Apache-2.0. Source: libraz/go-oidc-provider. Vulnerability disclosure: SECURITY.md.