siyuan-note/siyuan · error
exchange OIDC authorization code failed
Error message
exchange OIDC authorization code failed: %w
What it means
Provider.Exchange performs the OAuth2 authorization-code token exchange via oauth2Config.Exchange; any failure (rejected code, redirect_uri mismatch, bad client secret, network error) is wrapped as "exchange OIDC authorization code failed" with the provider's underlying error. Sign-in cannot complete because the one-time code could not be converted into tokens.
Solutions
- Read the wrapped cause: go-oauth2 errors typically state 'invalid_grant', 401, or connection problems — fix the matching cause.
- Ensure the redirect URL passed to New is byte-identical to the callback registered with the IdP and used in the authorization request.
- Re-run the sign-in flow with a fresh authorization code; codes are single-use and short-lived, do not retry the same code.
- Verify the client ID/secret and, for GitHub, that the secret matches the OAuth app; verify the PKCE verifier is the one bound to the stored state.
- Check network/proxy reachability of the token endpoint from the kernel host.
Example fix
// before
claims, err := provider.Exchange(ctx, r.URL.Query().Get("code"), storedVerifier, storedNonce)
// reused/expired code causes failure; always consume a fresh callback once
if r.URL.Query().Get("code") != storedPendingCode {
http.Error(w, "stale authorization code, restart sign-in", http.StatusBadRequest)
return
}
claims, err := provider.Exchange(ctx, r.URL.Query().Get("code"), storedVerifier, storedNonce) Defensive patterns
Strategy: try-catch
Validate before calling
if r.URL.Query().Get("code") == "" || r.URL.Query().Get("state") != expectedState {
http.Error(w, "invalid callback parameters, restart sign-in", http.StatusBadRequest)
return
} Try / catch
claims, err := provider.Exchange(ctx, code, verifier, nonce)
if err != nil {
if strings.Contains(err.Error(), "exchange OIDC authorization code failed") {
// do NOT retry with the same code; redirect the user to restart sign-in
}
return err
} Prevention
- Treat authorization codes as strictly single-use; never replay a callback
- Keep the redirect URL identical across authorization request, token request and IdP registration
- Store the pending state/verifier/nonce server-side and expire it after a few minutes
- Check the wrapped cause for invalid_grant to distinguish replay from misconfiguration
When it happens
Trigger: Calling Exchange(ctx, code, codeVerifier, nonce) when the token endpoint rejects the request: authorization code already used or expired, redirect URL not matching the one used in AuthCodeURL, wrong client secret, PKCE verifier mismatch, or the IdP unreachable.
Common situations: User double-submitting the callback (code replay); redirect URL registered on the OAuth app differing from the configured one; rotated client secret not yet updated in config; clock skew causing code expiry; load balancer sending callback to a different instance than the one that generated the state.
Related errors
- Desktop OIDC login requires a loopback listener
- discover OIDC provider failed
- GitHub OAuth client secret is required
- OIDC authorization code is missing
- OIDC redirect URL is required
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/08314a862fff5590.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/oidc_provider/provider.go:94
Endpoint: discovered.Endpoint(),
RedirectURL: redirectURL,
Scopes: scopes,
},
verifier: discovered.Verifier(&oidc.Config{ClientID: config.ClientID}),
}, nil
}
func (p *Provider) AuthURL(state, nonce, codeVerifier string) string {
if p.kind == conf.OIDCProviderGitHub {
return p.oauth2Config.AuthCodeURL(state, oauth2.S256ChallengeOption(codeVerifier))
}
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)View on GitHub (pinned to 9f775e8a12)