{"record":{"id":"b77cc0b218be0eb7","repo":"bitwarden/server","slug":"unauthorized-b77cc0","errorCode":null,"errorMessage":"Unauthorized.","messagePattern":"Unauthorized\\.","errorType":"exception","errorClass":"UnauthorizedAccessException","httpStatus":401,"severity":"error","filePath":"src/Api/Auth/Controllers/AccountsController.cs","lineNumber":119,"sourceCode":"        _rotateUserApiKeyCommand = rotateUserApiKeyCommand;\n        _selfServiceChangeEmailCommand = selfServiceChangeEmailCommand;\n    }\n\n\n    [HttpPost(\"password-hint\")]\n    [AllowAnonymous]\n    public async Task PostPasswordHint([FromBody] PasswordHintRequestModel model)\n    {\n        await _userService.SendMasterPasswordHintAsync(model.Email);\n    }\n\n    [HttpPost(\"email-token\")]\n    public async Task PostEmailToken([FromBody] EmailTokenRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        // TODO: PM-39120 - PM30806_SelfServiceChangeEmailCommand flag cleanup, remove the flag\n        // check and keep only the SelfServiceChangeEmailCommand call.\n        if (_featureService.IsEnabled(FeatureFlagKeys.PM30806_SelfServiceChangeEmailCommand))\n        {\n            await _selfServiceChangeEmailCommand.InitiateChangeEmailAsync(\n                user, model.MasterPasswordHash, model.NewEmail);\n\n            return;\n        }\n\n        if (user.UsesKeyConnector)\n        {\n            throw new BadRequestException(\"You cannot change your email when using Key Connector.\");\n        }\n\n        if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/Auth/Controllers/AccountsController.cs#L101-L137","documentation":"Thrown as UnauthorizedAccessException (HTTP 401) from POST accounts/email-token when _userService.GetUserByPrincipalAsync(User) returns null. The endpoint requires an authenticated principal; a null user means no valid session/claim was resolved, so the request is rejected before any email-change logic runs.","triggerScenarios":"Calling POST /accounts/email-token with a missing, expired, or invalid access token, or a token whose claims do not map to a user record (e.g., the user was deleted after the token was issued).","commonSituations":"Access token expired between page load and the request; user signed out in another tab; token issued for a since-deleted account; client omitted the Authorization header; clock skew causing token validation failure.","solutions":["Refresh the access token (re-authenticate or use the refresh token) and retry the request.","Ensure the Authorization: Bearer <token> header is present and not malformed.","If the account was deleted, surface a sign-in error rather than retrying.","Sync client clocks (NTP) to avoid spurious token-expiry failures."],"exampleFix":"// before\nawait api.post('accounts/email-token', body); // 401 if token stale\n// after\nif (tokenIsExpired(accessToken)) accessToken = await refresh();\nawait api.post('accounts/email-token', body, { headers: { Authorization: `Bearer ${accessToken}` } });","handlingStrategy":"validation","validationCode":"function hasValidBearer(token) {\n  return typeof token === 'string' && /^Bearer \\S+$/i.test(token) && !tokenIsExpired(token);\n}","typeGuard":null,"tryCatchPattern":"try { await api.post('accounts/email-token', body); }\ncatch (e) { if (e?.response?.status === 401) { await refresh(); await api.post('accounts/email-token', body); } else throw e; }","preventionTips":["Attach a fresh Authorization: Bearer header to every authenticated request.","Refresh tokens proactively before expiry.","Sync client clocks via NTP.","Handle sign-out in other tabs by re-authenticating."],"tags":["bitwarden","authentication","unauthorized","email-change","aspnet"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}