{"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/9f775e8a12daef8255556097396f9b2739078892/kernel/model/oidc_provider/provider.go#L76-L112","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nclaims, err := provider.Exchange(ctx, r.URL.Query().Get(\"code\"), storedVerifier, storedNonce)\n// reused/expired code causes failure; always consume a fresh callback once\nif r.URL.Query().Get(\"code\") != storedPendingCode {\n    http.Error(w, \"stale authorization code, restart sign-in\", http.StatusBadRequest)\n    return\n}\nclaims, err := provider.Exchange(ctx, r.URL.Query().Get(\"code\"), storedVerifier, storedNonce)","handlingStrategy":"try-catch","validationCode":"if r.URL.Query().Get(\"code\") == \"\" || r.URL.Query().Get(\"state\") != expectedState {\n    http.Error(w, \"invalid callback parameters, restart sign-in\", http.StatusBadRequest)\n    return\n}","typeGuard":null,"tryCatchPattern":"claims, err := provider.Exchange(ctx, code, verifier, nonce)\nif err != nil {\n    if strings.Contains(err.Error(), \"exchange OIDC authorization code failed\") {\n        // do NOT retry with the same code; redirect the user to restart sign-in\n    }\n    return err\n}","preventionTips":["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"],"tags":["oidc","oauth2","token-exchange","network"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}