Search code examples
c#asp.net-coreasp.net-core-identity

ASP.NET Core Identity Authorization using Parameter for Team Membership


I have an application with tables like this:

  • User (ASP.NET Core Identity)
  • Team
  • UserTeam (many-to-many join table)

Users can be members of multiple teams, and can have different Roles within the teams. For example a User may be a TeamAdmin for TeamA but just a normal member of TeamB. Because of this, simple Role checks and Policies defined with static values won't work.

I'm looking for a way to authorize Controller Actions for Users based on their Team and their role in the Team, which will either be added as a Claim or with a separate RoleTeam table. Assuming a Controller action like this:

[HttpGet]
[Authorize(???)]
public IActionResult ManageTeam(Guid teamId)
{ }

I would need to verify that the User has the TeamAdmin Role for the Team in question. Is there a clean way to decorate it with an Authorize attribute that can access the teamId parameter? Or will I have to wrap the guts of all of these Actions in if (User.IsTeamAdmin(teamId) ... statements?


Solution

  • ASP.NET Core introduces the concept of policies that you can apply to your Authorize attribute. works like filters, but without writing filters.

    Each policy has one or more requirements that must all be met for the policy to pass. The Microsoft docs have a good example of setting up policies. In your case I'd do something like the following:

    First, start with a "requirement"

    public class TeamAccessRequirement : IAuthorizationRequirement
    {
    }
    

    Then add a requirement handler

    public class TeamAccessHandler : AuthorizationHandler<TeamAccessRequirement>
    {
        private readonly DbContext dbContext;
    
        public TeamAccessHandler(DbContext dbContext)
        {
            // example of an injected service. Put what you need here.
            this.dbContext = dbContext;
        }
    
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TeamAccessRequirement requirement)
        {
            // pattern matching is cool. if you can't do this, use context.Resource as AuthorizationFilterContext before and check for not null
            if (context.Resource is AuthorizationFilterContext authContext)
            {
                // you can grab the team id, (but no model builder to help you, sorry)
                var teamId = Guid.Parse(authContext.RouteData.Values["teamId"]);
    
                // now you can do the code you would have done in the guts of the actions.
                if (context.User.IsTeamAdmin(teamId))
                {
                    context.Succeed(requirement);
                }
                else
                {
                    context.Fail();
                }
            }
    
            return Task.CompletedTask;
        }
    }
    

    Then, you need to put this all together and enable it in the Startup.cs under ConfigureServices, like this:

        services.AddAuthorization(options =>
        {
            options.AddPolicy("HasAdminTeamAccess", policy =>
                policy.Requirements.Add(new TeamAccessRequirement()));
        });
    
        services.AddTransient<IAuthorizationHandler, TeamAccessHandler>();
    

    And finally, the usage:

    [HttpGet]
    [Authorize(Policy = "HasAdminTeamAccess")]
    public IActionResult ManageTeam(Guid teamId)
    { }
    

    Now your actions remain nice and clean. From here you can fine tune the policy by adding functionality to the requirements that you can call from the handler, or do whatever you want.