How to get user input parameters in my authorization policy requirement in ASP.NET Core 5.0?

Viewed 713

I want to authorize users to see only their own resources (e.g: Audits entity). So in the AuditController I have:

[MyAuthorize(Policy = nameof(ValidUserToSeeAuditAuthorizationHandler))]
[HttpGet]
public async Task<JsonResult<AuditView>> GetByIdAsync(Guid id)
{
    // my business to fetch the audit info based by its id
    // ...      
    return result;
}

Then I created my Requirement and AuthorizationHandler classes:

public class ValidUserToSeeAuditRequirment : IAuthorizationRequirement
{
    public ValidUserToSeeAuditRequirment(Guid auditId)
    {
        auditId = auditId;
    }


    public Guid AuditId { get; }
}

public class ValidUserToSeeAuditAuthorizationHandler : AuthorizationHandler<ValidUserToSeeAuditRequirment>
{
    private readonly AppUserManager _userManager;
    private readonly IUnitOfWork _appDbContext;

    public ValidUserToSeeAuditAuthorizationHandler(AppUserManager userManager, IUnitOfWork appDbContext)
    {
        _userManager = userManager;
        _appDbContext = appDbContext;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidUserToSeeAuditRequirment requirement)
    {
        if (!context.User.IsAuthenticated())
        {
            context.Fail();
            return Task.CompletedTask;
        }

        var theAudit = _appDbContext.Set<Audit>().SingleOrDefault(x => x.Id == requirement.AuditId);
        var authenticatedUserId = Convert.ToInt32(context.User.GetSubjectId());
                    
        // If the authenticated user created the audit, then he/she is valid to see it
        if (theAudit.SubjectauthenticatedUserId == authenticatedUserId)
        {
            // valid
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        // he/she is not authorized to see the resource (audit)
        context.Fail();
        return Task.CompletedTask;
    }
}

But in the Startup class I want to configure authorization policies. How do I configure my Requirement class to get the user input parameters from the controller's action method?

services.AddAuthorization(options =>
{
    // another policies
    // ...
    
    options.AddPolicy(name: nameof(ValidUserToSeeAuditAuthorizationHandler),
        policy =>
        {
            policy.RequireAuthenticatedUser();
            policy.AddRequirements(new ValidUserToSeeAuditRequirment( /****** HERE, how to pass the controller action method parameters ******/));
        });
});

services.AddTransient<IAuthorizationHandler, ValidUserToSeeAuditAuthorizationHandler>();
3 Answers

You can customize a AuthorizationPolicy provider to get the parameter.

public class CustomAuthorizepolicyProvider: DefaultAuthorizationPolicyProvider
{
    public CustomAuthorizepolicyProvider(IOptions<AuthorizationOptions> options):base(options)
    {

    }
    public override Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        
        if (policyName=="[specified plicyname]")
            {
                var authorizePolicy = new AuthorizationPolicyBuilder();
                authorizePolicy.AddRequirements(new ValidUserToSeeAuditRequirment(/* give the parameter*/)).Build();
                return Task.FromResult(authorizePolicy);
            }
        return base.GetPolicyAsync(policyName);
    }
}

Inject into startup. Note: it is a singleton.

services.AddSingleton<IAuthorizationPolicyProvider,CustomAuthorizepolicyProvider>();

I ended up with this solution:

/// <summary>
/// 
/// </summary>
public class ValidUserToSeeAuditRequirment : IAuthorizationRequirement
{
    
}

/// <summary>
/// Only an Admin and the authorized user can see the Audit
/// </summary>
public class ValidUserToSeeAuditAuthorizationHandler : AuthorizationHandler<ValidUserToSeeAuditRequirment>
{
    private readonly IHttpContextAccessor _httpContextAccessor;

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

protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidUserToSeeAuditRequirment requirement)
{
    // If he has the Admin Role, then he can see the Audit
    if (context.User.HasClaim(x => x.Type.ToUpperInvariant() == "ROLE" && x.Value.ToUpperInvariant() == "ADMIN"))
    {
        context.Succeed(requirement);
        return;
    }

    // Get the audit id from the Routing
    var auditIdFromRoute = _httpContextAccessor.HttpContext.GetRouteData()?.Values["id"].ToString();
    if (auditIdFromRoute is null || !Guid.TryParse(auditIdFromRoute, out Guid requestingAuditId))
    {
        context.Fail();
        return;
    }

    // get the authenticated user
    var userId = Convert.ToInt32(context.User.GetSubjectId());
        
    // check if the user has authorized to see the audit
    if(isUserAllowToSeeAudit(userId, requestingAuditId))
    {
        context.Succeed(requirement);
        return;
    }
    
    context.Fail();
}

private bool isUserAllowToSeeAudit(int userId, Guid auditId)
{
    // ...
}

I would suggest you used the IAuthorizationRequirement and AuthorizationHandler approach. Instances of AuthorizationHandler (where T is the requirement) are registered as singletons in your startup. As such, you can inject an IHttpRequestAccessor into the handler, giving it the capability of accessing the request. This ends up being something like this

    public class YourAuthorizationHandler : AuthorizationHandler<YourAuthorizationRequirement>
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    YourAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
   {
      _httpContextAccessor = httpContextAccessor;
   }

   protected async override Task HandleRequirementAsync(AuthorizationHandlerContext context, YourAuthorizationRequriement requirement)
   {
       var userIdInClaim = context.User.Claims.Where(claim => claimType == NameIdentifier).FirstOrDefault();
       var request = _httpContextAccessor.HttpContext.Request;
       request.EnableBuffering(); // allows the request to be read again

       // read the request from request.Body assuming an HTTP POST. It will depend.
       // do your logic checking the content here.
       return context.Succeed(requirement); // Assuming things are what you want.

   }
}

This is less complicated than it seems.

Read this Introduction to authorization in ASP.NET Core

Related