3

I think maybe the title is a bit skewed, however, here's my question and objective. I'm developing an application using ASP.NET Core 3.1 MVC. I need to limit user access to certain areas, pages etc. This I've already done within the Startup.cs file and adding the [Authorize] attribute to my administration controller. However, what I cannot seem to figure out is: if an admin removes a users administration privileges while that user is logged in and they attempt to access a secured page, how do I keep them from accessing that page? I know the logical answer is probably to have the user sign out and log back in, however, that's not what is needed in this case.

File Startup.cs (code snip)

public void ConfigureServices(IServiceCollection services)
{
    //Configuration
    services.Configure<HHConfig>(Configuration.GetSection("App"));
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer( 
        Configuration.GetConnectionString("DefaultConnection"))); 
    services.AddIdentity<ApplicationUser, IdentityRole>(options =>
    {
        options.Password.RequiredLength = 8;
        options.Password.RequiredUniqueChars = 1;
        options.SignIn.RequireConfirmedAccount = true;
        })
        .AddRoles<IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();            
        services.AddControllersWithViews();
        services.AddRazorPages(options => {
        options.Conventions.AuthorizeFolder("/Administration");
    });

    services.AddMvc().AddRazorPagesOptions(options =>
    {
        options.Conventions.AddAreaPageRoute("Identity", "/Account/Login", "/Account/Login");
    });

    //Transient and Scoped Services Here
    services.AddTransient<ApplicationDbContext>();
    services.AddScoped<IEmailManager, EmailManager>();
}

Administration Controller

[Authorize(Roles = "Admin")]
public class AdministrationController : Controller
{
    private readonly RoleManager<IdentityRole> roleManager;
    private readonly UserManager<ApplicationUser> userManager;
    private SignInManager<ApplicationUser> signInManager { get; }
    private readonly IEmailManager emailManager;

    public AdministrationController(RoleManager<IdentityRole> roleManager,UserManager<ApplicationUser> userManager,SignInManager<ApplicationUser> signInManager, IEmailManager emailMgr)
    {
        this.roleManager = roleManager;
        this.userManager = userManager;
        this.signInManager = signInManager;
        emailManager = emailMgr;
    }        
}     
Vy Do
  • 46,709
  • 59
  • 215
  • 313
Thanos79
  • 47
  • 7
  • 1
    Unless the app is caching the role data, then it should remove the privileges instantly and the next request would be denied. Is that not your experience? If not, then describe precisely how the app is behaving – ADyson Sep 23 '20 at 16:44
  • @ADyson. When I remove a users admin access from the database (manually) and then refresh the page, the logged in user is still able to access the restricted page. – Thanos79 Sep 23 '20 at 16:50

1 Answers1

3

The issue that you are running into is that the user's claims are stored in a cookie on the user's browser. What you need to do is to ensure that the cookie is updated when the user's claim is updated. One method of doing this is that you can refresh the user's sign-in on critical pages.

This can be accomplished in the following method:

  1. Register a UserClaimsPrincipalFactory so that every time SignInManager sings user in, the claims are created.

     services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, UserClaimService>();
    
  2. Implement a custom UserClaimsPrincipalFactory<TUser, TRole> like below

     public class UserClaimService : UserClaimsPrincipalFactory<ApplicationUser, ApplicationRole>
     {
         private readonly ApplicationDbContext _dbContext;
    
         public UserClaimService(ApplicationDbContext dbContext, UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor)
         {
             _dbContext = dbContext;
         }
    
         public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
         {
             var principal = await base.CreateAsync(user);
    
             // Get user claims from DB using dbContext
    
             // Add claims
             ((ClaimsIdentity)principal.Identity).AddClaim(new Claim("claimType", "some important claim value"));
    
             return principal;
         }
     }
    
  3. Later in your application when you change something in the DB and would like to reflect this to your authenticated and signed in user, following lines achieves this:

     var user = await _userManager.GetUserAsync(User);
     await _signInManager.RefreshSignInAsync(user);
    

This will silently make the user sign in again without any interaction on their part and ensure that they are shown up-to-date content. While this isn't the same question, there are other answers that can assist with this in this question. Which is where I have to give the credit for this answer to.

DCCoder
  • 1,587
  • 4
  • 16
  • 29
  • I'm not sure how to go about getting user claims from the DB using dbcontext? I think my brain isn't working. Would you be able to help clarify that please? – Thanos79 Sep 24 '20 at 14:40
  • Just get what roles that user is associated with from the DBContext – DCCoder Sep 24 '20 at 16:49
  • wouldn't I use UserManager to do that vs DBContext? Maybe I'm misconstruing things here. – Thanos79 Sep 24 '20 at 17:28
  • You can, the concept remains the same – DCCoder Sep 24 '20 at 18:01
  • I guess I'm still not following. The concept of Claims is kind of foreign to me, although I'm learning. I've placed a breakpoint at the AddClaim line, however, it's not adding a claim in the DB. How would you go about getting claims from DBContext? – Thanos79 Sep 24 '20 at 18:40
  • The AddClaim will not modify the database, this will only add the claims to the IdentityUser that is currently signed in. Adding claims to the user in the database is a completely different topic. As far as how to get them from the context you Just pull the AspNetUserRoleClaims where the userid is equal to the user's id, and then pull the roles based on the role id. – DCCoder Sep 24 '20 at 18:43