{"record":{"id":"2c17a69c7c682548","repo":"siyuan-note/siyuan","slug":"oidc-validation-transaction-was-not-found-or-has-e","errorCode":null,"errorMessage":"OIDC validation transaction was not found or has expired","messagePattern":"OIDC validation transaction was not found or has expired","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/oidc.go","lineNumber":774,"sourceCode":"\tstate := oidcTransactions.byPoll[pollToken]\n\ttransaction := oidcTransactions.byState[state]\n\tif transaction == nil || (transaction.Flow != oidcFlowDesktop && transaction.Flow != oidcFlowValidate) ||\n\t\tbinding == \"\" || binding != transaction.Binding {\n\t\treturn nil, false\n\t}\n\tcopy := *transaction\n\treturn &copy, true\n}\n\nfunc activateOIDCValidation(pollToken, binding string) (activated bool, err error) {\n\toidcTransactions.Lock()\n\tdefer oidcTransactions.Unlock()\n\tcleanupOIDCTransactionsLocked()\n\tstate := oidcTransactions.byPoll[pollToken]\n\ttransaction := oidcTransactions.byState[state]\n\tif transaction == nil || transaction.Flow != oidcFlowValidate || transaction.Binding == \"\" ||\n\t\tbinding == \"\" || transaction.Binding != binding || !transaction.Completed || !transaction.Success {\n\t\treturn false, errors.New(\"OIDC validation transaction was not found or has expired\")\n\t}\n\tif transaction.Activated {\n\t\treturn false, nil\n\t}\n\tif transaction.Config == nil {\n\t\treturn false, errors.New(\"OIDC validation configuration is missing\")\n\t}\n\tconfigurationChanged, swapped := Conf.CompareAndSetOIDC(transaction.ConfigVersion, transaction.Config)\n\tif !swapped {\n\t\tdeleteOIDCTransactionLocked(state)\n\t\treturn false, errors.New(\"OIDC configuration changed during validation\")\n\t}\n\ttransaction.Config = nil\n\ttransaction.Activated = true\n\treturn configurationChanged, nil\n}\n\nfunc cancelOIDCValidation(pollToken, binding string) bool {","sourceCodeStart":756,"sourceCodeEnd":792,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/oidc.go#L756-L792","documentation":"activateOIDCValidation looks up a pending OIDC validation flow by its state and verifies it is a validate-flow transaction with a matching binding that completed successfully. If no transaction exists for the state, or any precondition (flow type, binding match, completed, success) fails, it reports that the transaction was not found or expired. Transactions are short-lived by design and are purged by cleanupOIDCTransactionsLocked.","triggerScenarios":"Calling OIDCValidateActivate with a pollToken whose state no longer resolves (expired/purged transaction), a poll token from a non-validate flow, a mismatched binding value, or activating before token exchange completed/failed (transaction.Completed or transaction.Success false). Also raised by tests TestActivateOIDCValidationAppliesCandidateOnce, TestActivateOIDCValidationRejectsChangedConfiguration, TestCancelOIDCValidationPreventsActivation.","commonSituations":"User takes too long between scanning/polling and activation so the transaction TTL expires; server restart wipes in-memory oidcTransactions; frontend passes the wrong binding (e.g. stale browser tab); double-clicking activate after cancel already deleted the transaction.","solutions":["Restart the validation flow from OIDCValidateStart to obtain a fresh poll token/state, then retry activation","Verify the client sends the same binding that was used when the validation transaction was started","Check that token exchange (finishOIDCExchange) completed successfully before calling activate; inspect transaction.Completed/Success","Reduce the gap between poll success and activation or increase the transaction TTL if users routinely need longer","After a kernel restart, discard old poll tokens — they are in-memory and cannot be recovered"],"exampleFix":"// before\nok, err := model.OIDCValidateActivate(stalePollToken, binding) // expired\n// after\n// re-run validation first\nstart, err := model.OIDCValidateStart(redirectURL)\n// ... poll ... then:\nok, err := model.OIDCValidateActivate(start.PollToken, binding)","handlingStrategy":"try-catch","validationCode":"// Go: before activating, confirm the flow is still pending and fresh\n// (client-side: only activate within the documented TTL and after poll reports success)\nif !pollResult.Success || time.Since(pollResult.CompletedAt) > ttl { restartValidation() }","typeGuard":"func canActivate(txn *model.OIDCTransactionInfo) bool {\n    return txn != nil && txn.Flow == \"validate\" && txn.Completed && txn.Success && !txn.Activated\n}","tryCatchPattern":"ok, err := model.OIDCValidateActivate(pollToken, binding)\nif err != nil && strings.Contains(err.Error(), \"not found or has expired\") {\n    // restart the validation flow to get a fresh transaction\n    return restartOIDCValidation()\n}","preventionTips":["Activate promptly after poll success; do not leave the flow idle past the transaction TTL","Never cache poll tokens across kernel restarts — they are in-memory","Always send the exact binding value issued at flow start","Disable the activate button once it has succeeded to avoid double activation"],"tags":["oidc","auth","session-expired","state-management"],"backgroundTag":"oauth-state-expired","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"}