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

  1. Read the wrapped cause: go-oauth2 errors typically state 'invalid_grant', 401, or connection problems — fix the matching cause.
  2. Ensure the redirect URL passed to New is byte-identical to the callback registered with the IdP and used in the authorization request.
  3. Re-run the sign-in flow with a fresh authorization code; codes are single-use and short-lived, do not retry the same code.
  4. 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.
  5. 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

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


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)