bitwarden/server · error · BadRequestException
Invalid token.
Error message
Invalid token.
What it means
Thrown as a 400 BadRequestException("Invalid token.") from the AllowAnonymous POST {id}/delete-recover-token endpoint. The token (model.Token) is a protected, time-limited, organization-bound data blob produced by _orgDeleteTokenDataFactory; TryUnprotect must succeed AND data.Valid must be true AND data.IsValid(organization) must be true. Failure of any one condition aborts the delete-recover confirmation.
Source
Thrown at src/Api/AdminConsole/Controllers/OrganizationsController.cs:350
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
public async Task PostDelete(string id, [FromBody] SecretVerificationRequestModel model)
{
await Delete(id, model);
}
[HttpPost("{id}/delete-recover-token")]
[AllowAnonymous]
public async Task PostDeleteRecoverToken(Guid id, [FromBody] OrganizationVerifyDeleteRecoverRequestModel model)
{
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization == null)
{
throw new NotFoundException();
}
if (!_orgDeleteTokenDataFactory.TryUnprotect(model.Token, out var data) || !data.Valid || !data.IsValid(organization))
{
throw new BadRequestException("Invalid token.");
}
if (organization.IsValidClient())
{
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
if (provider.IsBillable())
{
await _providerBillingService.ScaleSeats(
provider,
organization.PlanType,
-organization.Seats ?? 0);
}
}
await _organizationDeleteCommand.DeleteAsync(organization);
}
[HttpPost("{id}/api-key")]View on GitHub (pinned to e93b962371)
Solutions
- Request a fresh delete-recover token (re-trigger the delete-recover flow) and use only the most recent token.
- On self-hosted deployments, persist the ASP.NET data-protection key ring (DataProtection keystorage) to durable storage so TryUnprotect can validate older tokens.
- Ensure the token is transmitted verbatim with no whitespace/URL-truncation; URL-decode and trim before sending.
- Confirm system clocks on the server are correct (NTP) so the token's validity window is honored.
Example fix
// before: reusing a stale token from an earlier email
await api.post(`organizations/${id}/delete-recover-token`, { token: oldToken });
// after: fetch the latest token and redeem it immediately
const fresh = await getMostRecentRecoveryToken();
await api.post(`organizations/${id}/delete-recover-token`, { token: fresh.trim() }); Defensive patterns
Strategy: validation
Validate before calling
function looksLikeValidToken(token) {
return typeof token === 'string' && token.trim().length > 16 && !/\s/.test(token.trim());
}
if (!looksLikeValidToken(model.token)) abort('token missing/truncated'); Type guard
function isNonEmptyToken(v): v is string {
return typeof v === 'string' && v.trim().length > 0;
} Try / catch
try { await api.post(`organizations/${id}/delete-recover-token`, model); }
catch (e) {
if (e?.response?.status === 400) await requestFreshDeleteRecoverToken();
else throw e;
} Prevention
- Always redeem the most recently issued token.
- Persist the ASP.NET data-protection key ring on self-hosted deployments.
- URL-decode and trim tokens before submission; watch for copy-paste truncation.
- Keep server clocks synced via NTP.
When it happens
Trigger: Calling POST organizations/{id}/delete-recover-token where model.Token is expired, tampered, signed by a different key, already consumed, or was issued for a different organization than {id}. Also triggered if the token data protection key has rotated since issuance (e.g., after a server migration/restore without key persistence).
Common situations: Token reused after the email link was already clicked; clock skew pushing the token outside its validity window; self-hosted instance whose data-protection keys were not persisted across a container/VM recreation so TryUnprotect fails; copy-paste truncation of the token string; user clicked an old recovery email after requesting a new one.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- The token associated with your request is invalid or has exp
- Invalid token.
- InvalidSsoToken
- Resource not found.
- User verification failed.
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/b0f9672b72a438f1.
Report an issue: GitHub.