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
op.New(...) returns http.Handler — net/http, chi, ginSAFETYop.New refuses unsafe or conflicting configuration — short client secrets, missing tables, repeated optionsPROFILEop.WithProfile(profile.FAPI2Baseline) — PAR · PKCE · DPoP at one switchLOGINop.WithLoginFlow — password, passkey, TOTP, email OTP, recovery codes and step-up rules in one structUIop.WithUI(HTMLUI | SPAUI | DriverUI) — the built-in pages, your SPA, or your own driverGRANTSDevice code · CIBA · token exchange · JWT bearer — each one op.With<Name>Grant(…)STORAGEBundled full stores — inmem · SQL · DynamoDB; Redis is a volatile tier through compositeTOKENSRefresh rotation and reuse detection outside FAPI 2.0, offline_access TTL bucketCONFORMANCEOFCS 201 PASSED / 259 MODULES · 9 plans · 0 strict release blockersWithLogger and WithPrometheus.Five typical configurations. Each one links to a runnable example in the upstream repository.
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-minimaland Quick Start.
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.
feature.MTLS.private_key_jwt or mTLS, and a client assertion's aud is the issuer.Signed request objects are accepted but not required; profile.FAPI2MessageSigning requires them. See Use case: FAPI 2.0 Baseline.
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-credentialsand Use case: client_credentials.
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.
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-volatileand Use case: hot/cold split.
| You are | Start with |
|---|---|
| New to OAuth 2.0 and OIDC | OAuth 2.0 / OIDC primer, then Minimal OP |
| Ready to run the library | Install, Required options, Mount on your router |
| Deciding whether to adopt it | Why go-oidc-provider, Security posture, FAQ |
| Building something specific | Use cases |
go get github.com/libraz/[email protected]Upgrading
v1.5.0 changes the stable surface.
op.WithProfile requires PKCE. op.WithProfile(profile.OIDCCore) admits clients that cannot send it.op.WithUserStore.op.WithUI.op.New refuses configurations that would start but not be safe, such as a static client secret shorter than 32 bytes.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.