{"record":{"id":"72b01c018392fc31","repo":"siyuan-note/siyuan","slug":"oidc-nonce-does-not-match","errorCode":null,"errorMessage":"OIDC nonce does not match","messagePattern":"OIDC nonce does not match","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/oidc_provider/provider.go","lineNumber":108,"sourceCode":"\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)\n\t}\n\treturn claims, nil\n}\n\nfunc newGitHub(config *conf.OIDC, redirectURL string) *Provider {\n\tscopes := append([]string{}, config.Scopes...)\n\tif len(scopes) == 0 || isDefaultOIDCScopes(scopes) {\n\t\tscopes = []string{\"read:user\", \"user:email\"}\n\t} else {\n\t\tfiltered := scopes[:0]\n\t\tfor _, scope := range scopes {\n\t\t\tif scope != oidc.ScopeOpenID && scope != \"profile\" && scope != \"email\" {\n\t\t\t\tfiltered = append(filtered, scope)\n\t\t\t}","sourceCodeStart":90,"sourceCodeEnd":126,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/oidc_provider/provider.go#L90-L126","documentation":"Exchange compares the nonce embedded in the verified ID token with the nonce that was originally bound to the authorization request (via AuthCodeURL's oidc.Nonce option). A mismatch means the returned token was not minted in response to this client's request — a replay/CSRF-style protection — so the sign-in is aborted. Typically indicates mixed-up or replayed state across concurrent sign-in sessions.","triggerScenarios":"Calling Exchange with a nonce value different from the one used when generating the authorization URL: the caller regenerated AuthCodeURL with a new nonce but validated against an old stored nonce; two browser tabs overwrote each other's pending sign-in state; the state/nonce store returned the wrong entry.","commonSituations":"Users opening the sign-in flow in two tabs and completing the older one; server-side session/state storage keyed incorrectly (per-instance vs shared cache); a restart clearing in-memory pending state; proxies caching the authorization redirect.","solutions":["Restart the sign-in flow from the beginning so a fresh nonce is generated, bound to the authorization URL, and stored with the pending state.","Ensure the nonce stored at authorization-start is the exact value passed to Exchange (same session key, no overwrites).","Make pending sign-in state (state + nonce + verifier) shared and consistent if the kernel runs multiple instances (e.g. store in the DB/cache, not process memory).","Advise users not to run concurrent sign-ins in multiple tabs, or key pending state per browser tab/session id."],"exampleFix":"// before\nnonce := randomToken()\nurl := provider.AuthCodeURL(state, oidc.Nonce(newRandomNonce())) // nonce mismatch: different value stored\nsessions[state] = nonce\n// after\nnonce := randomToken()\nurl := provider.AuthCodeURL(state, oidc.Nonce(nonce)) // bind the SAME nonce\nsessions[state] = nonce","handlingStrategy":"validation","validationCode":"stored, ok := pendingSessions[state]\nif !ok || stored.Nonce == \"\" {\n    http.Error(w, \"unknown or expired sign-in state, restart\", http.StatusBadRequest)\n    return\n}","typeGuard":null,"tryCatchPattern":"claims, err := provider.Exchange(ctx, code, verifier, nonce)\nif err != nil {\n    if err.Error() == \"OIDC nonce does not match\" {\n        // invalidate the pending session and redirect the user to restart sign-in\n    }\n    return err\n}","preventionTips":["Bind one nonce per sign-in attempt and store it with the same state key used at Exchange time","Expire and clear pending sessions on kernel restart or use persistent storage","Guard against concurrent sign-ins overwriting pending state (per-session keys)"],"tags":["oidc","nonce","security","replay-protection"],"backgroundTag":"oauth-nonce-mismatch","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"}