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

ASP.NET Core MVC check Authorization on post back


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;
    }        
}     

Solution

  • 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.