LogoPly
Features

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:

FieldMeaning
SubjectStable user identifier
IssuerToken issuer
AudienceIntended audiences
ExpiryExpiration time
IssuedAtIssue time
NotBeforeEarliest valid time
NonceID token nonce
EmailEmail claim, when present
NameName claim, when present
RawFull 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. NewProvider rejects 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_verified before trusting an email address for account linking.
  • Keep the JWKS cache enabled. Disabling it makes every request fetch keys.