ASP.NET Core 3.1 - Admin 2FA Requirement

Ken Haggerty
Created 08/26/2020 - Updated 11/25/2020 00:11

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 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.

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>
    }
}
BSNP 2FA Disabled
UWIP 2FA Disabled
BSNP 2FA Enabled
UWIP 2FA Enabled
Update 11/24/2020

I added the Rename Related Entities article and updated the project and article links.

Comment Count = 0

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications