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

How to customize role retrieval for Authorization in ASP.NET Core Identity when not using Entity Framework?


My team is converting apps from old AS400 applications to ASP.NET Core but still using the DB/2 tables (not Entity Framework). Especially for role access as we have a few tables as our central point for defining user roles and access, which is used concurrently between old RGP apps, and new .Net apps. I'm using this article as a reference for using ASP.NET Core Identity without EF.

I'm trying to figure out how to customize and override how the roles for a specific user is collected; I was hoping I could override something in the RoleStore class, but that doesn't seem to be the case because I put a breakpoint in each method, and never gets hit.

I did see this question, and tried the top answer, but the overridden IsInRole method never gets hit.

I currently am using [Authorize(Roles = "Administrator")] on top of my home controller for testing. I also have a global role policy filter setup, using AuthorizationPolicyBuilder with the RequireRole("~") method (I've tested both separately for the CustomClaimsPrincipal as well). I plan to eventually use the [Authorize(Policy="")] for role checks at the controller level instead of [Authorize(Role="")]. But for now I'm just trying to figure out how to customize the role access from the database and see how it is accessed for both role and policy based authorization. I am currently targeting .Net 5.0.

Below is my pretty basic startup.cs page attempting to setup a CustomClaimsPrincipal to override the IsInRole method:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<IUserStore<ApplicationUser>, UserStore>();
        services.AddTransient<IRoleStore<ApplicationRole>, RoleStore>();
        services.AddIdentity<ApplicationUser, ApplicationRole>()
                .AddDefaultTokenProviders();
        services.AddControllersWithViews(
            config =>
            {
                var policy = new AuthorizationPolicyBuilder();
                policy.RequireRole("Test");
                config.Filters.Add(new AuthorizeFilter(policy.Build()));
            }
        );
        services.AddTransient<IClaimsTransformation, ClaimsTransformer>();

    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

public class CustomClaimsPrincipal : ClaimsPrincipal
{
    public CustomClaimsPrincipal(IPrincipal principal) : base(principal)
    { }

    public override bool IsInRole(string role)
    {
        // ...
        return base.IsInRole(role);
    }
}

public class ClaimsTransformer : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var customPrincipal = new CustomClaimsPrincipal(principal) as ClaimsPrincipal;
        return Task.FromResult(customPrincipal);
    }
}

Solution

  • Well, looking into it some more, this article by Microsoft demonstrates how to create custom authorization handlers.

    These two links were also pretty helpful:

    SO question

    video

    With that, I added in my startup class (for testing purposes) these classes:

    public class ApplicationAccessRequirement : IAuthorizationRequirement
    {
    }
    
    public class ApplicationAccessHandler : AuthorizationHandler<ApplicationAccessRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                       ApplicationAccessRequirement requirement)
        {
            //custom db access and verification of roles.. Replace true with
            //boolean type response. 
            if (true)
            {
                context.Succeed(requirement);
            }
    
            //TODO: Use the following if targeting a version of
            //.NET Framework older than 4.6:
            //      return Task.FromResult(0);
            return Task.CompletedTask;
        }
    }
    

    Then in the ConfigureServices method:

        services.AddControllersWithViews(
            config =>
            {
                var policy = new AuthorizationPolicyBuilder();
                //policy.RequireRole("Test");
                policy.Requirements.Add(new ApplicationAccessRequirement());
                config.Filters.Add(new AuthorizeFilter(policy.Build()));
            }
        );
    
        services.AddAuthorization(options =>
        {
            options.AddPolicy("ApplicationAccess", policy =>
                policy.Requirements.Add(new ApplicationAccessRequirement()));
        });
    
        services.AddSingleton<IAuthorizationHandler, ApplicationAccessHandler>();
    

    This creates a global policy.. if you wanted to just access it per controller, you would just add, [Authorize(Policy = "ApplicationAccess")] above whatever method or controller to authorize and remove the config =>... section for the global filter.

    At this point I will need to decide when to use this, or just create a custom Authorization Attribute so I can pass the roles individually as a parameter and cross check the database in there.