Overview
Go's OAuth2 story is two small official-adjacent libraries plus composition:
golang.org/x/oauth2 implements the client half of the flows, and coreos/go-oidc layers
OIDC discovery and ID-token verification on top. There is no resource-server "starter" —
you write (or import) a middleware that validates bearer JWTs against the provider's JWKS.
The provider side is either delegated to an external IdP (Keycloak, or the Go-native
Zitadel/Ory Hydra/Dex) or embedded via Ory Fosite. Protocol semantics live in
Authentication & authorization; this note
is the Go wiring.
Key points
golang.org/x/oauth2:oauth2.Config(client id/secret, endpoint, scopes, redirect),AuthCodeURL/Exchangefor the auth-code flow,TokenSourcefor automatic refresh,Config.Clientreturning an*http.Clientthat injects and refreshes tokens.clientcredentials.Config(x/oauth2/clientcredentials) is the service-to-service flow — machine identity, no user, the workhorse of internal APIs.coreos/go-oidc(github.com/coreos/go-oidc/v3/oidc):oidc.NewProviderperforms discovery from the issuer URL, caches the JWKS, andprovider.Verifiervalidates ID tokens (signature,iss,aud,exp) — the correct default for verification.- JWT libraries:
golang-jwt/jwtv5 (the maintained fork of dgrijalva/jwt-go) for general JWT work;lestrrat-go/jwxfor full JOSE (JWKS, JWE, JWS). Both grew strict-by- default APIs after the algorithm-confusion era — always pin expected algorithms, never trust the token'salgheader. - Resource-server middleware is DIY-but-small: a
func(http.Handler) http.Handlerthat extracts the bearer token, verifies it, and stashes claims in the request context; gin/echo versions are the same logic behind their middleware signatures. - Flows that matter: authorization code + PKCE (
oauth2.S256ChallengeOptionandoauth2.VerifierOptionare built in) for anything user-facing, client credentials for services. Implicit and password grants are dead (OAuth 2.1 removes them) — don't build them. - Providers in Go: delegate to Keycloak like everyone else, or stay in-ecosystem — Dex (federating OIDC proxy, CNCF), Zitadel (full IdP), Ory Hydra (headless OAuth2 server) with Ory Fosite as the embeddable SDK Hydra itself is built on.
- Token propagation: pass the inbound bearer token (or exchange it, RFC 8693) on
outgoing calls via
oauth2.NewClient+ a static/reuseTokenSource; inside a mesh, mTLS often carries workload identity instead.
Details
Client side: x/oauth2 in practice
oauth2.Config covers the redirect dance; the subtle part is token lifecycle.
Config.TokenSource(ctx, tok) wraps a token with auto-refresh (ReuseTokenSource), and
oauth2.NewClient builds an *http.Client whose transport injects Authorization: Bearer. Pitfalls worth remembering: the package silently ignores refresh failures until a
request fails; the ctx passed at creation controls the refresh requests' HTTP client
(via oauth2.HTTPClient context key), which matters for proxies and test doubles.
Provider endpoint presets live in subpackages (x/oauth2/google, github, etc.), but for
any OIDC-compliant IdP, go-oidc's discovered provider.Endpoint() is the better source.
Resource server: verification middleware
The canonical shape, framework-independent:
- Extract
Authorization: Bearer(reject anything else). - Verify with a cached verifier:
go-oidc'sVerifierfor ID-token-shaped JWTs, orjwx'sjwk.Cache+jwt.Parsewith pinned algorithms for plain access tokens. - Check audience/scope claims for the route; put the claims struct into
context.
For gin the same function is wrapped as gin.HandlerFunc calling c.Abort() on failure;
echo and chi are analogous. Multi-tenant APIs cache one verifier per issuer. If the IdP
issues opaque tokens, verification becomes an introspection call (RFC 7662) — cache the
result briefly.
Provider side: embed or delegate
| Option | What it is | When |
|---|---|---|
| Keycloak | External Java IdP, the default everywhere | You just need an IdP; ops-friendly, huge feature set |
| Dex | Go OIDC provider that federates upstream connectors (LDAP, GitHub, SAML) | Kubernetes-adjacent, need OIDC in front of existing identity |
| Zitadel | Go-native full IdP (multi-tenant, passkeys) | Want a modern self-hosted IdP in the ecosystem |
| Ory Hydra | Headless certified OAuth2/OIDC server; you supply login UI | Need certified flows but own the UX |
| Ory Fosite | Library: handlers for each grant, storage interfaces | Embedding an authorization server inside your own binary |
Embedding via Fosite is real work (storage, consent, key rotation are yours); default to delegating unless the product is identity.
Testing
- Mock OIDC servers:
oauth2-mock-style servers or the lightweightmockoidcpackage stand up a discovery endpoint + JWKS and mint arbitrary tokens — ideal for middleware unit tests. - Testcontainers: the Keycloak module for testcontainers-go runs the real IdP in integration tests; Dex is light enough to run as a plain container too.
- Static keys: for pure JWT middleware tests, generate an ed25519/RSA key in the test,
serve its JWKS from
httptest.NewServer, sign tokens withjwx— no network, no mocks.
Examples
// Client credentials: service-to-service token source.
cc := clientcredentials.Config{
ClientID: os.Getenv("CLIENT_ID"),
ClientSecret: os.Getenv("CLIENT_SECRET"),
TokenURL: "https://idp.example.com/oauth2/token",
Scopes: []string{"inventory:read"},
}
client := cc.Client(ctx) // *http.Client, auto-refreshing
// Resource server: go-oidc verification middleware.
provider, _ := oidc.NewProvider(ctx, "https://idp.example.com")
verifier := provider.Verifier(&oidc.Config{ClientID: "inventory-api"})
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
raw, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
if !ok {
http.Error(w, "missing bearer token", http.StatusUnauthorized)
return
}
idToken, err := verifier.Verify(r.Context(), raw)
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r.WithContext(withClaims(r.Context(), idToken)))
})
}
Related
- Security in Go applications — the map — parent map of concern → library.
- Authentication & authorization — the protocol semantics behind these flows.
- Web security in Go apps — sessions, CSRF and the rest of the perimeter.
- Integration testing — testcontainers patterns used for Keycloak/Dex here.
- OAuth2 & OIDC in Java — the same protocols via Spring/Quarkus starters instead of hand-rolled middleware.