OpenID Connect
Discover providers and verify OpenID Connect ID tokens.
OpenID Connect
The oidc package discovers an OpenID Connect provider, verifies signed
tokens, and provides middleware for protected routes.
import "github.com/Dziqha/ply/oidc"
provider, err := oidc.NewProvider(ctx, oidc.ProviderConfig{
Issuer: "https://accounts.example.com",
ClientID: os.Getenv("OIDC_CLIENT_ID"),
ClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
Scopes: []string{"openid", "profile", "email"},
})
if err != nil {
log.Fatal(err)
}NewProvider fetches /.well-known/openid-configuration and validates that
the returned issuer matches the configured issuer. The provider keeps the
authorization, token, userinfo, JWKS, and end-session endpoints.
Login and callback
The provider returns an OAuth2 configuration with the discovered endpoints:
state, _ := oauth2.NewState()
verifier, _ := oauth2.NewVerifier()
nonce, _ := oauth2.NewNonce()
manager.Set(c, "oidc_state", state)
manager.Set(c, "oidc_verifier", verifier)
manager.Set(c, "oidc_nonce", nonce)
c.Redirect(302, provider.AuthCodeURL(state, nonce, verifier, "https://app.example.com/auth/callback"))On the callback, validate the state, exchange the code, and verify the ID token:
expectedState, _ := manager.Get(c, "oidc_state").(string)
if !oauth2.ValidateState(expectedState, c.Query("state")) {
c.Text(400, "invalid state")
return
}
verifier, _ := manager.Get(c, "oidc_verifier").(string)
token, err := provider.Exchange(c.Ctx, c.Query("code"), verifier, "https://app.example.com/auth/callback")
if err != nil {
c.Text(401, "token exchange failed")
return
}
claims, err := provider.Verifier().Verify(c.Ctx, token.IDToken)
if err != nil {
c.Text(401, "invalid id token")
return
}
nonce, _ := manager.Get(c, "oidc_nonce").(string)
if !oidc.ValidateNonce(claims, nonce) {
c.Text(401, "invalid nonce")
return
}AuthCodeURL adds the nonce to the authorization request. The nonce check
binds the ID token to the login attempt that started the flow.
Claims
Verify returns *oidc.Claims:
| Field | Meaning |
|---|---|
Subject | Stable user identifier |
Issuer | Token issuer |
Audience | Intended audiences |
Expiry | Expiration time |
IssuedAt | Issue time |
NotBefore | Earliest valid time |
Nonce | ID token nonce |
Email | Email claim, when present |
Name | Name claim, when present |
Raw | Full claim map |
The verifier requires exp, checks iss and aud, applies a one-minute
leeway, and validates azp when the token has multiple audiences.
Middleware
Use the verifier middleware when the application accepts signed JWT access tokens:
app.Use(provider.Verifier().Middleware())
app.Get("/me", func(c *core.Context) {
claims := oidc.ClaimsFrom(c)
c.JSON(200, map[string]string{"subject": claims.Subject})
})The middleware reads Authorization: Bearer <token>, stores the claims under
oidc.ClaimsKey, and returns 401 with a WWW-Authenticate challenge when
verification fails.
Standalone verifier
A service that already knows the issuer and JWKS URL can skip discovery:
verifier, err := oidc.NewVerifier(oidc.VerifierConfig{
Issuer: "https://accounts.example.com",
Audience: "my-api",
JWKSURL: "https://accounts.example.com/.well-known/jwks.json",
})Supported signing algorithms are RS256, RS384, RS512, PS256, PS384,
PS512, ES256, ES384, ES512, and EdDSA.
Client authentication
For confidential clients, set ClientSecret. The default token request sends
the client credentials in the form body. Use AuthStyleBasic when the provider
requires HTTP Basic authentication:
oidc.ProviderConfig{
// ...
ClientSecret: os.Getenv("OIDC_CLIENT_SECRET"),
AuthStyle: oauth2.AuthStyleBasic,
}UserInfo
var profile struct {
Subject string `json:"sub"`
Email string `json:"email"`
}
if err := provider.UserInfo(c.Ctx, token, &profile); err != nil {
c.Text(401, "userinfo failed")
return
}JWKS caching
Signing keys are cached for five minutes by default. The verifier refreshes
the key set when the cache expires or when a token references an unknown key.
MinRefresh limits how often an unknown key can trigger a fetch; the default
is 10 seconds.
oidc.ProviderConfig{
// ...
CacheTTL: 15 * time.Minute,
MinRefresh: 30 * time.Second,
}The cache honors a Cache-Control: max-age header between 30 seconds and one
hour. A key rotation therefore does not require a restart.
Errors
OnError receives verification failures from the middleware:
provider, err := oidc.NewProvider(ctx, oidc.ProviderConfig{
// ...
OnError: func(err error) {
logger.Warn("oidc verification failed", "error", err)
},
})Standalone Verify calls return the error directly.
Security
- Use HTTPS for the issuer and JWKS URL.
- Verify the issuer exactly.
NewProviderrejects a discovery document with a different issuer. - Validate the state and nonce values on the callback.
- Treat the ID token as proof of authentication, not as a general API access token. Use the access token for API authorization.
- Check
email_verifiedbefore trusting an email address for account linking. - Keep the JWKS cache enabled. Disabling it makes every request fetch keys.

Ply