ASP.NET Core 3.1 - Admin 2FA Requirement


Ken Haggerty
Created 08/26/2020 - Updated 08/31/2020 06:29

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.

Users Without Identity Project and Article Series

Access to the research project source code may be purchased on KenHaggerty.Com at Manage > Assets. A project which implements users without Identity has been published to demo.kenhaggerty.com. I enjoy writing these articles. It often enhances and clarifies my coding. The research project is a result of a lot of refactoring and hopefully provides logical segues for the articles. Thank you for supporting my efforts.

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 08/31/2020

I added the 2FA Admin Override article link.


Comment Count = 0

Please log in to comment or follow.

Login Register
Follow to get web notifications when new comments are posted to this article.
Logged in users receive web notifications for new articles, topics and assets.
Web Notifications