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

  1. Refresh the access token (re-authenticate or use the refresh token) and retry the request.
  2. Ensure the Authorization: Bearer <token> header is present and not malformed.
  3. If the account was deleted, surface a sign-in error rather than retrying.
  4. 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

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

Related errors


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