3

I have the following useful load in a token generated with JWT

{ "sub": "flamelsoft@gmail.com", "jti": "0bca1034-f3ce-4f72-bd91-65c1a61924c4", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Administrator", "exp": 1509480891, "iss": "http://localhost:40528", "aud": "http://localhost:40528" }

with this code Startup.cs

        public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<DBContextSCM>(options =>
        options.UseMySql(Configuration.GetConnectionString("DefaultConnection"), b =>
         b.MigrationsAssembly("FlamelsoftSCM")));

        services.AddIdentity<User, Role>()
            .AddEntityFrameworkStores<DBContextSCM>()
            .AddDefaultTokenProviders();

        services.AddScoped(typeof(IRepository<>), typeof(Repository<>));

        services.AddAuthentication()
             .AddJwtBearer(cfg =>
             {
                 cfg.RequireHttpsMetadata = false;
                 cfg.SaveToken = true;

                 cfg.TokenValidationParameters = new TokenValidationParameters()
                 {
                     ValidIssuer = Configuration["Tokens:Issuer"],
                     ValidAudience = Configuration["Tokens:Issuer"],
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
                 };

             });

        services.AddMvc();
    }

AccountController.cs

        [HttpPost]
    [Authorize(Roles="Administrator")]
    public async Task<IActionResult> Register([FromBody]RegisterModel model)
    {
        try
        {
            var user = new User { UserName = model.Email, Email = model.Email };
            var result = await _userManager.CreateAsync(user, model.Password);
            if (result.Succeeded)
            {
                var role = await _roleManager.FindByIdAsync(model.Role);
                result = await _userManager.AddToRoleAsync(user, role.Name);

                if (result.Succeeded)
                    return View(model);
            }
            return BadRequest($"Error: Could not create user");
        }
        catch (Exception ex)
        {
            return BadRequest($"Error: {ex.Message}");
        }
    }

user.service.ts

export class UserService {

constructor(private http: Http, private config: AppConfig, private currentUser: User) { }

create(user: User) {
    return this.http.post(this.config.apiUrl + 'Account/Register', user, this.jwt());
}

private jwt() {
    const userJson = localStorage.getItem('currentUser');
    this.currentUser = userJson !== null ? JSON.parse(userJson) : new User();

    if (this.currentUser && this.currentUser.token) {
        let headers = new Headers({ 'Authorization': 'Bearer ' + this.currentUser.token });
        return new RequestOptions({ headers: headers });
    }
}}

The problem is that the validation of the role does not work, the request arrives at the controller and returns a code 200 in the header, but never enters the class. When I remove the [Authorize (Roles = "Administrator")] it enters correctly my code. Is there something badly defined? Or what would be the alternative to define an authorization through roles.

  • I think the first step would be to determine whether it's the authentication or the authorization that _fails_. Could you replace `[Authorize (Roles = "Administrator")]` by `[Authorize]`? This will enforce that the user be successfully authenticated but not require they're part of the `Administrator` role. Depending on the result we'll decide where to look next. – Mickaël Derriey Oct 31 '17 at 23:16
  • Is correct, if I replace [Authorize (Roles = "Administrator")] by [Authorize] if it works. I think the problem is the following. The claims / role URI is not found. http://schemas.microsoft.com/ws/2008/06/identity/claims/role – Team Flamelsoft Nov 01 '17 at 00:58
  • Any idea how to fix it? – Team Flamelsoft Nov 01 '17 at 01:07
  • I can recommend you to use the JWT middleware events to inspect the `ClaimsPrincipal` that is generated from the token. `JwtBearerOptions` exposes an `Events` property that allows you to hook some logic at different stages of the authentication process. The `TokenValidated` method will be called after authentication is successful as you can [see here](https://github.com/aspnet/Security/blob/rel/2.0.0/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs#L138). Inspect the claims of the generated principal and give us the result. – Mickaël Derriey Nov 01 '17 at 02:25
  • Thank you very much, the solution was the following, `[Authorize(AuthenticationSchemes = "Bearer", Roles = "Administrator")]` – Team Flamelsoft Nov 01 '17 at 18:24
  • I now understand why you need this. Thank you, that helped me, too. If bearer tokens is the only authentication scheme in your application, you could set it as a default in `Startup` with `services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(cfg => { // setting options here })` and add `app.UseAuthentication()` in `Configure()`. That would allow you to not have to specify the authentication scheme in the `Authorize` attribute. I'll try to post a more detailed answer at some point. – Mickaël Derriey Nov 02 '17 at 00:46

2 Answers2

6

TL;DR

As mentioned in the comments of the original question, changing:

[HttpPost]
[Authorize(Roles = "Administrator")]
public async Task<IActionResult> Register([FromBody]RegisterModel model)
{
    // Code
}

to

[HttpPost]
[Authorize(AuthenticationSchemes = "Bearer", Roles = "Administrator")]
public async Task<IActionResult> Register([FromBody]RegisterModel model)
{
    // Code
}

resolved the issue.

Bearer is the default authentication scheme name when using JWT bearer authentication in ASP.NET Core.


But why do we need to specify the AuthenticationSchemes property on the [Authorize] attribute?

It's because configuring authentication schemes doesn't mean they will run on each HTTP request. If a specific action is accessible to anonymous users, why bother extract user information from a cookie or a token? MVC is smart about this and will only run authentication handlers when it's needed, that is, during requests that are somehow protected.

In our case, MVC discovers the [Authorize] attribute, hence knows it has to run authentication and authorization to determine if the request is authorized or not. The trick lies in the fact that it will only run the authentication schemes handlers which have been specified. Here, we had none, so no authentication was performed, which meant authorization failed since the request was considered anonymous.

Adding the authentication scheme to the attribute instructed MVC to run that handler, which extracted user information from the token in the HTTP request, which lead to the Administrator role being discovered, and the request was allowed.


As a side note, there's another way to achieve this, without resorting to using the AuthenticationSchemes property of the [Authorize] attribute.

Imagine that your application only has one authentication scheme configured, it would be a pain to have to specify that AuthenticationSchemes property on every [Authorize] attribute.

With ASP.NET Core, you can configure a default authentication scheme. Doing so implies that the associated handler will be run for each HTTP request, regardless of whether the resource is protected or not.

Setting this up is done in two parts:

public class Startup
{
    public void ConfiguresServices(IServiceCollection services)
    {
        services
            .AddAuthentication(JwtBearerDefaults.AuthenticationScheme /* this sets the default authentication scheme */)
            .AddJwtBearer(options =>
            {
                // Configure options here
            });
    }

    public void Configure(IApplicationBuilder app)
    {
        // This inserts the middleware that will execute the 
        // default authentication scheme handler on every request
        app.UseAuthentication();

        app.UseMvc();
    }
}

Doing this means that by the time MVC evaluates whether the request is authorized or not, authentication will have taken place already, so not specifying any value for the AuthenticationSchemes property of the [Authorize] attribute won't be a problem.

The authorization part of the process will still run and check against the authenticated user whether they're part of the Administrator group or not.

Mickaël Derriey
  • 12,796
  • 1
  • 53
  • 57
2

I know this question already has an answer, but something important is left out here. You need to make sure you're actually setting the claims for the logged in user. In my case, I'm using JWT Authentication, so this step is very important:

    var claims = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, user.UserName) });
    var roles = await _userManager.GetRolesAsync(user);
    if (roles.Count > 0)
    {
        foreach (var role in roles) { claims.AddClaim(new Claim(ClaimTypes.Role, role)); }
    }

    var token = new JwtSecurityToken(
        issuer: _configuration["JWT:Issuer"],
        audience: _configuration["JWT:Audience"],
        expires: DateTime.UtcNow.AddMinutes(15),
        signingCredentials: signingCredentials,
        claims: claims.Claims);

I was banging my head trying to figure out why HttpContext.User didn't include what I expected trying to narrow down the [Authroization(Roles="Admin")] issue. Turns out, if you're using JWT Auth you need to remember to set the Claims[] to the identity. Maybe this is done automatically in other dotnet ways, but jwt seems to require you to set that manually.

After I set the claims for the user, the [Authorize(Roles = "Whatever")] worked as expected.

mwilson
  • 12,295
  • 7
  • 55
  • 95
  • 1
    Sheesh, the number of times I've done this. I just started to work with Roles again in my auth attribute. Didn't work ... started poking around SO. Found your post. Checked my code ... I'd commented out all my claims code 6 months ago, dropped the project and just came back it. What an idiot I can be. Thanks. – Jammer Feb 20 '20 at 08:19