简体   繁体   中英

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, eg [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.

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())
                ));

I misused the Policy extension of the Authorize attribute.

I should have been using [Authorize(Roles = "")].

I updated the question to reflect my mistake.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM