Azure AD Authentication with Custom Authorization in ASP.Net Core 6
Asked Answered
M

1

7

I have implemented the Azure AD Authentication in ASP.Net Core 6 using the standard way and have used the [Authorize] attribute on top of the controller class. All these are working fine.

builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration, "AzureAd");

Apart from the Authentication, I am trying to build the custom Authorization using TypeFilterAttribute class. Code snippet below:

public class CustomAuthorizeAttribute : TypeFilterAttribute
{
    public CustomAuthorizeAttribute(params Roles[] roles) : base(typeof(CustomAuthorizeFilter))
    {
        Roles[] _roles = roles;
        Arguments = new object[] { _roles };
    }
}

public class CustomAuthorizeFilter : IAuthorizationFilter
{
    private readonly Roles[] _roles;
    private readonly IUserService _userService;

    public CustomAuthorizeFilter(Roles[] roles, IUserService userService)
    {
        _roles = roles ?? throw new UnauthorizedAccessException("OnAuthorization : Missing role parameter");
        _userService = userService;
    }

    public async void OnAuthorization(AuthorizationFilterContext context)
    {
        if (context != null && context.HttpContext != null &&
            context.HttpContext.User != null &&
            context.HttpContext.User.Identity != null)
        {
            string? userEmailId = context.HttpContext.User.Identity.Name;
            if (string.IsNullOrEmpty(userEmailId))
            {
                context.Result = new ContentResult()
                {
                    Content = "OnAuthorization : Invalid User : Email Id is not present",
                    StatusCode = 401
                };

                return;
            }

            var userDetails = await _userService.GetUserProfile(userEmailId);

            if (userDetails == null)
            {
                context.Result = new ContentResult()
                {
                    Content = "OnAuthorization : Invalid User : User does not exist",
                    StatusCode = 403
                };
            }
        }
    }
}

Usage: For each controller action method, I would like to the Roles who can access the Api. In the below code snippet, Roles is an enum. Basically, I am trying to implement the Role based access for each of the Api.

[CustomAuthorize(Roles.Admin1,Roles.Admin2)]

Issue: The CustomAuthorizeAttribute is getting called. But, the controller action is being invoked ir-respective of the Authentication within the CustomAuthorizeAttribute.

what is that I am missing here?

Muleteer answered 10/8, 2022 at 15:6 Comment(2)
Have you registered CustomAuthorizeAttribute in Program.cs file? If you have, pls delete it.Lonne
@JasonPan No, I have not registered thatMuleteer
L
4

The issue

The problem is the method itself, you are using a call to an async method

var userDetails = await _userService.GetUserProfile(userEmailId);

That forced you to change to method signature to:

public async void OnAuthorization(AuthorizationFilterContext context)

Since the call to this method cannot be awaited (you cannot change it to public async Task OnAuthorization(AuthorizationFilterContext context)) the framework never executes the code after the await in time. The evaluation of if (userDetails == null) is not performed in time.

Demonstration of the issue by providing a dirty fix

If you change the line

var userDetails = await _userService.GetUserProfile(userEmailId);

to

var userDetails = _userService.GetUserProfile(userEmailId).Result;

you will notice the authentication is performed correctly. However, using .Result is not best practice and is not recommended. So either change GetUserProfile to a synchronous function or use an authorization mechanism that support Task based methods, allowing you to properly await methods.

Solution

There is an IAsyncAuthorizationFilter interface that supports Task based methods, so use that one. It takes minimal code changes:

public class CustomAuthorizeFilter : IAsyncAuthorizationFilter
{
    private readonly Roles[] _roles;
    private readonly IUserService _userService;

    public CustomAuthorizeFilter(Roles[] roles, IUserService userService)
    {
        _roles = roles ?? throw new UnauthorizedAccessException("OnAuthorization : Missing role parameter");
        _userService = userService;
    }

    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        if (context != null && context.HttpContext != null &&
            context.HttpContext.User != null &&
            context.HttpContext.User.Identity != null)
        {
            string? userEmailId = context.HttpContext.User.Identity.Name;
            if (string.IsNullOrEmpty(userEmailId))
            {
                context.Result = new ContentResult()
                {
                    Content = "OnAuthorization : Invalid User : Email Id is not present",
                    StatusCode = 401
                };

                return;
            }

            var userDetails = await _userService.GetUserProfile(userEmailId);

            if (userDetails == null)
            {
                context.Result = new ContentResult()
                {
                    Content = "OnAuthorization : Invalid User : User does not exist",
                    StatusCode = 403
                };
            }
        }
    }
}

Do mind: in .Net Core there are new ways introduced to perform authorization, see the docs. For example, you could use Policy Based or Role Based authorization that also natively supports Task based methods allowing you to use async methods in your custom policies.

References

Lenoralenore answered 18/8, 2022 at 7:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.