siyuan-note/siyuan · error
OIDC nonce does not match
Error message
OIDC nonce does not match
What it means
Thrown by Provider.Exchange() when idToken.Nonce does not equal the nonce argument. The nonce is a single-use random string sent in the authorization request and embedded in the id_token to prevent replay/token-injection attacks. A mismatch indicates the token was not issued for this specific login attempt.
Source
Thrown at kernel/model/oidc_provider/provider.go:108
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 {
if scope != oidc.ScopeOpenID && scope != "profile" && scope != "email" {
filtered = append(filtered, scope)
}View on GitHub (pinned to 251596fc0d)
Solutions
- Ensure the nonce generated in AuthURL is persisted (session, signed cookie, or database) and correctly retrieved in the callback handler.
- Verify the nonce passed to Exchange is the exact same value passed to AuthURL for this login session.
- Use a per-session nonce store that survives the redirect (e.g., signed HTTP-only cookie rather than in-memory map).
- If the server restarts mid-login, the user must re-initiate the OIDC flow to get a new nonce.
Example fix
// before
nonce := generateNonce()
authURL := provider.AuthURL(state, nonce, codeVerifier)
// ... redirect user ...
// On callback:
claims, err := provider.Exchange(ctx, code, codeVerifier, generateNonce()) // fresh nonce!
// after
nonce := generateNonce()
session.Set("oidc_nonce", nonce)
session.Save()
authURL := provider.AuthURL(state, nonce, codeVerifier)
// ... redirect user ...
// On callback:
storedNonce := session.Get("oidc_nonce").(string)
claims, err := provider.Exchange(ctx, code, codeVerifier, storedNonce) Defensive patterns
Strategy: validation
Validate before calling
// Ensure nonce is persisted and retrieved correctly
if nonce == "" {
return errors.New("nonce is empty; cannot verify OIDC response")
}
// Compare with stored nonce from the auth request
storedNonce := session.Get("oidc_nonce")
if storedNonce != nonce {
return errors.New("nonce mismatch detected before exchange")
} Type guard
func isValidNonce(stored, received string) bool {
return stored != "" && received != "" && stored == received
} Try / catch
claims, err := provider.Exchange(ctx, code, codeVerifier, nonce)
if err != nil && strings.Contains(err.Error(), "nonce does not match") {
// Session lost the nonce or potential replay — force re-authentication
session.Delete("oidc_nonce")
http.Redirect(w, r, "/api/oidc/login", http.StatusTemporaryRedirect)
return
} Prevention
- Store the nonce in a signed HTTP-only cookie that survives the redirect.
- Never generate a new nonce in the callback handler; always use the one from the auth request.
- Clear the nonce after successful exchange to prevent reuse.
- Use per-session nonce storage, not a global map.
When it happens
Trigger: Calling Exchange(ctx, code, codeVerifier, nonce) where the nonce extracted from the verified id_token differs from the nonce passed in. This occurs when: the nonce was not persisted correctly between AuthURL generation and the callback, a different nonce was used in AuthURL vs Exchange, or an attacker attempted a token injection/replay.
Common situations: The nonce was generated for AuthURL but stored in a session/cookie that expired or was cleared before the callback, so a fresh/empty nonce is compared. The server restarted between generating the auth URL and handling the callback, losing the in-memory nonce. Multiple concurrent login flows mixed up nonces. A genuine replay attack attempt (rare but this is the security guard catching it).
Related errors
- OIDC login binding does not match
- Save OIDC login session failed
- OIDC issuer URL must use HTTPS unless it is a loopback addre
- OIDC login requires at least one claim rule when Allow all u
- remote access requires at least one authentication method
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/72b01c018392fc31.
Report an issue: GitHub.