fullstackhero/dotnet-starter-kit · warning · UnauthorizedException

Unauthorized

Error message

Unauthorized

What it means

EnrollTwoFactorCommandHandler throws UnauthorizedException when ICurrentUser.IsAuthenticated() is false, guarding the 2FA enrollment flow against anonymous access. Enrollment rotates the authenticator key, so it must always run in the context of a real authenticated user.

Solutions

  1. Sign in again and retry with a valid Authorization: Bearer header
  2. Ensure the endpoint requires authentication (no [AllowAnonymous]) and middleware order is UseAuthentication then UseAuthorization
  3. Validate JWT parameters (issuer, audience, key, lifetime) if valid tokens are being rejected
  4. In unit tests, provide an ICurrentUser fake with IsAuthenticated() = true

Example fix

// before
const res = await fetch('/api/v1/users/2fa/enroll', { method: 'POST' });
// after
const res = await apiFetch('/api/v1/users/2fa/enroll', { method: 'POST', auth: true });
Defensive patterns

Strategy: try-catch

Validate before calling

function canEnroll2fa() {
  return Boolean(accessToken) && !isTokenExpired(accessToken);
}
if (!canEnroll2fa()) await reauthenticate();

Try / catch

try {
  const qr = await api.enrollTwoFactor();
} catch (e) {
  if (e.status === 401) { await reauthenticate(); return retry(); }
  throw e;
}

Prevention

When it happens

Trigger: Hitting the enroll-2FA endpoint without a valid bearer token, with an expired JWT, or invoking the handler in tests without an authenticated ICurrentUser stub.

Common situations: Access token expired between login and enrollment; frontend dropped the Authorization header on a retry; auth middleware ordering issue; integration test forgot to authenticate.

Understand the failure class

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/912b49dda5aa8506. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Identity/Modules.Identity/Features/v1/TwoFactor/Enroll/EnrollTwoFactorCommandHandler.cs:33

    private const string IssuerName = "FullStackHero";

    private readonly UserManager<FshUser> _userManager;
    private readonly ICurrentUser _currentUser;

    public EnrollTwoFactorCommandHandler(UserManager<FshUser> userManager, ICurrentUser currentUser)
    {
        _userManager = userManager;
        _currentUser = currentUser;
    }

    public async ValueTask<TwoFactorEnrollmentResponse> Handle(
        EnrollTwoFactorCommand command, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(command);

        if (!_currentUser.IsAuthenticated())
        {
            throw new UnauthorizedException();
        }

        var userId = _currentUser.GetUserId().ToString();
        var user = await _userManager.FindByIdAsync(userId)
            ?? throw new NotFoundException($"User {userId} not found.");

        // Always reset so calling enroll twice rotates the secret — prevents stale codes
        // from a prior incomplete enrollment from silently succeeding.
        await _userManager.ResetAuthenticatorKeyAsync(user);
        var sharedKey = await _userManager.GetAuthenticatorKeyAsync(user)
            ?? throw new CustomException("Failed to generate authenticator key.");

        var email = user.Email ?? user.UserName ?? user.Id;
        var authenticatorUri = string.Format(
            System.Globalization.CultureInfo.InvariantCulture,
            "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6",
            UrlEncoder.Default.Encode(IssuerName),
            UrlEncoder.Default.Encode(email),

View on GitHub (pinned to 3f2959e683)