Search code examples
c#asp.net-identityasp.net-authenticationasp.net-core-8

ASP.NET Core 8 - Authorization redirecting to Account/AccessDenied


I have a Razor page with the attribute:

[Authorize(Policy = "Staff")]

and the following authorization configuration:

options.AddPolicy("Staff", policy =>
        {
            policy.RequireAuthenticatedUser();
            policy.AddAuthenticationSchemes("MyAdSchemeName");
            policy.RequireRole("Staff");
        });

When navigating to this page, it correctly redirects me to the login page, however, the return URL passed as the query string param is Account/AccessDenied.

I feel like maybe I'm missing something, but this feels wrong as surely authentication needs to occur before it can be determined whether a 403 should be returned?

My expectation (which may be wrong) is that navigating to the page as a non-authenticated user should redirect to the login page, the login is processed (i.e. roles obtained) & then redirected back to the originally requested page where it can then determine whether they have access (i.e. the required role)?

When I remove the RequireRole call against the policy definition, it works - but obviously allows any all authenticated users regardless of role.

As an aside, Account/AccessDenied doesn't even exist in my app, and I cannot see how to change it. I've tried the following, but to no avail:

services.Configure<CookieAuthenticationOptions>("MyAdSchemeName", options => options.AccessDeniedPath = "/errors/403");

Program.cs code:

var authentication = services.AddAuthentication();

authentication
    .AddMicrosoftIdentityWebApp(
        configuration.GetSection("AdConfig"), 
        openIdConnectScheme: "MyAdSchemeName", 
        cookieScheme: null, 
        displayName: "MyAdSchemeName");

services.Configure<CookieAuthenticationOptions>("MyAdSchemeName", options => options.AccessDeniedPath = "/errors/403");

services.ConfigureApplicationCookie(options =>
{
    options.Events = new CookieAuthenticationEvents
    {
        OnRedirectToAccessDenied = context =>
        {
            context.Response.StatusCode = 403;
            return Task.FromResult(0);
        }
    };
    options.AccessDeniedPath = "/errors/403";
    options.LoginPath = new PathString("/login");
});

services.AddAuthorization(options =>
{
    options.FallbackPolicy = options.DefaultPolicy;
    
    
    options.AddPolicy(PolicyNames.Staff, policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.AddAuthenticationSchemes("MyAdSchemeName");
        policy.RequireRole("Staff");
    });
}); 

Solution

  • Actually, there are multiple questions asked here and various factors involved so why don't we take a step back, clean up the configuration a bit and start with a working sample:

    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie();
    
    services.AddAuthorization(options =>
    {
        options.DefaultPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .Build();
    
        // staff authentication policy
        options.AddPolicy("Staff", policy =>
        {
            policy.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme);
            policy.RequireClaim(ClaimTypes.Role, "Staff");
        });
    });
    

    The above demonstrates configuration of:

    • the default policy
    • a custom policy named "Staff", which allows access only to users with "Staff" role

    Account/AccessDenied is one of the default redirects of ASP.NET Core Identity and a View with the same name is located under the Views folder, typically. If you want to change that modify the above sample as follows:

    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(options =>
        {
            options.AccessDeniedPath = "...";
        });
    

    As a extra note, apart from using the Authorize attribute you can apply the "Staff" policy in a whole area, which I find very convenient:

    endpoints.MapControllerRoute(
        name: "Staff",
        pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}")
    .RequireAuthorization("Staff");
    

    I hope it helps and that you can further adjust it to your customization needs.