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
- Sign in again and retry with a valid Authorization: Bearer header
- Ensure the endpoint requires authentication (no [AllowAnonymous]) and middleware order is UseAuthentication then UseAuthorization
- Validate JWT parameters (issuer, audience, key, lifetime) if valid tokens are being rejected
- 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
- Complete enroll→verify inside one authenticated session
- Centralize auth header injection in the API client
- Refresh tokens proactively near expiry
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
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)