siyuan-note/siyuan · error

exchange OIDC authorization code failed: %w

Error message

exchange OIDC authorization code failed: %w

What it means

Thrown by Provider.Exchange() when the OAuth2 token exchange fails. After the user is redirected back with an authorization code, Exchange calls the provider's token endpoint with the code plus PKCE verifier. Any failure (invalid code, expired code, wrong redirect URI, wrong client secret, network error) is wrapped with %w.

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 251596fc0d)

Solutions

  1. Ensure the code_verifier passed to Exchange is the exact same one used to build the AuthURL (persist it server-side across the redirect).
  2. Check that the redirect URL is identical in both the authorization and exchange steps.
  3. Verify the client secret in SiYuan config matches what the provider has on record.
  4. Inspect the wrapped error for the provider's specific rejection reason (e.g., 'invalid_grant', 'bad_verification_code').
  5. If codes are expiring, reduce the latency between user consent and the callback handling.

Example fix

// before
claims, err := provider.Exchange(ctx, code, codeVerifier, nonce)

// after
// Ensure codeVerifier is the same one used in AuthURL()
storedVerifier := session.Get("oidc_code_verifier").(string)
claims, err := provider.Exchange(ctx, code, storedVerifier, nonce)
if err != nil {
    log.Printf("token exchange failed (check code expiry, PKCE match, redirect URL): %v", err)
    return
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-validate: ensure code and verifier are non-empty
if code == "" || codeVerifier == "" {
    return nil, errors.New("authorization code and PKCE verifier are required")
}

Try / catch

claims, err := provider.Exchange(ctx, code, codeVerifier, nonce)
if err != nil {
    if strings.Contains(err.Error(), "exchange OIDC authorization code") {
        // Could be expired code, PKCE mismatch, or network error
        // Redirect user to re-authenticate rather than retrying the stale code
        http.Redirect(w, r, "/api/oidc/login", http.StatusTemporaryRedirect)
        return
    }
}

Prevention

When it happens

Trigger: Calling Exchange(ctx, code, codeVerifier, nonce) where: the authorization code is expired or already-used, the code_verifier does not match the code_challenge sent during AuthURL, the redirect URL differs from the one used in the authorization request, the client secret is wrong, or the network call to the token endpoint fails.

Common situations: The user waited too long between consent and the callback (code expiry, typically 10 min). The PKCE verifier was not persisted across the redirect (e.g., stored in a session that expired). The redirect URL changed between auth and exchange (e.g., different port after restart). The client secret was rotated on the provider side but not updated in SiYuan config. Clock skew causing token endpoint to reject the request.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/08314a862fff5590. Report an issue: GitHub.