Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 904
Valuator Avatar asked Jan 22 '18 17:01

Valuator


People also ask

How does Authorize attribute work in ASP.NET Core?

Authorization in ASP.NET Core is controlled with AuthorizeAttribute and its various parameters. In its most basic form, applying the [Authorize] attribute to a controller, action, or Razor Page, limits access to that component to authenticated users. Now only authenticated users can access the Logout function.

Can we use MemberShip in .NET Core?

MemberShip is not compatible in asp.net core, so you cannot use it. If you don't want to migrate the database schema, you can use Identity instead.


1 Answers

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.

like image 54
Balah Avatar answered Oct 23 '22 15:10

Balah