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
- Refresh or re-supply a valid access token (re-login) before calling the two-factor list endpoint.
- Ensure the request includes the Authorization: Bearer <token> header with a non-expired token.
- If the user record was deleted, no token will work; recreate or restore the account.
- 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
- Refresh tokens proactively before expiry.
- Handle 401 globally in an axios/fetch interceptor to trigger re-login.
- Do not assume a long-cached token is still valid.
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/8bed5378bc5544d8.
Report an issue: GitHub.