siyuan-note/siyuan · error
verify OIDC ID token failed
Error message
verify OIDC ID token failed: %w
What it means
Exchange verifies the returned ID token's signature, issuer, audience and expiry with the discovery-derived verifier; a failed verification is wrapped as "verify OIDC ID token failed". The token is therefore untrusted (bad signature, wrong issuer/audience, expired, or malformed) and sign-in is refused rather than accepting unverified claims.
Solutions
- Read the wrapped cause — go-oidc names the exact problem (expired, signature, issuer, audience).
- Synchronize the server clock (NTP) to eliminate expiry-related rejections.
- Make the configured IssuerURL exactly match the iss claim of the issued tokens (correct realm/tenant path).
- Confirm client_id/audience matches the app registered at the IdP and that the token's signing keys are served at the advertised jwks_uri and reachable from the kernel.
- Retry sign-in after IdP key rotation so a fresh JWKS is fetched.
Example fix
// before
cfg := &conf.OIDC{Provider: conf.OIDCProviderCustom, IssuerURL: "https://sso.example.com/realms/old"} // token iss = .../realms/new
provider, err := New(cfg, redirectURL)
// after
cfg := &conf.OIDC{Provider: conf.OIDCProviderCustom, IssuerURL: "https://sso.example.com/realms/new"} // matches token iss
provider, err := New(cfg, redirectURL) Defensive patterns
Strategy: try-catch
Validate before calling
// pre-check clock skew and issuer reachability before sign-in
if skew := time.Now().Sub(time.Now().UTC()); skew > 2*time.Minute || skew < -2*time.Minute {
return errors.New("server clock skew too large for JWT validation")
} Try / catch
claims, err := provider.Exchange(ctx, code, verifier, nonce)
if err != nil {
if strings.Contains(err.Error(), "verify OIDC ID token failed") {
// log the wrapped cause; alert on expired tokens (clock skew) vs signature/issuer mismatch
}
return err
} Prevention
- Run NTP on all hosts running the kernel
- Ensure the configured IssuerURL exactly equals the token's iss claim
- Keep client_id/audience aligned with the IdP app registration
- Allow JWKS refresh after IdP signing-key rotation
When it happens
Trigger: Calling Exchange when p.verifier.Verify(ctx, rawIDToken) fails: token signed by a key not in the JWKS, issuer mismatch between discovery URL and token iss, client_id not in aud, token expired (clock skew), or malformed/alg-mismatched token.
Common situations: Container clock skew making fresh tokens look expired; wrong issuer/realm configured so the token's iss differs from the discovered issuer; IdP rotated signing keys while the cached JWKS was stale; multi-tenant endpoints (e.g. Microsoft) issuing tenant-specific issuers; audience mismatch after changing the registered app ID.
Related errors
- decode OIDC claims failed
- OIDC issuer URL must use HTTPS unless it is a loopback…
- OIDC nonce does not match
- A loopback OIDC redirect URL is required for local access
- A public HTTPS OIDC redirect URL is required for remote…
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/b4ae1cff867d2af7.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/oidc_provider/provider.go:105
}
return p.oauth2Config.AuthCodeURL(state, oidc.Nonce(nonce), oauth2.S256ChallengeOption(codeVerifier))
}
func (p *Provider) Exchange(ctx context.Context, code, codeVerifier, nonce string) (map[string]any, error) {
token, err := p.oauth2Config.Exchange(ctx, code, oauth2.VerifierOption(codeVerifier))
if err != nil {
return nil, fmt.Errorf("exchange OIDC authorization code failed: %w", err)
}
if p.kind == conf.OIDCProviderGitHub {
return exchangeGitHubClaims(ctx, token)
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok || rawIDToken == "" {
return nil, errors.New("OIDC response does not contain an ID token")
}
idToken, err := p.verifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, fmt.Errorf("verify OIDC ID token failed: %w", err)
}
if idToken.Nonce != nonce {
return nil, errors.New("OIDC nonce does not match")
}
claims := map[string]any{}
if err = idToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("decode OIDC claims failed: %w", err)
}
return claims, nil
}
func newGitHub(config *conf.OIDC, redirectURL string) *Provider {
scopes := append([]string{}, config.Scopes...)
if len(scopes) == 0 || isDefaultOIDCScopes(scopes) {
scopes = []string{"read:user", "user:email"}
} else {
filtered := scopes[:0]
for _, scope := range scopes {View on GitHub (pinned to 9f775e8a12)