{"record":{"id":"b0f9672b72a438f1","repo":"bitwarden/server","slug":"invalid-token-b0f967","errorCode":null,"errorMessage":"Invalid token.","messagePattern":"Invalid token\\.","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Api/AdminConsole/Controllers/OrganizationsController.cs","lineNumber":350,"sourceCode":"    [Obsolete(\"This endpoint is deprecated. Use DELETE method instead\")]\n    public async Task PostDelete(string id, [FromBody] SecretVerificationRequestModel model)\n    {\n        await Delete(id, model);\n    }\n\n    [HttpPost(\"{id}/delete-recover-token\")]\n    [AllowAnonymous]\n    public async Task PostDeleteRecoverToken(Guid id, [FromBody] OrganizationVerifyDeleteRecoverRequestModel model)\n    {\n        var organization = await _organizationRepository.GetByIdAsync(id);\n        if (organization == null)\n        {\n            throw new NotFoundException();\n        }\n\n        if (!_orgDeleteTokenDataFactory.TryUnprotect(model.Token, out var data) || !data.Valid || !data.IsValid(organization))\n        {\n            throw new BadRequestException(\"Invalid token.\");\n        }\n\n        if (organization.IsValidClient())\n        {\n            var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);\n            if (provider.IsBillable())\n            {\n                await _providerBillingService.ScaleSeats(\n                    provider,\n                    organization.PlanType,\n                    -organization.Seats ?? 0);\n            }\n        }\n\n        await _organizationDeleteCommand.DeleteAsync(organization);\n    }\n\n    [HttpPost(\"{id}/api-key\")]","sourceCodeStart":332,"sourceCodeEnd":368,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L332-L368","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: reusing a stale token from an earlier email\nawait api.post(`organizations/${id}/delete-recover-token`, { token: oldToken });\n// after: fetch the latest token and redeem it immediately\nconst fresh = await getMostRecentRecoveryToken();\nawait api.post(`organizations/${id}/delete-recover-token`, { token: fresh.trim() });","handlingStrategy":"validation","validationCode":"function looksLikeValidToken(token) {\n  return typeof token === 'string' && token.trim().length > 16 && !/\\s/.test(token.trim());\n}\nif (!looksLikeValidToken(model.token)) abort('token missing/truncated');","typeGuard":"function isNonEmptyToken(v): v is string {\n  return typeof v === 'string' && v.trim().length > 0;\n}","tryCatchPattern":"try { await api.post(`organizations/${id}/delete-recover-token`, model); }\ncatch (e) {\n  if (e?.response?.status === 400) await requestFreshDeleteRecoverToken();\n  else throw e;\n}","preventionTips":["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."],"tags":["bitwarden","token","data-protection","delete-recover","aspnet"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}