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

  1. Read the wrapped cause — go-oidc names the exact problem (expired, signature, issuer, audience).
  2. Synchronize the server clock (NTP) to eliminate expiry-related rejections.
  3. Make the configured IssuerURL exactly match the iss claim of the issued tokens (correct realm/tenant path).
  4. 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.
  5. 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

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


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)