0
votes

I am trying to add two policies to asp.net core api.

  1. ReadOnly - only allow get calls
  2. AppUser - allow get,put,post,delete

//// policies are in appsettings json

Policies.json
{
    "name": "ReadOnly"
    "ids":["user1", "user2"],
}
,
{
    "name": "AppUser"
    "ids":["user3", "user4"]
}

//// requirement

public class IdRequirement : IAuthorizationRequirement
{
    public string Name { get; set; }
    public List<string> Ids { get; set; } = new List<string>();

    public IdRequirement(string name, List<string> ids)
    {
        Name = name;
        Ids = ids;
    }
}

//// handler

public class IdHandler : AuthorizationHandler<IdRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IdRequirement requirement)
    {
        if (context.User.Identity.IsAuthenticated) 
        {
            string currentUserid = GetId(context.User);

            if (requirement.Ids != null && requirement.Ids.Any(x => x == currentUserid))
            {
                context.Succeed(requirement);
            }
        }

        return Task.CompletedTask;
    }
}

//// in services

services.AddAuthorization(options =>
{
    var reqList = configuration.GetSection("Policies").Get<List<IdRequirement>>();

    //// app users must have readonly access by default. ReadOnly is the default policy.
    //// not sure this is the correct way?
    
    var appUserReq = reqList.Find(x => x.Name.Equals("AppUser", StringComparison.Ordinal));
    var readOnlyReq = reqList.Find(x => x.Name.Equals("ReadOnly", StringComparison.Ordinal));

    readOnlyReq.Ids = readOnlyReq.Ids.Union(appUserReq.ids).ToList();

    reqList.ForEach(requirement =>
    {
        options.AddPolicy(requirement.Name, policy =>
        {
            policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
            policy.Requirements.Add(requirement);

        });
    });
});

//// in configure

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers().RequireAuthorization("ReadOnly");
});

//// controller

public class TestController : BaseController
{
    // GET: userid
    [HttpGet("testget")]
    public async Task<IActionResult> GetSomething()
    {
        return Ok();
    }

    [Authorize(Policy = "AppUser")]
    [HttpPost("testpost")]
    public async Task<IActionResult> PostSomething()
    {
        return Ok();
    }
}

If I call testget, it will only authorize readonly requirement. Which is correct for me.

If I call testpost, it will first authorize appuser requirement, then again call readonly requirement.

I don't want to trigger readonly requirement when using [Authorize(Policy = "AppUser")]

  1. Am I missing here or misuse concept?
  2. Is this default behavior?

I referred this article https://github.com/blowdart/AspNetAuthorizationWorkshop

2

2 Answers

1
votes

You are not missing anything. When putting [Authorize(Policy = "AppUser")] on an action, it will authenticate through this policy AppUser first. If not succeed, it will redirect to AccessDeniedPath. At this time, it will go through the endpoint, and RequireAuthorization as a global authorization in the endpoint will be triggered again. If you do not configure the AccessDeniedPath, it will return 404.

0
votes

Finally using help from this post, I achieved like below. Make sure that you do not need add Authorize attribute anymore in controllers. And can modify RequireAuthorizationForHttpMethods if you want any custom setting for particular controller.

var mutatingHttpMethods = new HashSet<HttpMethod>()
{
    HttpMethod.Post,
    HttpMethod.Put,
    HttpMethod.Delete
};

var getHttpMethods = new HashSet<HttpMethod>()
{
    HttpMethod.Get
};

endpoints
    .MapControllers()
    .RequireAuthorizationForHttpMethods(httpMethods => httpMethods.Any(httpMethod => getHttpMethods.Contains(httpMethod)), "ReadOnly")
    .RequireAuthorizationForHttpMethods(httpMethods => httpMethods.Any(httpMethod => mutatingHttpMethods.Contains(httpMethod)), "AppUser");