{"record":{"id":"e6c6bfd05db8cc5b","repo":"bitwarden/server","slug":"the-token-associated-with-your-request-is-invalid","errorCode":null,"errorMessage":"The token associated with your request is invalid or has expired. A valid token is required to continue.","messagePattern":"The token associated with your request is invalid or has expired\\. A valid token is required to continue\\.","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Api/Auth/Controllers/WebAuthnController.cs","lineNumber":140,"sourceCode":"\n    private async Task ValidateIfUserCanUsePasskeyLogin(Guid userId)\n    {\n        var requireSsoPolicyRequirement = await _policyRequirementQuery.GetAsyncVNext<RequireSsoPolicyRequirement>(userId);\n\n        if (!requireSsoPolicyRequirement.CanUsePasskeyLogin)\n        {\n            throw new BadRequestException(\"Passkeys cannot be created for your account. SSO login is required.\");\n        }\n    }\n\n    [Authorize(Policies.Application)]\n    [HttpPut()]\n    public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateRequestModel model)\n    {\n        var tokenable = _assertionOptionsDataProtector.Unprotect(model.Token);\n        if (!tokenable.TokenIsValid(WebAuthnLoginAssertionOptionsScope.UpdateKeySet) || tokenable.Options == null)\n        {\n            throw new BadRequestException(\"The token associated with your request is invalid or has expired. A valid token is required to continue.\");\n        }\n\n        var (_, credential) = await _assertWebAuthnLoginCredentialCommand.AssertWebAuthnLoginCredential(tokenable.Options, model.DeviceResponse);\n        if (credential == null || credential.SupportsPrf != true)\n        {\n            throw new BadRequestException(\"Unable to update credential.\");\n        }\n\n        // assign new keys to credential\n        credential.EncryptedUserKey = model.EncryptedUserKey;\n        credential.EncryptedPrivateKey = model.EncryptedPrivateKey;\n        credential.EncryptedPublicKey = model.EncryptedPublicKey;\n\n        await _credentialRepository.UpdateAsync(credential);\n    }\n\n    [Authorize(Policies.Web)]\n    [HttpPost(\"{id}/delete\")]","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/Auth/Controllers/WebAuthnController.cs#L122-L158","documentation":"Thrown (HTTP 400) by PUT /webauthn when the WebAuthn assertion-options token for the UpdateCredential flow fails. The token is ASP.NET data-protection-encrypted and scoped to WebAuthnLoginAssertionOptionsScope.UpdateKeySet; it is rejected if Unprotect throws, TokenIsValid(UpdateKeySet) is false, or the tokenable carries no Options payload. This flow rotates a user's encryption keys via a PRf-capable passkey, so the token must be freshly issued for that exact scope.","triggerScenarios":"Submitting a token minted for a different scope (e.g. login) to the UpdateCredential endpoint; replaying an already-consumed token; a token past its lifetime; a data-protection key mismatch where the server that issued the token differs from the server that unprotects it (unshared key ring).","commonSituations":"Multi-node deployment without persisted/shared data-protection keys (each node encrypts with its own key); client reused a login token instead of requesting UpdateKeySet options; long delay between requesting options and submitting; redeploy onto a new host that lost ephemeral keys.","solutions":["Re-request assertion options for the UpdateKeySet scope and submit the freshly returned token to UpdateCredential immediately.","Persist and share ASP.NET data-protection keys across all server instances (Azure Blob/Redis/EFS key ring) so any node can unprotect tokens any node issued.","Confirm the client forwards the exact token string returned by the options endpoint with no truncation or URL-encoding damage.","Verify server clocks are synced so token expiry windows are not prematurely crossed."],"exampleFix":"// before: reusing a login-scoped token\nupdateCred({ token: loginToken, deviceResponse, ... }); // -> 400 invalid/expired\n\n// after: request the update-key-set scoped token first\nvar opts = await post('/webauthn/assertion-options', { scope: 'UpdateKeySet' });\nawait put('/webauthn', { token: opts.token, deviceResponse, encryptedUserKey, encryptedPrivateKey, encryptedPublicKey });","handlingStrategy":"validation","validationCode":"// Client-side: ensure a token was issued for the correct scope and is fresh before calling UpdateCredential.\nif (!assertionOptionsToken || scopeUsedToObtainIt !== 'UpdateKeySet') {\n  const opts = await requestAssertionOptions({ scope: 'UpdateKeySet' });\n  assertionOptionsToken = opts.token;\n}\nif (Date.now() - tokenIssuedAt > TOKEN_TTL_MS) {\n  /* re-request before submit */\n}","typeGuard":null,"tryCatchPattern":"// HTTP client: catch the 400 and prompt re-issuance of the update-key-set token.\ntry {\n  await put('/webauthn', payload);\n} catch (e) {\n  if (e.isBadRequest && /invalid or has expired/i.test(e.message)) {\n    await refreshUpdateKeySetToken(); // re-request options then retry once\n  } else { throw e; }\n}","preventionTips":["Persist ASP.NET data-protection keys to shared storage so all nodes share one key ring.","Always request assertion options scoped to UpdateKeySet immediately before calling UpdateCredential.","Do not cache or reuse tokens across flows."],"tags":["webauthn","passkey","token","authentication","data-protection","aspnet"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}