bitwarden/server · error · UnauthorizedAccessException

Unauthorized.

Error message

Unauthorized.

What it means

UnauthorizedAccessException is thrown (which the Bitwarden pipeline maps to HTTP 401 'Unauthorized.') when _userService.GetUserByPrincipalAsync(User) returns null in the GET /two-factor endpoint. This means the authenticated principal could not be resolved to a real User entity, typically because the token is absent, expired, revoked, or the user record no longer exists.

Source

Thrown at src/Api/Auth/Controllers/TwoFactorController.cs:89

        _authRequestRepository = authRequestRepository;
        _duoUniversalTokenService = duoUniversalConfigService;
        _twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
        _twoFactorUserVerificationDataProtector = twoFactorUserVerificationDataProtector;
        _twoFactorUserVerificationTokenableFactory = twoFactorUserVerificationTokenableFactory;
        _ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
        _twoFactorEmailService = twoFactorEmailService;
        _startTwoFactorWebAuthnRegistrationCommand = startTwoFactorWebAuthnRegistrationCommand;
        _completeTwoFactorWebAuthnRegistrationCommand = completeTwoFactorWebAuthnRegistrationCommand;
        _deleteTwoFactorWebAuthnCredentialCommand = deleteTwoFactorWebAuthnCredentialCommand;
    }

    [HttpGet("")]
    public async Task<ListResponseModel<TwoFactorProviderResponseModel>> Get()
    {
        var user = await _userService.GetUserByPrincipalAsync(User);
        if (user == null)
        {
            throw new UnauthorizedAccessException();
        }

        var providers = user.GetTwoFactorProviders()?.Select(
            p => new TwoFactorProviderResponseModel(p.Key, p.Value));
        return new ListResponseModel<TwoFactorProviderResponseModel>(providers);
    }

    [HttpGet("~/organizations/{id}/two-factor")]
    public async Task<ListResponseModel<TwoFactorProviderResponseModel>> GetOrganization(string id)
    {
        var orgIdGuid = new Guid(id);
        if (!await _currentContext.OrganizationAdmin(orgIdGuid))
        {
            throw new NotFoundException();
        }

        var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
        if (organization == null)

View on GitHub (pinned to e93b962371)

Solutions

  1. Refresh or re-supply a valid access token (re-login) before calling the two-factor list endpoint.
  2. Ensure the request includes the Authorization: Bearer <token> header with a non-expired token.
  3. If the user record was deleted, no token will work; recreate or restore the account.
  4. In integration tests, seed an authenticated user principal via the test SutProvider/auth fixtures.

Example fix

// before
const res = await api.get('/users/two-factor'); // stale token
// after
await authService.refreshToken();
const res = await api.get('/users/two-factor', { headers: authHeader() });
Defensive patterns

Strategy: validation

Validate before calling

if (!authService.hasValidToken()) { await authService.refreshOrReLogin(); }

Try / catch

try { return await api.get('/users/two-factor'); }
catch (e) {
  if (e.response?.status === 401) { await authService.reauthenticate(); throw e; }
  throw e;
}

Prevention

When it happens

Trigger: GET /api/users/two-factor (TwoFactorController.Get at line 89) called with a missing/expired access token, a revoked session, or a principal whose user was deleted. The explicit null-check guard fires before any provider data is loaded.

Common situations: Client stored a stale access token after logout/session-expiry, the auth cookie expired, or the account was deleted between token issuance and this call. Also occurs in tests/mocks where the user principal is not seeded.

Understand the failure class

Related errors


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