{"record":{"id":"9d5e8d1d60a42c4d","repo":"siyuan-note/siyuan","slug":"oidc-login-binding-does-not-match","errorCode":null,"errorMessage":"OIDC login binding does not match","messagePattern":"OIDC login binding does not match","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/oidc.go","lineNumber":755,"sourceCode":"\tif state == \"\" {\n\t\treturn nil, false, errors.New(\"OIDC state is missing\")\n\t}\n\toidcTransactions.Lock()\n\tcleanupOIDCTransactionsLocked()\n\ttransaction := oidcTransactions.byState[state]\n\tif transaction == nil {\n\t\toidcTransactions.Unlock()\n\t\treturn nil, false, errors.New(\"OIDC login transaction was not found or has expired\")\n\t}\n\tif transaction.ConfigVersion != oidcConfigurationVersion(Conf.GetOIDC()) {\n\t\tdeleteOIDCTransactionLocked(state)\n\t\toidcTransactions.Unlock()\n\t\treturn nil, false, errors.New(\"OIDC configuration changed during login\")\n\t}\n\tif !(allowDesktopWithoutBinding && (transaction.Flow == oidcFlowDesktop || transaction.Flow == oidcFlowValidate)) &&\n\t\t(binding == \"\" || binding != transaction.Binding) {\n\t\toidcTransactions.Unlock()\n\t\treturn nil, false, errors.New(\"OIDC login binding does not match\")\n\t}\n\tif !transaction.Claimed {\n\t\ttransaction.Claimed = true\n\t\tcopy := *transaction\n\t\toidcTransactions.Unlock()\n\t\treturn &copy, false, nil\n\t}\n\tdone := transaction.Done\n\toidcTransactions.Unlock()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, false, fmt.Errorf(\"wait for OIDC login transaction failed: %w\", ctx.Err())\n\tcase <-done:\n\t}\n\n\toidcTransactions.Lock()\n\tdefer oidcTransactions.Unlock()","sourceCodeStart":737,"sourceCodeEnd":773,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/oidc.go#L737-L773","documentation":"The binding supplied at claim differs from the binding stored on the transaction, and the desktop/validate-without-binding exemption does not apply. The binding ties a state to the session/device that started it, so a mismatch blocks session hijack or replay of a state into another session.","triggerScenarios":"Poll or callback where the binding cookie was cleared or rotated between start and claim, or a different session attempts to consume a state.","commonSituations":"User cleared cookies mid-login; browser split across profiles/containers; cross-tab interference; an attempt to replay a captured state in another session.","solutions":["Restart the flow in a single browser profile and keep cookies stable through the whole flow.","Avoid clearing cookies between start and callback.","Confirm the client sends the same binding from start through poll/callback."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Confirm the binding that will be presented at claim matches the one captured at start.\nif startBinding != expectedBinding {\n    return errors.New(\"binding mismatch - restart the flow in one session\")\n}","typeGuard":null,"tryCatchPattern":"// On binding mismatch, restart the flow instead of retrying with a different session.\nif err != nil && strings.Contains(err.Error(), \"binding does not match\") {\n    restartOIDCFlow(c)\n}","preventionTips":["Keep cookies stable from start through callback (no clearing/rotating mid-flow).","Run the entire flow in one browser profile/container.","Do not share state URLs across sessions."],"tags":["oidc","authentication","session","security"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}