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

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 02/23/2021

I added the Enhanced User Series' article links.

Ken Haggerty
Created 08/26/20
Updated 02/23/21 23:58 GMT

Log In or Reset Quota to read more.

Successfully completed. Thank you for contributing.
Processing...
Something went wrong. Please try again.
Contribute to enjoy content without advertisments.
You can contribute without registering.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?