bitwarden/server · error · BadRequestException

The token associated with your request is expired. A valid t

Error message

The token associated with your request is expired. A valid token is required to continue.

What it means

BadRequestException 'The token associated with your request is expired...' is thrown in POST /webauthn (WebAuthnController.Post) when _createOptionsDataProtector.Unprotect(model.Token) succeeds but tokenable.TokenIsValid(user) returns false. The token is a data-protection-protected creation-options payload bound to the user; invalidity means it expired or is bound to a different user.

Source

Thrown at src/Api/Auth/Controllers/WebAuthnController.cs:111

        return new WebAuthnLoginAssertionOptionsResponseModel
        {
            Options = options,
            Token = token
        };
    }

    [Authorize(Policies.Application)]
    [HttpPost("")]
    public async Task<WebAuthnCredentialResponseModel> Post([FromBody] WebAuthnLoginCredentialCreateRequestModel model)
    {
        var user = await GetUserAsync();
        await ValidateIfUserCanUsePasskeyLogin(user.Id);
        var tokenable = _createOptionsDataProtector.Unprotect(model.Token);

        if (!tokenable.TokenIsValid(user))
        {
            throw new BadRequestException("The token associated with your request is expired. A valid token is required to continue.");
        }

        var credential = await _createWebAuthnLoginCredentialCommand.CreateWebAuthnLoginCredentialAsync(user, model.Name, tokenable.Options, model.DeviceResponse, model.SupportsPrf, model.EncryptedUserKey, model.EncryptedPublicKey, model.EncryptedPrivateKey);
        if (credential == null)
        {
            throw new BadRequestException("Unable to complete WebAuthn registration.");
        }

        return new WebAuthnCredentialResponseModel(credential);
    }

    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.");

View on GitHub (pinned to e93b962371)

Solutions

  1. Request fresh creation options (POST /webauthn/options or equivalent) and immediately complete registration.
  2. Use the same authenticated user for options request and credential creation.
  3. On self-hosted, ensure data-protection keys are persisted/consistent across restarts.
  4. Do not cache the token; treat it as short-lived.

Example fix

// before
api.post('/webauthn', { token: cachedOptionsToken, deviceResponse })
// after
const { token } = await api.post('/webauthn/options');
const deviceResponse = await navigator.credentials.create({ publicKey: parseOptions(token) });
api.post('/webauthn', { token, name, deviceResponse });
Defensive patterns

Strategy: validation

Validate before calling

if (!model.token) { model.token = (await api.post('/webauthn/options')).token; }

Try / catch

try { await api.post('/webauthn', model); }
catch (e) {
  if (e.response?.status === 400 && /expired/.test(e.response.data?.message)) {
    model.token = (await api.post('/webauthn/options')).token;
    throw new RetryableError('Restart passkey creation with a fresh token', model);
  }
  throw e;
}

Prevention

When it happens

Trigger: POST /webauthn (passkey creation, line 111) where model.Token is expired, reused after consumption, or minted for a different user than the current principal. The user must also pass ValidateIfUserCanUsePasskeyLogin first.

Common situations: Delay between requesting creation options and submitting the device response, user switched accounts, token cached client-side, or the data-protection keys rotated on a self-hosted reinstall.

Related errors


AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13). Data as JSON: /api/errors/11b9d1d6cf2e056d. Report an issue: GitHub.