Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core policy-based authentication not working

I have to manage our user's roles in our local database, not in Azure AD. But, I also need policy-based authorization for controllers because we have both admin and customer areas.

To handle this, I added an authorization filter that loads the user's role from either Session or the database, adds an Identity to the Principal, and then moves along. This Identity adds an appropriate Role Claim.

Before leaving the authorization filter, the IsInRole returns true as expected, and there are two Identities.

My authorization filter looks like this:

public class MyAuthFilter : IAsyncAuthorizationFilter
{
    private readonly IUserService userService;

    public MyAuthFilter(IUserService userService)
    {
        this.userService = userService;
    }

    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        var user = context.HttpContext.User;

        if (user.Identity.IsAuthenticated)
        {
            AuthUserViewModel authUserViewModel;

            var sessionViewModelJson = context.HttpContext.Session.GetString(user.AzureObjectId());
            if (string.IsNullOrEmpty(sessionViewModelJson))
            {
                authUserViewModel = await ConstructSessionViewModel(context);
            }
            else
            {
                authUserViewModel = JsonConvert.DeserializeObject<AuthUserViewModel>(sessionViewModelJson);
            }

            user.AddIdentity(authUserViewModel?.Role);
        }
    }

    private async Task<AuthUserViewModel> ConstructSessionViewModel(AuthorizationFilterContext context)
    {
        var user = context.HttpContext.User;
        var parsedObjectId = Guid.Parse(user.AzureObjectId());

        var findUserResult = await userService.FindByAzureObjectId(new FindByAzureObjectIdRequest
        {
            AzureObjectId = parsedObjectId
        });

        if (findUserResult.Success)
        {
            var userModel = findUserResult.User;

            var viewModel = new AuthUserViewModel
            {
                AzureObjectId = parsedObjectId,
                UserId = userModel.Id,
                SchoolId = userModel.SchoolId.GetValueOrDefault(),
                Name = userModel.Name,
                Email = userModel.Email,
                PhoneNumber = userModel.PhoneNumber,
                Role = userModel.Role
            };

            context.HttpContext.Session.SetString(user.AzureObjectId(), JsonConvert.SerializeObject(viewModel));

            return viewModel;
        }

        return null;
    }
}

That AddIdentity extension method looks like this:

public static void AddIdentity(this ClaimsPrincipal principal, string role)
{
    if (string.IsNullOrWhiteSpace(role) || principal.IsInRole(role))
    {
        return;
    }

    switch (role)
    {
        case Roles.School:
            principal.AddIdentity(new SchoolIdentity());
            break;
        case Roles.Admin:
            principal.AddIdentity(new AdminIdentity());
            break;
    }
}

and in this case, the SchoolIdentity is what gets added, and it looks like this:

public class SchoolIdentity : ClaimsIdentity
{
    public SchoolIdentity()
    {
        AddClaim(new SchoolPortalClaim());
    }
}

and finally, the SchoolPortalClaim looks like this:

public class SchoolPortalClaim : Claim
{
    public SchoolPortalClaim() : base(ClaimTypes.Role, "School")
    {
    }

    public SchoolPortalClaim(BinaryReader reader) : base(reader)
    {
    }

    public SchoolPortalClaim(BinaryReader reader, ClaimsIdentity subject) : base(reader, subject)
    {
    }

    protected SchoolPortalClaim(Claim other) : base(other)
    {
    }

    protected SchoolPortalClaim(Claim other, ClaimsIdentity subject) : base(other, subject)
    {
    }

    public SchoolPortalClaim(string type, string value) : base(type, value)
    {
    }

    public SchoolPortalClaim(string type, string value, string valueType) : base(type, value, valueType)
    {
    }

    public SchoolPortalClaim(string type, string value, string valueType, string issuer) : base(type, value, valueType, issuer)
    {
    }

    public SchoolPortalClaim(string type, string value, string valueType, string issuer, string originalIssuer) : base(type, value, valueType, issuer, originalIssuer)
    {
    }

    public SchoolPortalClaim(string type, string value, string valueType, string issuer, string originalIssuer, ClaimsIdentity subject) : base(type, value, valueType, issuer, originalIssuer, subject)
    {
    }
}

The issue comes when the policy executes:

services.AddAuthorization(options =>
{
    options.AddPolicy(Policies.School,
        policy => policy.RequireAssertion(
            context => context.User.IsInRole(Roles.School)));
});

The context.User does not have the Identity that was added by the authorization filter.

How do I get this to move downstream?

The Controller in question looks like this:

[Area(Areas.School)]
[Authorize(Policy = Policies.School)]
public class HomeController : BaseController
{
    public HomeController(IUserService userService) :
        base(userService)
    {
    }

    public IActionResult Index()
    {
        return RedirectToAction("Index", "Presentation", new {Area = "School"});
    }
}
like image 958
Mike Perrenoud Avatar asked Sep 18 '25 03:09

Mike Perrenoud


1 Answers

The fundamental issue here is that the RequireAssertion callback gets invoked before IAsyncAuthorizationFilter.OnAuthorizationAsync, which means the ClaimsIdentity you add in OnAuthorizationAsync has not been added at the time you need it.

Instead of using a custom authz filter, you can turn to a custom implementation of IClaimsTransformation, which declares a TransformAsync method. This method takes the current ClaimsPrincipal and allows you to return either the same ClaimsPrincipal or a new one, according to your needs.

Here's a skeleton example:

public class MyClaimsTransformation : IClaimsTransformation
{
    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        // Your existing logic to add the relevant ClaimsIdentity.
        // You might want to check if the ClaimsPrincipal already contains either
        // SchoolIdentity or AdminIdentity here, as this operation may run
        // more than once.
        // ...
    }
}

To register this implementation, use something like this, in ConfigureServices:

services.AddSingleton<IClaimsTransformation, MyClaimsTransformation>();
like image 74
Kirk Larkin Avatar answered Sep 20 '25 18:09

Kirk Larkin