Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"context.Resource as AuthorizationFilterContext" returning null in ASP.NET Core 3.0

Tags:

c#

asp.net

I am trying to implement a custom authorization requirement following a tutorial. It seems like context.Resource no longer contains AuthorizationFilterContext and as a result:

var authFilterContext = context.Resource as AuthorizationFilterContext;

returns null and the rest of the logic fails. I am also not able to get the query string value because it is null. The following is the code:

public class CanEditOnlyOtherAdminRolesAndClaimsHandler :
   AuthorizationHandler<ManageAdminRolesAndClaimsRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
       ManageAdminRolesAndClaimsRequirement requirement)
        {
            var authFilterContext = context.Resource as AuthorizationFilterContext;
            if (authFilterContext == null)
            {
                return Task.CompletedTask;
            }

            string loggedInAdminId =
                context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value;

            string adminIdBeingEdited = authFilterContext.HttpContext.Request.Query["userId"];

            if (context.User.IsInRole("Admin") &&
                context.User.HasClaim(claim => claim.Type == "Edit Role" && claim.Value == "true") &&
                adminIdBeingEdited.ToLower() != loggedInAdminId.ToLower())
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

How should I fix this problem in ASP.NET Core 3.0?

like image 405
Giorgi Aptsiauri Avatar asked Dec 05 '19 14:12

Giorgi Aptsiauri


2 Answers

This is due to the new endpoint routing in .NET Core 3.0.

To quote the below ticket.

This is because when using endpoint routing in ASP.NET Core 3.0:

Mvc will no longer add AuthorizeFilter to ActionDescriptor and ResourceInvoker will not call AuthorizeAsync() https://github.com/aspnet/AspNetCore/blob/90ab2cb965aeb8ada13bc4b936b3735ca8dd28df/src/Mvc/Mvc.Core/src/ApplicationModels/AuthorizationApplicationModelProvider.cs#L40

Mvc will add all Filter as metadata to endpoint.Metadata https://github.com/aspnet/AspNetCore/blob/5561338cfecac5ca4b1dda2f09a7f66153d0b5fe/src/Mvc/Mvc.Core/src/Routing/ActionEndpointFactory.cs#L348

instead by AuthorizationMiddleware call the AuthorizeAsync() and resouorce is Endpoint https://github.com/aspnet/AspNetCore/blob/5561338cfecac5ca4b1dda2f09a7f66153d0b5fe/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs#L63

New method.

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CookieOrTokenAuthorizationRequirement requirement)
{
    if (context.Resource is Endpoint endpoint)
    {
        if (endpoint.Metadata.OfType<IFilterMetadata>().Any(filter => filter is MyFilter))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }
    }
}

https://github.com/dotnet/aspnetcore/issues/11075

It's worth noting too that using the new context you won't be able to access route data as you were before with the AuthorizationFilterContext. You will need to inject an IHttpContextAccessor into the AuthorizationHandler.

// Ensure your handler is registered as scoped
services.AddScoped<IAuthorizationHandler, InvestorRequirementHandler>();


public class InvestorRequirementHandler : AuthorizationHandler<InvestorRequirement>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public InvestorRequirementHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, InvestorRequirement requirement)
    {
        var tenant = httpContextAccessor.HttpContext.GetRouteData().Values[ExceptionHandlerMiddleware.TenantCodeKey].ToString();
    }
}
like image 93
CountZero Avatar answered Oct 18 '22 01:10

CountZero


public class CanEditOnlyOtherAdminRolesAndClaimsHandler :
   AuthorizationHandler<ManageAdminRolesAndClaimsRequirement>
    {
       private readonly IHttpContextAccessor httpContextAccessor;

       public CanEditOnlyOtherAdminRolesAndClaimsHandler(IHttpContextAccessor httpContextAccessor)
       {
            this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
       }

       protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
       ManageAdminRolesAndClaimsRequirement requirement)
        {
            if (context.User == null || !context.User.Identity.IsAuthenticated)
            {
                context.Fail();
                return Task.CompletedTask;
            }

            string loggedInAdminId =
                context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value;

            string adminIdBeingEdited = httpContextAccessor.HttpContext.Request.Query["userId"].ToString();

            if (context.User.IsInRole("Admin") &&
            context.User.HasClaim(claim => claim.Type == "Edit Role" && claim.Value == "true") &&
            adminIdBeingEdited.ToLower() != loggedInUserId.ToLower())
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }
}

Then add the following services to ConfigureServices method in Startup class:

 public void ConfigureServices(IServiceCollection services)
        {           
          services.AddAuthorization(options =>
            {
             options.AddPolicy("ManageRolesPolicy", policy => policy.Requirements.Add(new ManageAdminRolesAndClaimsRequirement()));
            }

          services.AddScoped<IAuthorizationHandler, CanEditOnlyOtherAdminRolesAndClaimsHandler>();       
      }

If you want to handle a multiple custom authorization for a requirement:

in CanEditOnlyOtherAdminRolesAndClaimsHandler class you check if the user is in Admin role and has Edit Role claim. Let's suppose that you require that the user must to be in Super Admin role, in this scenario, you can either:

- edit the condition in CanEditOnlyOtherAdminRolesAndClaimsHandler class to be as a following:

if (context.User.IsInRole("Admin") &&
                context.User.HasClaim(claim => claim.Type == "Edit Role" && claim.Value == "true") &&
                adminIdBeingEdited.ToLower() != loggedInUserId.ToLower() || 
                context.User.IsInRole("Super Admin") && adminIdBeingEdited.ToLower() != loggedInUserId.ToLower())
                {
                    context.Succeed(requirement);
                }

- or customize another authorization handler for the new requirement which in this case is Super Admin role:

Create a new class and name it ManageRolesAndClaimsSuperAdminHandler, the implementation of this class should be as follows

public class ManageRolesAndClaimsSuperAdminHandler : AuthorizationHandler<ManageAdminRolesAndClaimsRequirement>
    {
        private readonly IHttpContextAccessor httpContextAccessor;

        public ManageUsersRolesSuperAdminHandler(IHttpContextAccessor httpContextAccessor)
        {
            this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        }

        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ManageAdminRolesAndClaimsRequirement  requirement)
        {
        if (context.User == null || !context.User.Identity.IsAuthenticated)
            {
                context.Fail();
                return Task.CompletedTask;
            }

            string loggedInAdminId = context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value;

            string adminIdBeingEdited = httpContextAccessor.HttpContext.Request.Query["userId"].ToString();

            if (context.User.IsInRole("Super Admin") && adminIdBeingEdited.ToLower() != loggedInUserId.ToLower())
            {
                context.Succeed(requirement);
            }
            return Task.CompletedTask;
        }
    }

Now register the new handler in ConfigureServices method in Startup class

public void ConfigureServices(IServiceCollection services)
      {        
          services.AddAuthorization(options =>
            {
             options.AddPolicy("ManageRolesPolicy", policy => policy.Requirements.Add(new ManageAdminRolesAndClaimsRequirement()));
            }

          services.AddScoped<IAuthorizationHandler, CanEditOnlyOtherAdminRolesAndClaimsHandler>();
          services.AddScoped<IAuthorizationHandler, ManageRolesAndClaimsSuperAdminHandler>();
      }
like image 41
Husam Ebish Avatar answered Oct 18 '22 02:10

Husam Ebish