{"record":{"id":"08314a862fff5590","repo":"siyuan-note/siyuan","slug":"exchange-oidc-authorization-code-failed-w","errorCode":null,"errorMessage":"exchange OIDC authorization code failed: %w","messagePattern":"exchange OIDC authorization code failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/oidc_provider/provider.go","lineNumber":94,"sourceCode":"\t\t\tEndpoint:     discovered.Endpoint(),\n\t\t\tRedirectURL:  redirectURL,\n\t\t\tScopes:       scopes,\n\t\t},\n\t\tverifier: discovered.Verifier(&oidc.Config{ClientID: config.ClientID}),\n\t}, nil\n}\n\nfunc (p *Provider) AuthURL(state, nonce, codeVerifier string) string {\n\tif p.kind == conf.OIDCProviderGitHub {\n\t\treturn p.oauth2Config.AuthCodeURL(state, oauth2.S256ChallengeOption(codeVerifier))\n\t}\n\treturn p.oauth2Config.AuthCodeURL(state, oidc.Nonce(nonce), oauth2.S256ChallengeOption(codeVerifier))\n}\n\nfunc (p *Provider) Exchange(ctx context.Context, code, codeVerifier, nonce string) (map[string]any, error) {\n\ttoken, err := p.oauth2Config.Exchange(ctx, code, oauth2.VerifierOption(codeVerifier))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"exchange OIDC authorization code failed: %w\", err)\n\t}\n\tif p.kind == conf.OIDCProviderGitHub {\n\t\treturn exchangeGitHubClaims(ctx, token)\n\t}\n\trawIDToken, ok := token.Extra(\"id_token\").(string)\n\tif !ok || rawIDToken == \"\" {\n\t\treturn nil, errors.New(\"OIDC response does not contain an ID token\")\n\t}\n\tidToken, err := p.verifier.Verify(ctx, rawIDToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"verify OIDC ID token failed: %w\", err)\n\t}\n\tif idToken.Nonce != nonce {\n\t\treturn nil, errors.New(\"OIDC nonce does not match\")\n\t}\n\tclaims := map[string]any{}\n\tif err = idToken.Claims(&claims); err != nil {\n\t\treturn nil, fmt.Errorf(\"decode OIDC claims failed: %w\", err)","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/oidc_provider/provider.go#L76-L112","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the code_verifier passed to Exchange is the exact same one used to build the AuthURL (persist it server-side across the redirect).","Check that the redirect URL is identical in both the authorization and exchange steps.","Verify the client secret in SiYuan config matches what the provider has on record.","Inspect the wrapped error for the provider's specific rejection reason (e.g., 'invalid_grant', 'bad_verification_code').","If codes are expiring, reduce the latency between user consent and the callback handling."],"exampleFix":"// before\nclaims, err := provider.Exchange(ctx, code, codeVerifier, nonce)\n\n// after\n// Ensure codeVerifier is the same one used in AuthURL()\nstoredVerifier := session.Get(\"oidc_code_verifier\").(string)\nclaims, err := provider.Exchange(ctx, code, storedVerifier, nonce)\nif err != nil {\n    log.Printf(\"token exchange failed (check code expiry, PKCE match, redirect URL): %v\", err)\n    return\n}","handlingStrategy":"try-catch","validationCode":"// Pre-validate: ensure code and verifier are non-empty\nif code == \"\" || codeVerifier == \"\" {\n    return nil, errors.New(\"authorization code and PKCE verifier are required\")\n}","typeGuard":null,"tryCatchPattern":"claims, err := provider.Exchange(ctx, code, codeVerifier, nonce)\nif err != nil {\n    if strings.Contains(err.Error(), \"exchange OIDC authorization code\") {\n        // Could be expired code, PKCE mismatch, or network error\n        // Redirect user to re-authenticate rather than retrying the stale code\n        http.Redirect(w, r, \"/api/oidc/login\", http.StatusTemporaryRedirect)\n        return\n    }\n}","preventionTips":["Persist the PKCE code_verifier and nonce in a session that survives the OAuth redirect.","Ensure the redirect URL is identical in both authorization and exchange steps.","Handle the callback promptly to avoid authorization code expiry (typically 10 minutes).","Log the wrapped error to identify the specific provider rejection reason."],"tags":["oidc","authentication","oauth2","pkce","network","runtime"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}