3 Minutes Society.NET Core Jwt Policy Authorization Certification
I. Preface #
Hello, I'm back. A few days ago I talked about the simplest case of Jwt authentication, but it's not powerful enough for real projects. Yes, in a real complex and demanding customer, we're at a loss. Now we need to make the certification authorization more complex and practical, which is called in professional terms API authentication for custom policies, this case runs in.NET Core 3.0, and we'll browse in swagger to try out if the project is working. For version 2.x of.NET Core, some of the code in this article doesn't apply, but I'll explain it here.
2. Try in.NET Core #
We all know that Jwt is for certification, and Microsoft has provided us with AuthorizationHandle, the gateway to ghost fighting in the city.
We'll implement it first, and we can also get context from the AutohorizationHandlerContext that relies on injection, so we can do more with permissions
public class PolicyHandler : AuthorizationHandler<PolicyRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement) { var http = (context.Resource as Microsoft.AspNetCore.Routing.RouteEndpoint); var questUrl = "/"+http.RoutePattern.RawText; //Assign User Rights var userPermissions = requirement.UserPermissions; //Is it verified var isAuthenticated = context.User.Identity.IsAuthenticated; if (isAuthenticated) { if (userPermissions.Any(u=>u.Url == questUrl)) { //User name var userName = context.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.NameIdentifier).Value; if (userPermissions.Any(w => w.UserName == userName)) { context.Succeed(requirement); } } } return Task.CompletedTask; } }
First, we rewrote the HandleRequirementAsync method. If you have seen the source code of AspNetCore, you must know that it is the beginning of Jwt authentication, that is, if you rewrite it, the original set will not go away. Let's look at the source code, I paste it below, and you can see that this is the most basic authorization, which is done through context.Succeed(requirement)Final certification action!
public class DenyAnonymousAuthorizationRequirement : AuthorizationHandler<DenyAnonymousAuthorizationRequirement>, IAuthorizationRequirement { /// <summary> /// Makes a decision if authorization is allowed based on a specific requirement. /// </summary> /// <param name="context">The authorization context.</param> /// <param name="requirement">The requirement to evaluate.</param> protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DenyAnonymousAuthorizationRequirement requirement) { var user = context.User; var userIsAnonymous = user?.Identity == null || !user.Identities.Any(i => i.IsAuthenticated); if (!userIsAnonymous) { context.Succeed(requirement); } return Task.CompletedTask; } }
So what is Succeed?It is a defining action in the AuthorizationHandlerContext, including Fail(), and so is it. Of course, the implementation is not detailed, but it's still complex inside, but what we need is that DenyAnonymous AuthorizationRequirement is treated as part of the abstraction.
public abstract class AuthorizationHandler<TRequirement> : IAuthorizationHandler where TRequirement : IAuthorizationRequirement {}
Well, back to the point (it's exciting to see the source), we've just implemented a custom authentication strategy in PolicyHandler, and there are two other approaches mentioned above.Now we configure it in the project and start it, and I used Swagger in my code for later demonstrations.
In AddJwtBearer we added jwt validation, which includes validation parameters and several event handling, which is basic and not explained.However, some of the features that add jwt to Wagger are written in AddSecurityDefinition.
public void ConfigureServices(IServiceCollection services) { //Add Policy Authentication Mode services.AddAuthorization(options => { options.AddPolicy("Permission", policy => policy.Requirements.Add(new PolicyRequirement())); }) .AddAuthentication(s => { //Add JWT Scheme s.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; s.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; s.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) //Add jwt validation: .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateLifetime = true,//Verify Failure Time ClockSkew = TimeSpan.FromSeconds(30), ValidateAudience = true,//Verify Audience //ValidAudience = Const.GetValidudience(),//Audience //Dynamic authentication is used here, and when the token is refreshed on login, the old token is forcibly invalidated AudienceValidator = (m, n, z) => { return m != null && m.FirstOrDefault().Equals(Const.ValidAudience); }, ValidateIssuer = true,//Verify Issuer ValidIssuer = Const.Domain,//Issuer, which is consistent with the previous jwt signature settings ValidateIssuerSigningKey = true,//Verify SecurityKey IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//Get SecurityKey }; options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { //Token expired if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) { context.Response.Headers.Add("Token-Expired", "true"); } return Task.CompletedTask; } }; }); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", Title = "HaoZi JWT", Description = "Be based on.NET Core 3.0 Of JWT Authentication", Contact = new OpenApiContact { Name = "zaranet", Email = "zaranet@163.com", Url = new Uri("http://cnblogs.com/zaranet"), }, }); c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() { Description = "Enter a request header in the box below that needs to be added Jwt To grant authorization Token: Bearer Token", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, BearerFormat = "JWT", Scheme = "Bearer" }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] { } } }); }); //Certification Services services.AddSingleton<IAuthorizationHandler, PolicyHandler>(); services.AddControllers(); }
In the code above, we add authentication rules through authentication mode, a class called PolicyRequirement that implements the IAuthorizationRequirement interface, where we need to define some rules that allow us to add the permission rules we want to identify through the constructor.That UserName is Attribute.
public class PolicyRequirement : IAuthorizationRequirement {/// <summary> /// User rights collection /// </summary> public List<UserPermission> UserPermissions { get; private set; } /// <summary> /// No permission action /// </summary> public string DeniedAction { get; set; } /// <summary> /// structure /// </summary> public PolicyRequirement() { //Jump to this route without permission DeniedAction = new PathString("/api/nopermission"); //Route configuration that users have access to, of course you can read it from the database, you can also put it in Redis for persistence UserPermissions = new List<UserPermission> { new UserPermission { Url="/api/value3", UserName="admin"}, }; } } public class UserPermission { public string UserName { get; set; } public string Url { get; set; } }
We should then start our service, where the middleware location for authentication in.NET Core 3.0 needs to be in the middle of routing and endpoint configuration.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
We usually have an API that gets token to let Jwt generate our token through JwtSecurityTokenHandler().WriteToken(token). Although JWT is stateless, you should also understand that if your JWT generates and you restart your website later, your JWT will fail because your key has changed and if your key has been written to deathThen this JWT will no longer expire and there is still a security risk. I do not explain this here. gettoken is defined as follows:
[ApiController] public class AuthController : ControllerBase { [AllowAnonymous] [HttpGet] [Route("api/nopermission")] public IActionResult NoPermission() { return Forbid("No Permission!"); } /// <summary> /// login /// </summary> [AllowAnonymous] [HttpGet] [Route("api/auth")] public IActionResult Get(string userName, string pwd) { if (CheckAccount(userName, pwd, out string role)) { Const.ValidAudience = userName + pwd + DateTime.Now.ToString(); // push the user's name into a claim, so we can identify the user later on. //Custom parameters can be added here, key s can start on their own var claims = new[] { new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds()}"), new Claim(ClaimTypes.NameIdentifier, userName), new Claim("Role", role) }; //sign the token using a secret key.This secret will be shared between your API and anything that needs to check that the token is legit. var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); //.NET Core's JwtSecurityToken class takes on the heavy lifting and actually creates the token. var token = new JwtSecurityToken( issuer: Const.Domain, //Issuer audience: Const.ValidAudience,//Expiration Time expires: DateTime.Now.AddMinutes(30),// Signature Certificate signingCredentials: creds, //Custom parameters claims: claims ); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) }); } else { return BadRequest(new { message = "username or password is incorrect." }); } } /// <summary> ///Simulated Logon Check /// </summary> private bool CheckAccount(string userName, string pwd, out string role) { role = "user"; if (string.IsNullOrEmpty(userName)) return false; if (userName.Equals("admin")) role = "admin"; return true; }
Perhaps more special is AllowAnonymous, the student who read my article may have seen it for the first time. What's good about it? There are no hard and hard requirements. I see several well-known bloggers add it, and I add ~... Finally, we created several resource controllers that are protected.
When you add policy permissions, for example, the policy name is XXX, then the corresponding api header should be XXX, and then at PolicyHandler we parsed whether Claims handled it.
// GET api/values1 [HttpGet] [Route("api/value1")] public ActionResult<IEnumerable<string>> Get() { return new string[] { "value1", "value1" }; } // GET api/values2 /** * The interface is privilege checked with the Authorize feature, and if it fails, the http return status code is 401 */ [HttpGet] [Route("api/value2")] [Authorize] public ActionResult<IEnumerable<string>> Get2() { var auth = HttpContext.AuthenticateAsync().Result.Principal.Claims; var userName = auth.FirstOrDefault(t => t.Type.Equals(ClaimTypes.NameIdentifier))?.Value; return new string[] { "This interface is accessible when it has been logged on", $"userName={userName}" }; } /** * This interface must use admin **/ [HttpGet] [Route("api/value3")] [Authorize("Permission")] public ActionResult<IEnumerable<string>> Get3() { //This is the way to get custom parameters var auth = HttpContext.AuthenticateAsync().Result.Principal.Claims; var userName = auth.FirstOrDefault(t => t.Type.Equals(ClaimTypes.NameIdentifier))?.Value; var role = auth.FirstOrDefault(t => t.Type.Equals("Role"))?.Value; return new string[] { "This interface has administrator privileges to access", $"userName={userName}", $"Role={role}" }; }
3. Effect Charts #
4. Chestnut source code and previous versions #
You see many pits of predecessor colors, the original (context.Resource as Microsoft.AspNetCore.Routing.RouteEndpoint); in fact, it is no longer available in.NET Core 3.0 because after.NET Core 3.0 enabled Endpoint Routing, the permission filter is no longer added to ActionDescriptor, but runs directly as a middleware, and all filters are added to endpoint.Metadata, if you were in.NET Core 2.1 & 2.2, you would typically write Handler as follows:
public class PolicyHandler : AuthorizationHandler<PolicyRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement) { //Assign User Rights var userPermissions = requirement.UserPermissions; //Convert from AuthorizationHandlerContext to HttpContext to fetch table information var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext; //Request Url var questUrl = httpContext.Request.Path.Value.ToUpperInvariant(); //Is it verified var isAuthenticated = httpContext.User.Identity.IsAuthenticated; if (isAuthenticated) { if (userPermissions.GroupBy(g => g.Url).Any(w => w.Key.ToUpperInvariant() == questUrl)) { //User name var userName = httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.NameIdentifier).Value; if (userPermissions.Any(w => w.UserName == userName && w.Url.ToUpperInvariant() == questUrl)) { context.Succeed(requirement); } else { //No permission to jump to rejection page httpContext.Response.Redirect(requirement.DeniedAction); } } else context.Succeed(requirement); } return Task.CompletedTask; } }
The source code for this case is on my Github: https://github.com/zaranetCore/aspNetCore_JsonwebToken/tree/master/Jwt_Policy_Demo Thank you all~!!