Search code examples
c#asp.net-core-mvcjwtasp.net-core-2.0angular2-jwt

.Net Core 2 JWT, Angular 2 Authorization through roles does not work


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.


Solution

  • 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.