Search code examples
c#asp.net-coreauthorizationrazor-pages

Roles Authorization not working for Razor Pages site using AddNegotiate


The Short Version:

I have a ASP.net core Razorpages app Authenticating against Windows Users via .addNegotiate(). Roles are then assigned based on AD Groups via a claims transformer, lets call one of the added Roles "Admin". And Additional policy "SiteAuth" has been set up based on these roles.

Authorizing against the "SiteAuth" policy a razor page using [Authorize(Policy = "SiteAuth")] works, however [Authorize(Roles = "Admin")] does not work and redirects to the Unauthorised page. Simplifying to [Authorize] does work.

Further more if (User.HasClaim(claim => claim.Value == "Admin")) also works as expected.

I've observed the claims transform being hit via a break point when hitting the page showing that the user should be authorized and the Claim has been added.

What have I missed or done wrong that [Authorize(Roles = "Admin")] does not Authorize as expected?

Longer Tech Details Version:

Program.cs - relevant parts:

builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
   .AddNegotiate();
builder.Services.AddAuthorization(options =>
{
    // By default, all incoming requests will be authorized according to the default policy.
    options.FallbackPolicy = options.DefaultPolicy;
    options.AddPolicy("SiteAuth", policy => policy.RequireAssertion(
        context => context.User.HasClaim(claim => claim.Type == ClaimTypes.Role && (new string[] { "SuperAdmin", "Admin", "User" }).Contains(claim.Value))));
});

builder.Services.AddControllers();
builder.Services.AddRazorPages().AddRazorPagesOptions(o =>
{
    o.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute());
    o.Conventions.Add(new ActionRouteConvention());
});

builder.Services.AddMemoryCache();
builder.Services.AddSession();
builder.Services.AddHttpContextAccessor();


builder.Services.AddScoped<IClaimsTransformation, ClaimsTransformer>();

/**** Other Services Added/Configured ***/


var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/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.UseSession();
app.UseAuthentication();
app.UseStatusCodePagesWithReExecute("/errors/{0}");
app.UseAuthorization();


app.MapControllers();
app.MapRazorPages();
app.MapGet("/debug-claims", (ClaimsPrincipal user) =>
{
    return Results.Ok(user.Claims.Select(c => new { c.Type, c.Value }));
}).RequireAuthorization();
app.Run();

ClaimsTransformer

public class ClaimsTransformer : IClaimsTransformation
{
    private readonly SiteSettings _siteSettings;
    private readonly IHttpContextAccessor _context;
    private readonly ILogger<ClaimsTransformer> _logger;

    public ClaimsTransformer(IHttpContextAccessor httpContextAccessor, SiteSettings siteSettings, ILogger<ClaimsTransformer> logger)
    {
        _siteSettings= siteSettings;
        _context= httpContextAccessor;
        _logger = logger;
    }
    
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var wi = (WindowsIdentity)principal.Identity;
        var ci = (ClaimsIdentity)principal.Identity;
        var groups = wi.Groups;

        if (groups != null)
        {
            foreach (var group in groups) //-- Getting all the AD groups that user belongs to---  
            {
                try
                {
                    string groupName = string.Empty;
                    if(_siteSettings.AdminGroups.TryGetValue(group.Translate(typeof(NTAccount)).ToString(), out groupName))
                    {
                        if(!principal.HasClaim(c => c.Value == groupName))
                        {
                            //var claim = new Claim("RAAuth", groupName);
                            //wi.AddClaim(claim);
                            _logger.LogInformation($"Adding role {groupName} to claims.");
                            var claim = new Claim(ClaimTypes.Role, groupName);
                            //wi.AddClaim(claim);
                            ci.AddClaim(claim);
                            //claim = new Claim(wi.RoleClaimType, group.Value);
                            //wi.AddClaim(claim);
                        }
                    }                    
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }
        }
        return Task.FromResult(principal);
    }    
}

Debug Info

Dumping the claims to screen gives:

{
    "type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
    "value": "Admin"
},
{
    "type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role",
    "value": "SuperAdmin"
},

Which strongly suggests the roles have been applied.

The role name was copy and pasted to the [Authorize(Roles = "Admin")] to eliminate typos

Hopefully I've messed something simple here.


Solution

  • It is because when using windows authentication the "RoleClaim Name" is different. Just use like following:

    ...
    var ci = (ClaimsIdentity)principal.Identity;
    ...
    var claim = new Claim(ci.RoleClaimType, groupName);
    ...
    

    Then the [Authorize(Roles = "Admin")]will work.
    To clarify:
    ClaimTypes.Role : "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
    ci.RoleClaimType: "http://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid"