bitwarden/server · error · UnauthorizedAccessException
Unauthorized.
Error message
Unauthorized.
What it means
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.
Source
Thrown at src/Api/Auth/Controllers/AccountsController.cs:119
_rotateUserApiKeyCommand = rotateUserApiKeyCommand;
_selfServiceChangeEmailCommand = selfServiceChangeEmailCommand;
}
[HttpPost("password-hint")]
[AllowAnonymous]
public async Task PostPasswordHint([FromBody] PasswordHintRequestModel model)
{
await _userService.SendMasterPasswordHintAsync(model.Email);
}
[HttpPost("email-token")]
public async Task PostEmailToken([FromBody] EmailTokenRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
// TODO: PM-39120 - PM30806_SelfServiceChangeEmailCommand flag cleanup, remove the flag
// check and keep only the SelfServiceChangeEmailCommand call.
if (_featureService.IsEnabled(FeatureFlagKeys.PM30806_SelfServiceChangeEmailCommand))
{
await _selfServiceChangeEmailCommand.InitiateChangeEmailAsync(
user, model.MasterPasswordHash, model.NewEmail);
return;
}
if (user.UsesKeyConnector)
{
throw new BadRequestException("You cannot change your email when using Key Connector.");
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))View on GitHub (pinned to e93b962371)
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.
Example fix
// before
await api.post('accounts/email-token', body); // 401 if token stale
// after
if (tokenIsExpired(accessToken)) accessToken = await refresh();
await api.post('accounts/email-token', body, { headers: { Authorization: `Bearer ${accessToken}` } }); Defensive patterns
Strategy: validation
Validate before calling
function hasValidBearer(token) {
return typeof token === 'string' && /^Bearer \S+$/i.test(token) && !tokenIsExpired(token);
} Try / catch
try { await api.post('accounts/email-token', body); }
catch (e) { if (e?.response?.status === 401) { await refresh(); await api.post('accounts/email-token', body); } else throw e; } Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Invalid password.
- User verification failed.
- Invalid password.
- You cannot change your email when using Key Connector.
- NewMasterPasswordHash and Key are required.
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/b77cc0b218be0eb7.
Report an issue: GitHub.