ASP.NET Core 3.1 - Admin 2FA Requirement
This article will describe how to implement a policy and a custom 2FA enabled claim to deny access to admin pages for administrators who have not enabled 2FA. I will assume you have downloaded the ASP.NET Core 3.1 - Users Without Identity Project or created a new ASP.NET Core 3.1 Razor Pages project. See Tutorial: Get started with Razor Pages in ASP.NET Core. You should review the earlier articles of the Users Without Identity Project series.
Users Without Identity Project and Article Series
Creation Series
- ASP.NET Core 3.1 - Users Without Identity
- ASP.NET Core 3.1 - User Entity
- ASP.NET Core 3.1 - Password Hasher
- ASP.NET Core 3.1 - User Management
- ASP.NET Core 3.1 - Admin Role
- ASP.NET Core 3.1 - Cookie Validator
- ASP.NET Core 3.1 - Concurrency Conflicts
- ASP.NET Core 3.1 - Must Change Password
- ASP.NET Core 3.1 - User Database Service
- ASP.NET Core 3.1 - Rename Related Entities
2FA Series
- ASP.NET Core 3.1 - 2FA Without Identity
- ASP.NET Core 3.1 - 2FA User Tokens
- ASP.NET Core 3.1 - 2FA Cookie Schemes
- ASP.NET Core 3.1 - 2FA Authenticating
- ASP.NET Core 3.1 - 2FA Sign In Service
- ASP.NET Core 3.1 - 2FA QR Code Generator
- ASP.NET Core 3.1 - Admin 2FA Requirement
- ASP.NET Core 3.1 - 2FA Admin Override
Enhanced User Series
- ASP.NET Core 3.1 - Enhanced User Without Identity
- ASP.NET Core 3.1 - 2FA Recovery Codes
- ASP.NET Core 3.1 - Login Lockout
- ASP.NET Core 3.1 - Created And Last Login Date
- ASP.NET Core 3.1 - Security Stamp
- ASP.NET Core 3.1 - Token Service
- ASP.NET Core 3.1 - Confirmed Email Address
- ASP.NET Core 3.1 - Password Recovery
I implemented the 2FA requirement for administrators using ASP.NET Core Identity in the BSNP and without Identity in the UWIP. MS Docs has an example for Identity which uses a TwoFactorEnabled policy and a custom UserClaimsPrincipalFactory. All admin pages must detect if the admin user logged in with a multi factor method. See Configure MFA for administration pages using ASP.NET Core Identity. The example requires the user to sign out and sign in after enabling or disabling 2FA.
After working with the example in the BSNP and a modified implementation for the UWIP, I developed a ApplicationUserClaimsPrincipalFactory which implements UserClaimsPrincipalFactory<ApplicationUser, IdentityRole> for Identity and a CreateUserPrincipalAsync method in the SignInService class for the UWIP. See the 2FA Sign In Service article.
BSNP - ApplicationUserClaimsPrincipalFactory.cs:
public class ApplicationUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole> { public ApplicationUserClaimsPrincipalFactory( UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor) { } public async override Task<ClaimsPrincipal> CreateAsync(ApplicationUser user) { var principal = await base.CreateAsync(user); var identity = (ClaimsIdentity)principal.Identity; var claims = new List<Claim> { new Claim(ApplicationClaimType.TwoFactorEnabled, user.TwoFactorEnabled.ToString()) }; identity.AddClaims(claims); return principal; } }
UWIP - SignInService.cs > CreateUserPrincipalAsync:
private async Task<ClaimsPrincipal> CreateUserPrincipalAsync(int userId, string authenticationMethod) { var user = await _userService.GetAppUserByIdAsync(userId); string rowVersionString = Encoding.Unicode.GetString(user.RowVersion); var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.Integer), new Claim(ClaimTypes.Name, user.LoginName), new Claim(ClaimTypes.UserData, rowVersionString), new Claim(ClaimTypes.AuthenticationMethod, authenticationMethod), new Claim(UWIPConstants.TwoFactorEnabledClaimType, user.TwoFactorEnabled.ToString()) }; if (user.IsAdmin) claims.Add(new Claim(ClaimTypes.Role, UWIPConstants.AdminRole)); if (user.MustChangePassword) claims.Add(new Claim(UWIPConstants.MustChangePasswordClaimType, string.Empty)); var applicationIdentity = new ClaimsIdentity(claims, UWIPConstants.ApplicationScheme); return new ClaimsPrincipal(applicationIdentity); }
Both implement a complied policy option, requireAdmin2FA which adds a 2FA enabled requirement to an Admin policy and a custom 2FA enabled claim to deny access to admin pages for administrators who have not enabled 2FA.
Edit Startup.cs > ConfigureServices:
services.AddAuthorization(options => { options.AddPolicy(UWIPConstants.AdminsPolicy, policy => { policy.RequireRole(UWIPConstants.AdminRole); if (requireAdmin2FA) policy.RequireClaim(UWIPConstants.TwoFactorEnabledClaimType, true.ToString()); }); });
Both projects use RefreshSignInAsync when 2FA is enabled and disabled which refreshes the custom 2FA enabled claim. No need to sign out and sign in. Access to admin pages is determined by the custom claim rather than the log in method.
Because the requirement is set by policy, attempts to browse admin pages without 2FA enabled redirects to the Access Denied page. I implement UI login to inform the admin user they need to enable 2FA like the MS Docs example. See UI logic to toggle user login information.
Edit _Layout.cshtml, inject AuthorizationService and SignInService:
@@using Microsoft.AspNetCore.Http.Features @@using Microsoft.AspNetCore.Authorization @@inject IAuthorizationService AuthorizationService @@using UsersWithoutIdentity.Services; @@inject ISignInService SignInService
Edit _Layout.cshtml:
@@if (SignInService.IsSignedIn(User) && User.IsInRole(UWIPConstants.AdminRole)) { @@if ((await AuthorizationService.AuthorizeAsync(User, UWIPConstants.AdminsPolicy)).Succeeded) { <li class="nav-item"> <a class="nav-link text-dark" asp-page="/Admin/Index">Admin</a> </li> } else { <li class="nav-item"> <a class="nav-link text-dark" asp-page="/Account/Manage/TwoFactorAuthentication" id="tooltip-demo" data-toggle="tooltip" data-placement="bottom" title="Administrators must enable Two-Factor Authentication."> Admin (Disabled) </a> </li> } }
Update 02/23/2021
I added the Enhanced User Series' article links.
Comments(0)