Search code examples
c#oauth.net-corejwtasp-net-core-spa-services

JWT Auth works with Authorize Attribute but not with [Authorize (Policy = "Administrator")]


I have a .NetCore 2.2 Application using Json Web Tokens to authenticate and authorize users.

When I add the [Authorize] Attribute to my controllers, I am able to add the Bearer Token to any requests to those controllers and interact with data.

When I change the Auth attribute to include a role, e.g. [Authorize (Policy="Administrator")] the requests always return a 403.

The User.cs model contains a Role enum with values User/Administrator.

Within Startup.cs I have added RequireRole/RequireAuthenticatedUser.

See Startup.cs

    public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors();

        services.AddMvc()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
            .AddJsonOptions(options => { options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });

        // In production, the Angular files will be served from this directory
        services.AddSpaStaticFiles(configuration =>
        {
            configuration.RootPath = "ClientApp/dist";
        });

        #region JWT
        // Configure AppSettings and add to DI  
        var appSettingsSection = Configuration.GetSection("AppSettings");
        services.Configure<AppSettings>(appSettingsSection);

        // Configure jwt authentication
        var appSettings = appSettingsSection.Get<AppSettings>();
        var key = Encoding.ASCII.GetBytes(appSettings.Secret);

        // Add Jwt Authentication Service
        services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(x =>
        {
            x.RequireHttpsMetadata = false;
            x.SaveToken = true;
            x.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false
            };
        });
        #endregion

        #region Add Transient DI
        services.AddTransient<IPlayerService, PlayerService>();
        #endregion

        #region Add Authorization
        services.AddAuthorization(options =>
        {
            options.AddPolicy("Administrator",
                p => p.RequireAuthenticatedUser().RequireRole(Role.Administrator.ToString())
            );
            options.AddPolicy("User",
                p => p.RequireAuthenticatedUser().RequireRole(
                    new[] { Role.User.ToString(), Role.User.ToString() }
                )
            );
        });
        #endregion

        #region Cookies
        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options => {
        options.AccessDeniedPath = "/User/ErrorNotAuthorised";
        options.LoginPath = "/User/ErrorNotAuthenticated";
    });
        #endregion
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            // seeder recreates and seeds database on each execution
            new DataSeeder(new PlayerService(), new ClubService(), new TeamService(), new TeamPlayerService(), new UserService()).Seed();
        }
        else
        {
            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.UseSpaStaticFiles();
        app.UseCookiePolicy();

        app.UseCors(x => x
        .AllowAnyOrigin()
        .AllowAnyMethod()
        .AllowAnyHeader());

        app.UseAuthentication();

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller}/{action=Index}/{id?}");
        });


        app.UseSpa(spa =>
        {
            // To learn more about options for serving an Angular SPA from ASP.NET Core,
            // see https://go.microsoft.com/fwlink/?linkid=864501

            spa.Options.SourcePath = "ClientApp";

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

Sample controller method:

    // POST: api/Player
    [Authorize(Policy="Administrator")]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public void Post([FromBody] Player player)
    {
        _service.AddPlayer(player);
    }

This controller method returns a 403 unauthorized request from all interactions. I think my JWT token doesn't contain the Role value, but I'm not sure how to check or how to include it.

Any help is appreciated.

EDIT:

Watch on Users

Users class

    public enum Role
{
    Administrator,
    User
}

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
    public Team Team { get; set; }
    public Role Role { get; set; }
    public string Token { get; set; }
}

EDIT 2:

So all that is really needed for the JWT to use Roles as a form of authentication is included in the Startup.cs function ConfigureServices below. I left out the JWT class, and have also included that below.

I changed the auth attribute on controllers to look for Roles = "Administrator" instead of Policies.

Startup.cs

            public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors();

        // Configure AppSettings and add to DI
        var appSettingsSection = Configuration.GetSection("AppSettings");
        services.Configure<AppSettings>(appSettingsSection);

        // Configure jwt authentication
        var appSettings = appSettingsSection.Get<AppSettings>();
        var key = Encoding.ASCII.GetBytes(appSettings.Secret);

        // Add Jwt Authentication Service
        services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(x =>
        {
            x.RequireHttpsMetadata = false;
            x.SaveToken = true;
            x.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false
            };
        });

JWT Helper class that previous I did not understand:

    {       
     // generate Jwt token
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(secret);
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new Claim[]
            {
                new Claim(ClaimTypes.Role, user.Role.ToString()),
                new Claim(ClaimTypes.Sid, user.Id.ToString())
            }),
            Expires = DateTime.UtcNow.AddDays(50),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };
        var token = tokenHandler.CreateToken(tokenDescriptor);
        user.Token = tokenHandler.WriteToken(token);
         return user;

}

Sample of controller w/ Role attribute:

            [Authorize(Roles = "Administrator")]
    [HttpPost]
    public void Post([FromBody] Player player)
    {
        _service.AddPlayer(player);
    }

Finally, most of this is obvious and I should've knew before I started the project never mind this post - but updating so anyone who comes across this in the future sees the more appropriate route.


Solution

  • Make sure Role claims are picked up from JWT token. Role claim name can be set this way:

    .AddJwtBearer(x =>
    {
        x.RequireHttpsMetadata = false;
        x.SaveToken = true;
        x.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = false,
            ValidateAudience = false,
    
            RoleClaimType = "role" // same name as in your JWT token, as by default it is 
            // "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" 
        };
        options.Events = new JwtBearerEvents
        {
            OnTokenValidated = context =>
            {
                var jwt = (context.SecurityToken as JwtSecurityToken)?.ToString();
                // get your JWT token here if you need to decode it e.g on https://jwt.io
                // And you can re-add role claim if it has different name in token compared to what you want to use in your ClaimIdentity:  
                AddRoleClaims(context.Principal);
                return Task.CompletedTask;
            }
        };
    
    });
    
    private static void AddRoleClaims(ClaimsPrincipal principal)
    {
        var claimsIdentity = principal.Identity as ClaimsIdentity;
        if (claimsIdentity != null)
        {
            if (claimsIdentity.HasClaim("role", "AdminRoleNameFromToken"))
            {
                if (!claimsIdentity.HasClaim("role", Role.Administrator.ToString()))
                {
                    claimsIdentity.AddClaim(new Claim("role", Role.Administrator.ToString()));
                }
            }
        }
    }
    

    And I would re-configure your policy as

    options.AddPolicy("Administrator", policy => policy.RequireAssertion(context =>
                        context.User.IsInRole(Role.Administrator.ToString())
                    ));