Role based authorization in ASP.NET Core 3.1 with Identity and ExternalLogin

Rémy Huot picture Rémy Huot · Jun 5, 2020 · Viewed 10.1k times · Source

Im new to .NET Core and I'm trying to setup Role based authorization in a .NET Core 3.1 project. I believe I clicked on every tutorials and threads talking about it online. My problem is that it seems to be working very easily on the tutorials, but it doesn't work for me. According to tutorials I have found, all I would have to do is assign a role to a user in a database, then use [Authorize(Roles="roleName")] before a Controller's Action. When I do that I always get a 403 error for a user having the specified role. When I use userManager.GetRolesAsync(user), I see that the user has the role. When I make a request to this action with [Authorize], it works when the user is logged in, as expected.

I checked in debug mode ClaimsPrincipal.Identity for the current user and I found out that RoleClaimType = "role". I checked the claims of the current user and found out that it doesn't have a claim with a type "role". Is this how [Authorize(Roles="...")] works? Does it look a the claims? If so, how do I had a claim for the user's role? The only way for a user to login in this application is with a Google account. So how am I supposed to add a claim if they are managed by the Google login?

Here's my code in Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDefaultIdentity<ApplicationUser>()
        .AddRoles<ApplicationRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>();

    services.AddIdentityServer()
        .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

    services.AddAuthentication()
        .AddGoogle(options =>
        {
            IConfigurationSection googleAuthNSection =
            Configuration.GetSection("Authentication:Google");

            options.ClientId = googleAuthNSection["ClientId"];
            options.ClientSecret = googleAuthNSection["ClientSecret"];
        })
        .AddIdentityServerJwt();

    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = "ClientApp/dist";
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    if (!env.IsDevelopment())
    {
        app.UseSpaStaticFiles();
    }
    app.UseRouting();
    app.UseIdentityServer();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller}/{action=Index}/{id?}");
        endpoints.MapRazorPages();
    });

    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "ClientApp";

            if (env.IsDevelopment())
            {
                spa.UseAngularCliServer(npmScript: "start");
            }
    });
}

Here's an exemple of an Action of a Controller

[Authorize(Roles = "Admin")]
[HttpGet("userinformations")]
public async Task<UserInformations> GetCurrentUserInformations()
{
    string strUserId = this.User.FindFirstValue(ClaimTypes.NameIdentifier);

    ApplicationUser user = await userManager.FindByIdAsync(strUserId);

    string[] roles = (await userManager.GetRolesAsync(user)).ToArray();

    UserInformations userInfo = new UserInformations()
    {
        UserName = user.UserName,
        FirstName = user.FirstName,
        LastName = user.LastName,
        Email = user.Email,
        Organization = user.idDefaultOrganisation.HasValue ? user.DefaultOrganization.OrganizationName : "",
        Claims = this.User.Claims.Select(c => $"{c.Type} : {c.Value}").ToArray(),
        Roles = roles
    };

    return userInfo;
}

When I make a request to this Action without [Authorize(Roles = "Admin")], I can see that the current user has the role Admin, but when I add it, I get a 403 error.

What am I doing wrong? I feel like I'm missing one line somewhere or something like that because it all seems so simple in the tutorials I found.

Answer

Michael Shterenberg picture Michael Shterenberg · Jun 5, 2020

Your assumption was correct, when you specify the [Authorize(Roles = "<role>")] attribute, ASP will create a RolesAuthorizationRequirement behind the scene.

Then the authorization handler will call this.HttpContext.User.IsInRole(<role>) to evaluate the policy.

In your case, the call is this.HttpContext.User.IsInRole("Admin")

The method User.IsInRole will look into a claim named "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" and compare its value to "Admin"

The ASP Authorization pipeline is not hooked to your UserManager logic, the basic API will only observe and validate the JWT token claims.

You should probably create your own AuthorizationHandler that checks if the user is indeed Admin

Or the less formal way using RequireAssertion :

services.AddAuthorization(options => options.AddPolicy("Admininstrators", builder =>
{
    builder.RequireAssertion(async context =>
    {
        string strUserId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
        var user = await userManager.FindByIdAsync(strUserId);
        string[] roles = (await userManager.GetRolesAsync(user)).ToArray();
        return roles.Contains("Admin");
    };
});

[Authorize("Admininstrators")]
[HttpGet("userinformations")]
public async Task<UserInformations> GetCurrentUserInformations()
{
   ...
}