{"record":{"id":"912b49dda5aa8506","repo":"fullstackhero/dotnet-starter-kit","slug":"unauthorized-enrolltwofactorcommandhandler","errorCode":null,"errorMessage":"Unauthorized","messagePattern":"Unauthorized","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"warning","filePath":"src/Modules/Identity/Modules.Identity/Features/v1/TwoFactor/Enroll/EnrollTwoFactorCommandHandler.cs","lineNumber":33,"sourceCode":"    private const string IssuerName = \"FullStackHero\";\n\n    private readonly UserManager<FshUser> _userManager;\n    private readonly ICurrentUser _currentUser;\n\n    public EnrollTwoFactorCommandHandler(UserManager<FshUser> userManager, ICurrentUser currentUser)\n    {\n        _userManager = userManager;\n        _currentUser = currentUser;\n    }\n\n    public async ValueTask<TwoFactorEnrollmentResponse> Handle(\n        EnrollTwoFactorCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        if (!_currentUser.IsAuthenticated())\n        {\n            throw new UnauthorizedException();\n        }\n\n        var userId = _currentUser.GetUserId().ToString();\n        var user = await _userManager.FindByIdAsync(userId)\n            ?? throw new NotFoundException($\"User {userId} not found.\");\n\n        // Always reset so calling enroll twice rotates the secret — prevents stale codes\n        // from a prior incomplete enrollment from silently succeeding.\n        await _userManager.ResetAuthenticatorKeyAsync(user);\n        var sharedKey = await _userManager.GetAuthenticatorKeyAsync(user)\n            ?? throw new CustomException(\"Failed to generate authenticator key.\");\n\n        var email = user.Email ?? user.UserName ?? user.Id;\n        var authenticatorUri = string.Format(\n            System.Globalization.CultureInfo.InvariantCulture,\n            \"otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6\",\n            UrlEncoder.Default.Encode(IssuerName),\n            UrlEncoder.Default.Encode(email),","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/TwoFactor/Enroll/EnrollTwoFactorCommandHandler.cs#L15-L51","documentation":"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.","triggerScenarios":"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.","commonSituations":"Access token expired between login and enrollment; frontend dropped the Authorization header on a retry; auth middleware ordering issue; integration test forgot to authenticate.","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"],"exampleFix":"// before\nconst res = await fetch('/api/v1/users/2fa/enroll', { method: 'POST' });\n// after\nconst res = await apiFetch('/api/v1/users/2fa/enroll', { method: 'POST', auth: true });","handlingStrategy":"try-catch","validationCode":"function canEnroll2fa() {\n  return Boolean(accessToken) && !isTokenExpired(accessToken);\n}\nif (!canEnroll2fa()) await reauthenticate();","typeGuard":null,"tryCatchPattern":"try {\n  const qr = await api.enrollTwoFactor();\n} catch (e) {\n  if (e.status === 401) { await reauthenticate(); return retry(); }\n  throw e;\n}","preventionTips":["Complete enroll→verify inside one authenticated session","Centralize auth header injection in the API client","Refresh tokens proactively near expiry"],"tags":["authentication","jwt","identity","two-factor"],"backgroundTag":"authentication-required","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}