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

  1. Request a fresh delete-recover token (re-trigger the delete-recover flow) and use only the most recent token.
  2. On self-hosted deployments, persist the ASP.NET data-protection key ring (DataProtection keystorage) to durable storage so TryUnprotect can validate older tokens.
  3. Ensure the token is transmitted verbatim with no whitespace/URL-truncation; URL-decode and trim before sending.
  4. 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

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

Related errors


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