joshcomley
joshcomley

Reputation: 28818

ASP.NET 5 Identity - refresh roles for a user

In ASP.NET 5, suppose I have the following controller:

[Route("api/[controller]")]
[Authorize(Roles = "Super")]
public class ValuesController : Controller
{
    // GET: api/values
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new[] { "value1", "value2" };
    }
}

Then suppose I have a user, Jack, who has logged in but is not a member of the Super role.

When Jack tries to access this resource he gets access denied, as expected.

Then, from another machine, I use the UserManager to add him to the Super role.

When Jack tries again to access this resource, it is still access denied until he logs out and logs back in again.

How can I make sure that the roles for each user are either not cached, or are refreshed somehow when I make a change?

Upvotes: 3

Views: 724

Answers (1)

joshcomley
joshcomley

Reputation: 28818

So here's how I managed to solve this in the end, but really this should be solved with security stamps. The solution below at least provides a "look in" before authorisation happens.

I created a new AuthorizationHandler that accepts a UserManager in its constructor:

public class SuperRequirement : AuthorizationHandler<SuperRequirement>, IAuthorizationRequirement
{
    public UserManager<ApplicationUser> UserManager { get; set; }

    public SuperRequirement(UserManager<ApplicationUser> userManager)
    {
        UserManager = userManager;
    }

    protected override void Handle(AuthorizationContext context, SuperRequirement requirement)
    {
        throw new System.NotImplementedException();
    }

    protected override async Task HandleAsync(AuthorizationContext context, SuperRequirement requirement)
    {
        var userName = context.User.Identity.Name;
        var pass = await UserManager.IsInRoleAsync(await UserManager.FindByNameAsync(userName), "Super");
        if (pass)
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail();
        }
    }
}

I then configured this in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    ...
    var sp = services.BuildServiceProvider();
    //var service = sp.GetService<ISomeService>();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("Super",
            policy =>
            {
                var userManager = sp.GetService<UserManager<ApplicationUser>>();
                policy.Requirements.Add(new SuperRequirement(userManager));
            });
    });
}

And finally I updated my controller to use the new authorization policy:

[Route("api/[controller]")]
[Authorize(Policy = "Super")]
public class ValuesController : Controller
{
    // GET: api/values
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new[] { "value1", "value2" };
    }
}

This way it checks the database each request to ensure the roles align, using the UserManager to do this.

Please note this is less efficient as each API request will now trigger extra calls to the database, so you should implement some kind of caching and invalidation mechanism; this is just to demonstrate the principle.

Upvotes: 1

Related Questions