bitwarden/server · error · BadRequestException
The token associated with your request is invalid or has exp
Error message
The token associated with your request is invalid or has expired. A valid token is required to continue.
What it means
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.
Source
Thrown at src/Api/Auth/Controllers/WebAuthnController.cs:140
private async Task ValidateIfUserCanUsePasskeyLogin(Guid userId)
{
var requireSsoPolicyRequirement = await _policyRequirementQuery.GetAsyncVNext<RequireSsoPolicyRequirement>(userId);
if (!requireSsoPolicyRequirement.CanUsePasskeyLogin)
{
throw new BadRequestException("Passkeys cannot be created for your account. SSO login is required.");
}
}
[Authorize(Policies.Application)]
[HttpPut()]
public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateRequestModel model)
{
var tokenable = _assertionOptionsDataProtector.Unprotect(model.Token);
if (!tokenable.TokenIsValid(WebAuthnLoginAssertionOptionsScope.UpdateKeySet) || tokenable.Options == null)
{
throw new BadRequestException("The token associated with your request is invalid or has expired. A valid token is required to continue.");
}
var (_, credential) = await _assertWebAuthnLoginCredentialCommand.AssertWebAuthnLoginCredential(tokenable.Options, model.DeviceResponse);
if (credential == null || credential.SupportsPrf != true)
{
throw new BadRequestException("Unable to update credential.");
}
// assign new keys to credential
credential.EncryptedUserKey = model.EncryptedUserKey;
credential.EncryptedPrivateKey = model.EncryptedPrivateKey;
credential.EncryptedPublicKey = model.EncryptedPublicKey;
await _credentialRepository.UpdateAsync(credential);
}
[Authorize(Policies.Web)]
[HttpPost("{id}/delete")]View on GitHub (pinned to e93b962371)
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.
Example fix
// before: reusing a login-scoped token
updateCred({ token: loginToken, deviceResponse, ... }); // -> 400 invalid/expired
// after: request the update-key-set scoped token first
var opts = await post('/webauthn/assertion-options', { scope: 'UpdateKeySet' });
await put('/webauthn', { token: opts.token, deviceResponse, encryptedUserKey, encryptedPrivateKey, encryptedPublicKey }); Defensive patterns
Strategy: validation
Validate before calling
// Client-side: ensure a token was issued for the correct scope and is fresh before calling UpdateCredential.
if (!assertionOptionsToken || scopeUsedToObtainIt !== 'UpdateKeySet') {
const opts = await requestAssertionOptions({ scope: 'UpdateKeySet' });
assertionOptionsToken = opts.token;
}
if (Date.now() - tokenIssuedAt > TOKEN_TTL_MS) {
/* re-request before submit */
} Try / catch
// HTTP client: catch the 400 and prompt re-issuance of the update-key-set token.
try {
await put('/webauthn', payload);
} catch (e) {
if (e.isBadRequest && /invalid or has expired/i.test(e.message)) {
await refreshUpdateKeySetToken(); // re-request options then retry once
} else { throw e; }
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- The token associated with your request is expired. A valid t
- InvalidSsoToken
- Invalid token.
- Unable to update credential.
- SsoOrganizationIdMismatch
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/e6c6bfd05db8cc5b.
Report an issue: GitHub.