ASP.NET Core 6.0 - Claims-Based Authorization

This article describes the implementation of claims-based authorization. I will assume you have downloaded the ASP.NET Core 6.0 - Users With Device 2FA Project.

Users With Device 2FA Project and Article Series

This article series is about the implementation of ASP.NET Core 6.0 with Visual Studio 2022. The ASP.NET Core 6.0 - Users With Device 2FA Project (UWD2FAP) implements WebAuthn, also known as FIDO2, instead of authenticator apps for two-factor authentication (2FA). The project implements Bootstrap v5 and Bootstrap Native. After a user registers, they can enable 2FA with Windows Hello, Android Lock Screen, or a FIDO2 security key. If I had this project when I created KenHaggerty. Com three years ago, I would have started with this user authentication template. The latest version is published at Preview. KenHaggerty. Com. I encourage you to register and evaluate multiple 2FA devices in Manage Account > Two-Factor Authentication. Details, screenshots, and related articles can be found at ASP.NET Core 6.0 - Users With Device 2FA Project. The details page includes the version change log.

In simple terms, authentication is the process of verifying who a user is, while authorization is the process of verifying what they have access to.

Comparing these processes to a real-world example, when you go through security in an airport, you show your ID to authenticate your identity. Then, when you arrive at the gate, you present your boarding pass to the flight attendant, so they can authorize you to board your flight and allow access to the plane. AUTH0 - What are authentication and authorization?

Claims-based authorization in ASP.NET Core or claims-based access control (CBAC) is not easy. While Role-based authorization in ASP.NET Core or role-based access control (RBAC) might be easier to understand and implement, CBAC provides more granular control of what the user can access supporting the principle of least privilege. The information security principle of least privilege asserts that users and applications should be granted access only to the data and operations they require to perform their jobs. See Enhance security with the principle of least privilege. RBAC is implemented with the predefined ClaimTypes.Role. CBAC is implemented with ClaimsIdentity and AuthorizationPolicy. Policies represent a collection of authorization requirements and the scheme or schemes they are evaluated against, all of which must succeed for authorization to succeed.

The ClaimsIdentity class is a concrete implementation of a claims-based identity; that is, an identity described by a collection of claims. A claim is a statement about an entity made by an issuer that describes a property, right, or some other quality of that entity. Such an entity is said to be the subject of the claim. A claim is represented by the Claim class. The claims contained in a ClaimsIdentity describe the entity that the corresponding identity represents, and can be used to make authorization and authentication decisions. A claims-based access model has many advantages over more traditional access models that rely exclusively on roles. For example, claims can provide much richer information about the identity they represent and can be evaluated for authorization or authentication in a far more specific manner. ClaimsIdentity Class

The UWD2FAP implements an AppUserClaim entity to store claim properties by AppUser. The CreateUserPrincipalAsync method in the SignInService adds the NameIdentifier, Name, Sid, and AuthenticationMethod predefined ClaimTypes then adds a claim for each AppUserClaim.

Services > SignInService
/// <summary>
/// Creates a <see cref="ClaimsPrincipal"/> for the <paramref name="appUser"/>, asynchronous.
/// </summary>
/// <param name="appUser">The the user to create a <see cref="ClaimsPrincipal"/> for.</param>
/// <param name="authenticationMethod">Name of the method used to authenticate the user.</param>
/// <returns><see cref="Task{TResult}"/> for <see cref="ClaimsPrincipal"/></returns>
private async Task<ClaimsPrincipal> CreateUserPrincipalAsync(AppUser appUser, string authenticationMethod)
{
    var claims = new List<Claim>
            {
                new Claim(ClaimTypes.NameIdentifier, appUser.Id.ToString(), ClaimValueTypes.Integer, AppSettings.ClaimIssuer),
                new Claim(ClaimTypes.Name, appUser.LoginName, ClaimValueTypes.String, AppSettings.ClaimIssuer),
                new Claim(ClaimTypes.Sid, appUser.SecurityStamp, ClaimValueTypes.String, AppSettings.ClaimIssuer),
                new Claim(ClaimTypes.AuthenticationMethod, authenticationMethod, ClaimValueTypes.String, AppSettings.ClaimIssuer)
            };

    var appUserClaims = await _userService.GetAppUserClaimsByAppUserIdAsync(appUser.Id).ConfigureAwait(false);
    foreach (var appUserClaim in appUserClaims)
        claims.Add(new Claim(appUserClaim.Type, appUserClaim.Value, appUserClaim.ValueType!, AppSettings.ClaimIssuer));

    return new ClaimsPrincipal(new ClaimsIdentity(claims, AppSettings.ApplicationScheme));
}

The UWD2FAP implements user claims like TwoFactorEnabled, TermsOfService, MustChangePassword, and authorization claims. To manage authorization claims, I developed an AuthorizationClaims class which holds the generic Claim values. The UWD2FAP defines application claims with AppSettings, AppClaimTypes, and AppClaimValues static classes which define constants for the claim.

AuthorizationClaims.cs
// Copyright © Ken Haggerty (https://kenhaggerty.com)
// Licensed under the MIT License.
namespace UsersWithDevice2FA.Models.AppClaims;
/// <summary>
/// Generic readonly list of all <see cref="Claim"/> used for authorization.
/// </summary>
public static class AuthorizationClaims
{
    public static readonly Claim Administrator = new(AppClaimTypes.Super, AppClaimValues.Administrator,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);
    public static readonly Claim Security = new(AppClaimTypes.Super, AppClaimValues.Security,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);

    public static readonly Claim EmailRead = new(AppClaimTypes.Email, AppClaimValues.Read,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);
    public static readonly Claim EmailWrite = new(AppClaimTypes.Email, AppClaimValues.Write,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);
    public static readonly Claim EmailSend = new(AppClaimTypes.Email, AppClaimValues.Send,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);

    public static readonly Claim AppUserRead = new(AppClaimTypes.AppUser, AppClaimValues.Read,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);
    public static readonly Claim AppUserWrite = new(AppClaimTypes.AppUser, AppClaimValues.Write,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);
    public static readonly Claim AppUserDelete = new(AppClaimTypes.AppUser, AppClaimValues.Delete,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);

    public static readonly Claim AuthorizationRead = new(AppClaimTypes.Authorization, 
        AppClaimValues.Read, ClaimValueTypes.String, AppSettings.ClaimIssuer);
    public static readonly Claim AuthorizationAssign = new(AppClaimTypes.Authorization,
        AppClaimValues.Assign, ClaimValueTypes.String, AppSettings.ClaimIssuer);

    public static readonly Claim CredentialRead = new(AppClaimTypes.Credential, AppClaimValues.Read,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);
    public static readonly Claim CredentialWrite = new(AppClaimTypes.Credential, AppClaimValues.Write,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);
    public static readonly Claim CredentialDelete = new(AppClaimTypes.Credential, AppClaimValues.Delete,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);

    public static readonly Claim ChallengeRead = new(AppClaimTypes.Challenge, AppClaimValues.Read,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);
    public static readonly Claim ChallengeDelete = new(AppClaimTypes.Challenge, AppClaimValues.Delete,
        ClaimValueTypes.String, AppSettings.ClaimIssuer);

    /// <summary>
    /// Gets an array of all <see cref="AuthorizationClaims"/>.
    /// </summary>
    /// <returns><see cref="T:Claim[]"/></returns>
    public static Claim[] ToArray()
    {
        Type t = typeof(AuthorizationClaims);
        FieldInfo[] fields = t.GetFields(BindingFlags.Static | BindingFlags.Public);
        List<Claim> claims = new();
        foreach (FieldInfo fi in fields)
        {
            if (fi.GetValue(t) is Claim claim)
                if (claim.Value != AppClaimValues.NA)
                    claims.Add(claim);
        }
        return claims.ToArray();
    }
    /// <summary>
    /// Gets an array of all <see cref="AuthorizationClaims"/> where
    /// Type = <see cref="AppClaimTypes.Super"/>.
    /// </summary>
    /// <returns><see cref="T:Claim[]"/></returns>
    public static Claim[] ToSuperArray()
    {
        Type t = typeof(AuthorizationClaims);
        FieldInfo[] fields = t.GetFields(BindingFlags.Static | BindingFlags.Public);
        List<Claim> claims = new();
        foreach (FieldInfo fi in fields)
        {
            if (fi.GetValue(t) is Claim claim)
                if (claim.Type == AppClaimTypes.Super)
                    claims.Add(claim);
        }
        return claims.ToArray();
    }
    /// <summary>
    /// Gets an array of all <see cref="AuthorizationClaims"/> where
    /// Type = <see cref="AppClaimTypes.Email"/>.
    /// </summary>
    /// <returns><see cref="T:Claim[]"/></returns>
    public static Claim[] ToEmailArray()
    {
        Type t = typeof(AuthorizationClaims);
        FieldInfo[] fields = t.GetFields(BindingFlags.Static | BindingFlags.Public);
        List<Claim> claims = new();
        foreach (FieldInfo fi in fields)
        {
            if (fi.GetValue(t) is Claim claim)
                if (claim.Type == AppClaimTypes.Email)
                    claims.Add(claim);
        }

        return claims.ToArray();
    }
    /// <summary>
    /// Gets an array of all <see cref="AuthorizationClaims"/> where
    /// Type = <see cref="AppClaimTypes.AppUser"/>.
    /// </summary>
    /// <returns><see cref="T:Claim[]"/></returns>
    public static Claim[] ToAppUserArray()
    {
        Type t = typeof(AuthorizationClaims);
        FieldInfo[] fields = t.GetFields(BindingFlags.Static | BindingFlags.Public);
        List<Claim> claims = new();
        foreach (FieldInfo fi in fields)
        {
            if (fi.GetValue(t) is Claim claim)
                if (claim.Type == AppClaimTypes.AppUser)
                    claims.Add(claim);
        }
        return claims.ToArray();
    }
    /// <summary>
    /// Gets an array of all <see cref="AuthorizationClaims"/> where
    /// Type = <see cref="AppClaimTypes.Authorization"/>.
    /// </summary>
    /// <returns><see cref="T:Claim[]"/></returns>
    public static Claim[] ToAuthorizationArray()
    {
        Type t = typeof(AuthorizationClaims);
        FieldInfo[] fields = t.GetFields(BindingFlags.Static | BindingFlags.Public);
        List<Claim> claims = new();
        foreach (FieldInfo fi in fields)
        {
            if (fi.GetValue(t) is Claim claim)
                if (claim.Type == AppClaimTypes.Authorization)
                    claims.Add(claim);
        }
        return claims.ToArray();
    }
    /// <summary>
    /// Gets an array of all <see cref="AuthorizationClaims"/> where
    /// Type = <see cref="AppClaimTypes.Credential"/>.
    /// </summary>
    /// <returns><see cref="T:Claim[]"/></returns>
    public static Claim[] ToCredentialArray()
    {
        Type t = typeof(AuthorizationClaims);
        FieldInfo[] fields = t.GetFields(BindingFlags.Static | BindingFlags.Public);
        List<Claim> claims = new();
        foreach (FieldInfo fi in fields)
        {
            if (fi.GetValue(t) is Claim claim)
                if (claim.Type == AppClaimTypes.Credential)
                    claims.Add(claim);
        }
        return claims.ToArray();
    }
    /// <summary>
    /// Gets an array of all <see cref="AuthorizationClaims"/> where
    /// Type = <see cref="AppClaimTypes.Challenge"/>.
    /// </summary>
    /// <returns><see cref="T:Claim[]"/></returns>
    public static Claim[] ToChallengeArray()
    {
        Type t = typeof(AuthorizationClaims);
        FieldInfo[] fields = t.GetFields(BindingFlags.Static | BindingFlags.Public);
        List<Claim> claims = new();
        foreach (FieldInfo fi in fields)
        {
            if (fi.GetValue(t) is Claim claim)
                if (claim.Type == AppClaimTypes.Challenge)
                    claims.Add(claim);
        }
        return claims.ToArray();
    }
}

The authorization claims should be understandable and provide the expected privilege. The AuthorizationClaims class and ToArray methods help to organize and associate claims with users.

AppUser Claims.
AppUser Claims Mobile.

The authorization claims are evaluated by policies. I expect a write claim to include a read privilege and a delete claim to include write and read privileges. Policies are added to the ServicesCollection with the AddAuthorization extension.

If an authorization policy contains multiple authorization requirements, all requirements must pass in order for the policy evaluation to succeed. In other words, multiple authorization requirements added to a single authorization policy are treated on an AND basis.

Program.cs
builder.Services.AddAuthorization(options =>
{
    ...
    options.AddPolicy(AuthorizationPolicies.AppUsersRead, policy =>
    {
        policy.RequireClaim(AppClaimTypes.AppUser);
    });
    options.AddPolicy(AuthorizationPolicies.AppUsersWrite, policy =>
    {
        policy.RequireClaim(AppClaimTypes.AppUser, new string[] { AppClaimValues.Write, AppClaimValues.Delete });
    });
    options.AddPolicy(AuthorizationPolicies.AppUsersDelete, policy =>
    {
        policy.RequireClaim(AppClaimTypes.AppUser, new string[] { AppClaimValues.Delete });
    });
    ...
});

The ASP.NET Core AuthorizeAttribute can restrict access to a page and all page methods by Policy.

namespace UsersWithDevice2FA.Pages.Admin.AppUsers;
[Authorize(Policy = AuthorizationPolicies.AppUsersRead)]
public class IndexModel : PageModel

The AuthorizeAttribute restricts access to all page methods, but more privileged methods can be protected. The AuthorizeAttribute cannot be applied to PageModel methods. Inject the AuthorizationService to the PageModel. Use AuthorizeAsync to verify the requirement. If the requirement is not satisfied the method can throw an exception or redirect to the Access Denied page.

private readonly IUserService _userService;
private readonly IAuthorizationService _authorizationService;
public IndexModel(IUserService userService, IAuthorizationService authorizationService)
{
    _userService = userService;
    _authorizationService = authorizationService;
}
public async Task<IActionResult> OnPostPurgeUsersAsync()
{
    if (!(await _authorizationService.AuthorizeAsync(User, AuthorizationPolicies.AppUsersDelete)).Succeeded)
        return Forbid(); //throw new InvalidOperationException($"Not authorized purge of AppUsers.");

    var allAppUsers = await _userService.GetAppUsers().ToListAsync().ConfigureAwait(false);
    foreach (var appUser in allAppUsers)
        if (!appUser.LoginNameUppercase.Equals("ADMINISTRATOR"))
            if (!await _userService.DeleteAppUserByIdAsync(appUser.Id).ConfigureAwait(false))
                throw new InvalidOperationException($"Error occurred purging users.");

    UsersStatusMessage = "Users purged successfully.";
    return RedirectToPage();
}

Navigation

Navigation to a protected page by a user who does not satisfy the policy requirement is redirected to the Access Denied page.

Program.cs
builder.Services.AddAuthentication(AppSettings.ApplicationScheme)
    .AddCookie(AppSettings.ApplicationScheme, options =>
    {
        options.LoginPath = "/account/login";
        options.LogoutPath = "/account/logout";
        options.AccessDeniedPath = "/account/accessdenied";
        options.ReturnUrlParameter = "returnurl";
        options.SlidingExpiration = true;
        options.Events = new CookieAuthenticationEvents
        {
            OnValidatePrincipal = CookieValidator.ValidateAsync
        };
    })
    .AddCookie(AppSettings.TwoFactorUserIdScheme, options =>
    {
        options.Cookie.Name = AppSettings.TwoFactorUserIdScheme;
        options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
    })
    .AddCookie(AppSettings.TwoFactorRememberMeScheme, options =>
    {
        options.Cookie.Name = AppSettings.TwoFactorRememberMeScheme;
    });
Access Denied.
Access Denied Mobile.

You can avoid Access Denied redirects by excluding navigational controls on the Razor page when a policy's requirement is not satisfied. Inject the AuthorizationService from the ServicesCollection to the Razor page.

_ViewImports.cshtml
...
@using Microsoft.AspNetCore.Authorization;
...
Index.cshtml
@page
@model IndexModel;
@inject IAuthorizationService _authorizationService;

When the page is displayed because the AppUsersRead policy has been satisfied, you can exclude markup when a more privileged policy is not satisfied. The AppUsersWrite policy protects create and edit. The AppUsersDelete policy protects delete.

@if ((await _authorizationService.AuthorizeAsync(User, AuthorizationPolicies.AppUsersWrite)).Succeeded)
{
    <a class="btn btn-primary mb-1 me-2" asp-page="./Create">New</a>
}
Administrator
Administrator AppUser List.
AppUsersRead
Readonly AppUser List.

Email Authorization

Notice the AppUser listing for the AppUsersRead user does not display email addresses.

@if ((await _authorizationService.AuthorizeAsync(User, AuthorizationPolicies.EmailRead)).Succeeded)
{
    <td class="d-none d-xxl-table-cell">
        @Html.DisplayFor(modelItem => item.Email)
    </td>
}

The Hard Parts

How to allow a user with any Authorization claim access the /Admin/Index page.

The Hard Parts TOC

I developed an Authorization class which implements AuthorizationHandler<Authorization>, IAuthorizationRequirement. Authorization would succeed the requirement if the user had any authorization claim in AuthorizationClaims. ToArray(). Authorization is implemented with the AddRequirements extension.

Program.cs
builder.Services.AddAuthorization(options =>
{
    ...
    options.AddPolicy(AuthorizationPolicies.AdminPanelAccess, policy =>
    {
        policy.AddRequirements(new Authorization(AuthorizationClaims.ToArray()));
    });
    ...
});
Authorization.cs
public class Authorization : AuthorizationHandler<Authorization>, IAuthorizationRequirement
{
    private readonly List<Claim> _claimList;
    public Authorization(List<Claim> claimList)
    {
        _claimList = claimList;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, Authorization authorization)
    {
        if (_claimList != null && _claimList.Any() && context.User.Claims.Intersect(_claimList, new AuthorizationClaimComparer()).Any())
        {
            context.Succeed(authorization);
        }
        return Task.CompletedTask;
    }
}

The Intersect extension requires an IEqualityComparer<Claim>. See IEqualityComparer. GetHashCode(Object) Method.

AuthorizationClaimComparer.cs
public class AuthorizationClaimComparer : IEqualityComparer<Claim>
{
    public bool Equals(Claim? x, Claim? y)
    {
        if (x == null && y == null) return true;
        if (x == null || y == null) return false;
        return x.Type == y.Type && x.Value == y.Value && x.ValueType == y.ValueType && x.Issuer == y.Issuer;
    }
    public int GetHashCode(Claim clam)
    {
        return clam.ToString().ToLower().GetHashCode();
    }
}

How to allow a user with an Administrator claim succeed all requirements.

The Hard Parts TOC

I wanted a super user which could access the entire site without any restriction. I added a new requirement to the Authorization handler. The Contains extension requires the AuthorizationClaimComparer.

public class Authorization : AuthorizationHandler<Authorization>, IAuthorizationRequirement
{
    private readonly List<Claim> _claimList;
    public Authorization(List<Claim> claimList)
    {
        _claimList = claimList;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, Authorization authorization)
    {
        if (context.User.Claims.Contains(AuthorizationClaims.Administrator, new AuthorizationClaimComparer()))
        {
            context.Succeed(authorization);
            return Task.CompletedTask;
        }

        if (_claimList != null && _claimList.Any() && context.User.Claims.Intersect(_claimList, new AuthorizationClaimComparer()).Any())
        {
            context.Succeed(authorization);
            return Task.CompletedTask;
        }
        return Task.CompletedTask;
    }
}

How to isolate Administrators from non-Administrators.

The Hard Parts TOC

I did not want a user with the AppUserDelete privilege deleting Administrators. I developed a GetAdminAppUserListAsync method which excludes Administrators when IsAdmin is false. This also allowed a super user which could access the entire site and not list or edit Administrators. I added a Security authorization claim for this super user.

AppUsers/Index OnGet
IsAdmin = User.Claims.Contains(AuthorizationClaims.Administrator, new AuthorizationClaimComparer());
AppUsers = await _userService.GetAdminAppUserListAsync(IsAdmin).ConfigureAwait(false);

How to require two-factor authentication enabled for all policies.

The Hard Parts TOC

The UWD2FAP implements an AppSettings. RequireAdmin2FA option. When set to true, the user must have two-factor authentication enabled to access Admin pages. I added a new requirement to the start of the Authorization handler. If the TwoFactorEnabled user claim's value is false, the handler returns a failed requirement and the policy is denied. The AuthorizationFailureReason is new in .NET 6.0.

if (AppSettings.RequireAdmin2FA)
{
    if (!context.User.HasClaim(c => c.Type == AppClaimTypes.TwoFactorEnabled && c.Value == bool.TrueString &&
        c.ValueType == ClaimValueTypes.Boolean && c.Issuer == AppSettings.ClaimIssuer))
    {
        context.Fail(new AuthorizationFailureReason(authorization, "Two-Factor Authentication must be enabled."));
        return Task.CompletedTask;
    }
}

How to allow a user with any Authorization claim navigate to the /Admin/Index page.

The Hard Parts TOC

I add a nav-item to the Bootstrap navbar if the user is logged in and has any AuthorizationClaims. If the AdminPanelAccess policy is satisfied, the nav-link navigates to the /Admin/Index page. The user will fail to authorize the AdminPanelAccess policy if AppSettings. RequireAdmin2FA is true and the TwoFactorEnabled user claim's value is false. In this case the nav-link displays "Admin (Disabled)" and navigates to the Two-Factor Authentication page in Manage Account. I demonstrate the new AuthorizationFailureReason feature with a nullable enabled Bootstrap tooltip.

_Layout.cshtml
@if (User.Identity!.IsAuthenticated && User.Claims.Intersect(AuthorizationClaims.ToArray(), new AuthorizationClaimComparer()).Any())
{
    var authorizationResult = await _authorizationService.AuthorizeAsync(User, AuthorizationPolicies.AdminPanelAccess).ConfigureAwait(false);
    if (authorizationResult.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"
                data-bs-toggle="tooltip"
                data-bs-placement="bottom"
                title="@(authorizationResult.Failure?.FailureReasons.FirstOrDefault()?.Message ?? "Failure or FailureReasons was not found.")">
                Admin (Disabled)
            </a>
        </li>
    }
}
Admin Disabled.
Admin Disabled Mobile.

Solution for the hard parts.

The Hard Parts TOC

The Authorization handler became SuperAuthorization when I allowed the Super: Administrator and Super: Security claims satisfy the requirement. After researching the RequireClaim method, I implemented claimType and allowedValues parameters with overloaded constructors. See AuthorizationPolicyBuilder. RequireClaim Method.

SuperAuthorization.cs
// Copyright © Ken Haggerty (https://kenhaggerty.com)
// Licensed under the MIT License.
namespace UsersWithDevice2FA.Services.Utilities;
public class SuperAuthorization : AuthorizationHandler<SuperAuthorization>, IAuthorizationRequirement
{
    private readonly string? _claimType;
    private readonly IEnumerable<string>? _allowedValues;
    private readonly IEnumerable<Claim>? _claimArray;
    /// <summary>
    /// Denies authorization when <see cref="AppSettings.RequireAdmin2FA"/> is set true
    /// and the user does not have Two-Factor authentication enabled.
    /// Allows claims where Type = <see cref="AppClaimTypes.Super"/>, satisfy the requirement.
    /// </summary>
    public SuperAuthorization()
    {
    }
    /// <summary>
    /// Denies authorization when <see cref="AppSettings.RequireAdmin2FA"/> is set true
    /// and the user does not have Two-Factor authentication enabled.
    /// Allows claims where Type = <see cref="AppClaimTypes.Super"/>
    /// or any claim in the <paramref name="claimArray"/>, satisfy the requirement.
    /// </summary>
    public SuperAuthorization(IEnumerable<Claim> claimArray)
    {
        _claimArray = claimArray;
    }
    /// <summary>
    /// Denies authorization when <see cref="AppSettings.RequireAdmin2FA"/> is set true
    /// and the user does not have Two-Factor authentication enabled.
    /// Allows claims where Type = <see cref="AppClaimTypes.Super"/>
    /// or any claim where Type = <paramref name="claimType"/>, satisfy the requirement.
    /// </summary>
    public SuperAuthorization(string claimType)
    {
        _claimType = claimType;
    }
    /// <summary>
    /// Denies authorization when <see cref="AppSettings.RequireAdmin2FA"/> is set true
    /// and the user does not have Two-Factor authentication enabled.
    /// Allows claims where Type = <see cref="AppClaimTypes.Super"/>
    /// or any claim where Type = <paramref name="claimType"/> and 
    /// when <paramref name="allowedValues"/> is not null, the Value is in the
    /// <paramref name="allowedValues"/>, satisfy the requirement.
    /// </summary>
    public SuperAuthorization(string claimType, IEnumerable<string>? allowedValues)
    {
        _claimType = claimType;
        _allowedValues = allowedValues;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SuperAuthorization superAuthorization)
    {
        if (AppSettings.RequireAdmin2FA)
        {
            if (!context.User.HasClaim(c => c.Type == AppClaimTypes.TwoFactorEnabled && c.Value == bool.TrueString &&
                c.ValueType == ClaimValueTypes.Boolean && c.Issuer == AppSettings.ClaimIssuer))
            {
                context.Fail(new AuthorizationFailureReason(superAuthorization, "Two-Factor Authentication must be enabled."));
                return Task.CompletedTask;
            }
        }

        var superClaims = AuthorizationClaims.ToSuperArray();
        if (superClaims.Any() && context.User.Claims.Intersect(superClaims, new AuthorizationClaimComparer()).Any())
        {
            context.Succeed(superAuthorization);
            return Task.CompletedTask;
        }

        if (_claimArray != null && _claimArray.Any() && context.User.Claims.Intersect(_claimArray, new AuthorizationClaimComparer()).Any())
        {
            context.Succeed(superAuthorization);
            return Task.CompletedTask;
        }

        if (_claimType != null && _allowedValues == null)
            if (context.User.HasClaim(c => c.Type == _claimType && c.Issuer == AppSettings.ClaimIssuer))
            {
                context.Succeed(superAuthorization);
                return Task.CompletedTask;
            }

        if (_claimType != null && _allowedValues != null && _allowedValues.Any())
            foreach (var value in _allowedValues)
                if (context.User.HasClaim(c => c.Type == _claimType && c.Value == value && c.Issuer == AppSettings.ClaimIssuer))
                {
                    context.Succeed(superAuthorization);
                    return Task.CompletedTask;
                }

        return Task.CompletedTask;
    }
}

The SuperAuthorization handler is easy to implement for a variety of conditions.

Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy(AuthorizationPolicies.Security, policy =>
    {
        policy.AddRequirements(new SuperAuthorization());
    });
    options.AddPolicy(AuthorizationPolicies.AdminPanelAccess, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AuthorizationClaims.ToArray()));
    });
    options.AddPolicy(AuthorizationPolicies.EmailRead, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.Email));
    });
    options.AddPolicy(AuthorizationPolicies.EmailWrite, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.Email, new string[] { AppClaimValues.Write, AppClaimValues.Send }));
    });
    options.AddPolicy(AuthorizationPolicies.EmailSend, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.Email, new string[] { AppClaimValues.Send }));
    });
    options.AddPolicy(AuthorizationPolicies.AppUsersRead, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.AppUser));
    });
    options.AddPolicy(AuthorizationPolicies.AppUsersWrite, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.AppUser, new string[] { AppClaimValues.Write, AppClaimValues.Delete }));
    });
    options.AddPolicy(AuthorizationPolicies.AppUsersDelete, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.AppUser, new string[] { AppClaimValues.Delete }));
    });
    options.AddPolicy(AuthorizationPolicies.AuthorizationRead, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.Authorization));
    });
    options.AddPolicy(AuthorizationPolicies.AuthorizationAssign, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.Authorization, new string[] { AppClaimValues.Assign }));
    });
    options.AddPolicy(AuthorizationPolicies.CredentialsRead, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.Credential));
    });
    options.AddPolicy(AuthorizationPolicies.CredentialsWrite, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.Credential, new string[] { AppClaimValues.Write, AppClaimValues.Delete }));
    });
    options.AddPolicy(AuthorizationPolicies.CredentialsDelete, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.Credential, new string[] { AppClaimValues.Delete }));
    });
    options.AddPolicy(AuthorizationPolicies.ChallengesRead, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.Challenge));
    });
    options.AddPolicy(AuthorizationPolicies.ChallengesDelete, policy =>
    {
        policy.AddRequirements(new SuperAuthorization(AppClaimTypes.Challenge, new string[] { AppClaimValues.Delete }));
    });
});
Ken Haggerty
Created 01/22/22
Updated 03/11/22 19:11 GMT

Log In or Reset Quota to read more.

Successfully completed. Thank you for contributing.
Contribute to enjoy content without advertisments.
Something went wrong. Please try again.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?