ASP.NET Core 6.0 add JWT authentication and authorization

preface

This article will introduce Authentication and Authorization respectively.

And with a simple example in ASP Net core 6.0 implements these two functions in the web API.


Related nouns

Authentication and Authorization look very much like each other. It's hard to distinguish them.

Authentication: identify the user's identity, which usually occurs when logging in.

Authorization: Grant user permission and specify which resources the user can access; The premise of authorization is to know who the user is, so authorization must be after authentication.


Authentication

Basic steps

  1. Install related Nuget packages: Microsoft AspNetCore. Authentication. JwtBearer
  2. Prepare configuration information (key, etc.)
  3. Add service
  4. Call Middleware
  5. Implement a JwtHelper to generate a Token
  6. Controller restrict access (add Authorize tag)

1 install Nuget package

Install Microsoft AspNetCore. Authentication. JwtBearer

In the package manager console:

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 6.0.1

2 prepare configuration information

In appsetting JSON, add a Jwt node

"Jwt": {
    "SecretKey": "lisheng741@qq.com",
    "Issuer": "WebAppIssuer",
    "Audience": "WebAppAudience"
}

3 add service

In program Register the service in the. CS file.

// Introduce the required namespace
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

// ......
var configuration = builder.Configuration;

// Registration service
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true, //Verify Issuer
        ValidIssuer = configuration["Jwt:Issuer"], //Issuer issuer
        ValidateAudience = true, //Verify Audience
        ValidAudience = configuration["Jwt:Audience"], //Subscriber Audience
        ValidateIssuerSigningKey = true, //Verify SecurityKey
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:SecretKey"])), //SecurityKey
        ValidateLifetime = true, //Is the expiration time verified
        ClockSkew = TimeSpan.FromSeconds(30), //Fault tolerance value of expiration time to solve the problem of server-side time synchronization (seconds)
        RequireExpirationTime = true,
    };
});

4 call Middleware

Calling UseAuthentication (authentication) must be called top note before any authentication middleware is needed, such as UseAuthorization.

// ......
app.UseAuthentication();
app.UseAuthorization();
// ......

5 JwtHelper class implementation

It is mainly used to generate the Token of JWT.

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace TestWebApi;

public class JwtHelper
{
    private readonly IConfiguration _configuration;

    public JwtHelper(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public string CreateToken()
    {
        // 1. Define the Claims to be used
        var claims = new[]
        {
            new Claim(ClaimTypes.Name, "u_admin"), //HttpContext.User.Identity.Name
            new Claim(ClaimTypes.Role, "r_admin"), //HttpContext.User.IsInRole("r_admin")
            new Claim(JwtRegisteredClaimNames.Jti, "admin"),
            new Claim("Username", "Admin"),
            new Claim("Name", "Super administrator")
        };

        // 2. From Appsettings JSON
        var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"]));

        // 3. Select encryption algorithm
        var algorithm = SecurityAlgorithms.HmacSha256;

        // 4. Generate Credentials
        var signingCredentials = new SigningCredentials(secretKey, algorithm);

        // 5. Generate a token according to the above
        var jwtSecurityToken = new JwtSecurityToken(
            _configuration["Jwt:Issuer"],     //Issuer
            _configuration["Jwt:Audience"],   //Audience
            claims,                          //Claims,
            DateTime.Now,                    //notBefore
            DateTime.Now.AddSeconds(30),    //expires
            signingCredentials               //Credentials
        );

        // 6. Change token to string
        var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);

        return token;
    }
}

The JwtHelper relies on IConfiguration (to read the configuration file), and the creation of JwtHelper is handed over to DI container in program Add service to CS:

var configuration = builder.Configuration;
builder.Services.AddSingleton(new JwtHelper(configuration));

Register JwtHelper as singleton mode.

6 controller configuration

Create a new AccountController, inject JwtHelper in the form of constructor, and add two actions: GetToken to obtain Token, and GetTest is labeled with [Authorize] to verify authentication.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace TestWebApi.Controllers;

[Route("api/[controller]/[action]")]
[ApiController]
public class AccountController : ControllerBase
{
    private readonly JwtHelper _jwtHelper;

    public AccountController(JwtHelper jwtHelper)
    {
        _jwtHelper = jwtHelper;
    }

    [HttpGet]
    public ActionResult<string> GetToken()
    {
        return _jwtHelper.CreateToken();
    }

    [Authorize]
    [HttpGet]
    public ActionResult<string> GetTest()
    {
        return "Test Authorize";
    }
}

7 test call

Method 1: debug software through interfaces such as Postman and Apifox

Use Postman to call / api/Account/GetToken to generate a Token

When calling / api/Account/GetTest, pass in the Token and get the returned result

Mode 2: debug on browser console

Debug / api/Account/GetToken

var xhr = new XMLHttpRequest();
xhr.addEventListener("readystatechange", function() {
   if(this.readyState === 4) {
      console.log(token = this.responseText); //A global variable token is used here to serve the next interface
   }
});
xhr.open("GET", "/api/Account/GetToken");
xhr.send();

Debug / api/Account/GetTest

var xhr = new XMLHttpRequest();
xhr.addEventListener("readystatechange", function() {
   if(this.readyState === 4) {
      console.log(this.status, this.responseText); //this.status is the response status code and 401 is the non authentication status
   }
});
xhr.open("GET", "/api/Account/GetTest");
xhr.setRequestHeader("Authorization",`Bearer ${token}`); //Attach a token
xhr.send();

Authorization

Note: authorization must be based on authentication, that is, if the above configuration on authentication is not completed, the following authorization will not succeed.

In the authorization part, we will first introduce relevant labels and authorization methods, and then introduce policy based authorization. The general contents of these three parts are described as follows:

Related tags: Authorize and AllowAnonymous

Authorization method: introduce the basic contents of Policy, Role and Scheme

Policy based authorization: in depth policy authorization method

Related label (Attribute)

Please refer to the official documents for the specific authorization related labels Simple authorization

[Authorize]

The Controller or Action marked with this label must be authenticated and can identify which authorization rules need to be met.

Authorization rules can be policies, Roles, or AuthenticationSchemes.

[Authorize(Policy = "", Roles ="", AuthenticationSchemes ="")]

[AllowAnonymous]

Anonymous access is allowed, and the level is higher than [Authorize]. If both work at the same time, it will take effect [AllowAnonymous]

Authorization method

Basically, there are only three authorization methods: Policy, Role and Scheme, which correspond to the three attributes of the Authorize tag.

1 Policy

The recommended authorization method is in ASP Net core is the most mentioned official document. A Policy can contain multiple requirements (the requirements may be Role matching, Claims matching, or other methods.)

The following is a basic example (it is a basic example, mainly a Policy based authorization method, and some configurations can be added continuously):

In program CS, add two policies:

policy1 requires the user to have a Claim whose Claim type value is EmployeeNumber.

policy2 requires the user to have a Claim whose ClaimType value is EmployeeNumber and whose ClaimValue value is 1, 2, 3, 4 or 5.

builder.Services.AddAuthorization(options => {
    options.AddPolicy("policy1", policy => policy.RequireClaim("EmployeeNumber"));
    options.AddPolicy("policy2", policy => policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
})

Add the [Authorize] tag to the controller to take effect:

[Authorize(Policy = "policy1")]
public class TestController : ControllerBase

Or on the Action of the controller:

public class TestController : ControllerBase
{
    [Authorize(Policy = "policy1")]
    public ActionResult<string> GetTest => "GetTest";
}

2 Role

Based on role authorization, users can pass authorization verification as long as they have roles.

When authenticating, add a Claim related to the role to identify the role owned by the user (Note: a user can have multiple role claims), such as:

new Claim(ClaimTypes.Role, "admin"),
new Claim(ClaimTypes.Role, "user")

In Controller or Action:

[Authorize(Roles = "user")]
public class TestController : ControllerBase
{
    public ActionResult<string> GetUser => "GetUser";
    
    [Authorize(Roles = "admin")] //Superimposed with the Authorize function of the controller, in addition to having user, you also need to have admin
    public ActionResult<string> GetAdmin => "GetAdmin";
    
    [Authorize(Roles = "user,admin")] //Either user or admin can be satisfied
    public ActionResult<string> GetUserOrAdmin => "GetUserOrAdmin";
}

3 Scheme

Schemes such as Cookies and Bearer can also be customized.

Since this method is not commonly used, it is not expanded here. Please refer to the official documents Limit identification by scheme.

Policy based authorization

A basic example of policy based authorization has been mentioned above. We will continue to explore this authorization method.

1 authorization process

Before going deeper into the authorization of Policy, it is necessary to describe the authorization process. The description of the authorization process is suggested to be viewed in combination with the source code, so as to better understand its role. Of course, this part is difficult to understand, and the author's statement may not be clear enough, and this part will not have an impact on the completion of authorization configuration. Therefore, if the reader can't understand or understand it, he can skip it for the time being without entanglement.

I suggest you take a look ASP.NET Core authentication and authorization 6: how is the authorization policy implemented? In this article, the source code related to authorization is sorted out, and the relationship between them is explained.

Here is a brief summary:

The interface s and class es related to authorization are as follows:

IAuthorizationService #The main method of verifying authorized services is AuthorizeAsync
DefaultAuthorizationService #Default implementation of IAuthorizationService
IAuthorizationHandler #Be responsible for checking whether the requirements are met. The main method is HandleAsync
IAuthorizationRequirement #Only attributes, no methods; A mechanism for marking services and for tracking the success of authorization.
AuthorizationHandler<TRequirement> #Main method HandleRequirementAsync

The relationship between these interface s and class es and the authorization process are as follows:

DefaultAuthorizationService implements the AuthorizeAsync method of IAuthorizationService.

The AuthorizeAsync method will obtain all instances that implement IAuthorizationHandler, and call the HandleAsync method of all instances to check whether the authorization requirements are met. If any HandleAsync returns Fail, the cycle will end (please refer to the official document for details) Result returned by handler )And prohibit user access.

IAuthorizationHandler functions as described above, providing a HandleAsync method to check authorization.

IAuthorizationRequirement is a requirement, which is mainly used in conjunction with the authorization handler < trequirement >.

Authorizationhandler < TRequirement > implements the HandleAsync method of IAuthorizationHandler and provides a HandleRequirementAsync method. HandleRequirementAsync is used to check whether the requirements are met. The default implementation of HandleAsync is to obtain all requests that implement trequirements (and the request is added to the list by Policy). Call HandleRequirementAsync circularly to check which requirements can meet the authorization.

Briefly:

When the [Authorize] tag takes effect, the AuthorizeAsync of IAuthorizationService (implemented by DefaultAuthorizationService) is called.

AuthorizeAsync will call HandleAsync of all iauthorizationhandlers (implemented by authorizationhandler < trequirement >).

HandleAsync will call the method of HandleRequirementAsync of authorizationhandler < trequirement >.

Note: only the main interfaces and classes are listed here, but some are not listed, such as IAuthorizationHandlerProvider (the default implementation of this interface is DefaultAuthorizationHandlerProvider, which is mainly used to collect IAuthorizationHandler and return IEnumerable < IAuthorizationHandler >)

2 implementation description

IAuthorizationService has been implemented by default and no extra work is required.

IAuthorizationHandler is implemented by authorizationhandler < trequirement >.

So what we need to do is:

The first step is to prepare requirements and implement iauthorization requirements

Step 2: add a Handler program to inherit the authorizationhandler < trequirement > and override the HandleRequirementAsync method

For specific implementation, you can refer to ASP.NET Core authentication and authorization 7: dynamic authorization Based on the authorization of authority, the idea of this article has been very clear, and the main steps are listed here.

3. Define permission items

Before implementing the Requirement, we need to define some permission items, which are mainly used as the name of Policy and passed into the Requirement we implement.

public static class UserPermission
{
    public const string User = "User";
    public const string UserCreate = User + ".Create";
    public const string UserDelete = User + ".Delete";
    public const string UserUpdate = User + ".Update";
}

As above, permissions such as "add", "delete" and "modify" are defined, in which User will have full permissions.

4. Implement requirements

public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
    public PermissionAuthorizationRequirement(string name)
    {
        Name = name;
    }
    public string Name { get; set; }
}

Use the Name attribute to represent the Name of the permission, which corresponds to the constant in UserPermission.

5 implement authorization Handler

Here, it is assumed that the ClaimType in the user's Claim is Permission, such as:

new Claim("Permission", UserPermission.UserCreate),
new Claim("Permission", UserPermission.UserUpdate)

As above, identify the permissions of the user UserCreate and UserUpdate.

Note: of course, the actual program is certainly not implemented in this way. Here is just a simple example.

Next, implement an authorization Handler:

public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
    {
        var permissions = context.User.Claims.Where(_ => _.Type == "Permission").Select(_ => _.Value).ToList();
        if (permissions.Any(_ => _.StartsWith(requirement.Name)))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}

When running HandleRequirementAsync, the item with ClaimType of Permission in the user's Claim will be taken out and its Value will be obtained to form a list < string >.

Then verify whether the requirements meet the authorization requirements, and run context Succeeded.

6 add authorization handler

In program CS, add PermissionAuthorizationHandler to DI:

builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

7 add authorization policy

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy(UserPermission.UserCreate, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(UserPermission.UserCreate)));
    options.AddPolicy(UserPermission.UserUpdate, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(UserPermission.UserUpdate)));
    options.AddPolicy(UserPermission.UserDelete, policy => policy.AddRequirements(new PermissionAuthorizationRequirement(UserPermission.UserDelete)));
});

8 controller configuration

The controller is as follows:

[Route("api/[controller]/[action]")]
[ApiController]
public class UserController : ControllerBase
{
    [HttpGet]
    [Authorize(UserPermission.UserCreate)]
    public ActionResult<string> UserCreate() => "UserCreate";

    [HttpGet]
    [Authorize(UserPermission.UserUpdate)]
    public ActionResult<string> UserUpdate() => "UserUpdate";

    [HttpGet]
    [Authorize(UserPermission.UserDelete)]
    public ActionResult<string> UserDelete() => "UserDelete";
}

Based on the above assumptions, the user access interface is as follows:

/api/User/UserCreate #success
/api/User/UserUpdate #success
/api/User/UserDelete #403 no permission

So far, Policy based authorization has been basically completed.

The next content will be the improvement or supplement of some of the above contents.

Perfect: implement policy provider PolicyProvider

Generally, the following authorization policies are added in the program CS, as follows:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("policy", policy => policy.RequireClaim("EmployeeNumber"));
});

Via authorizationoptions Addpolicy adding authorization policy is not flexible and cannot be added dynamically.

By implementing IAuthorizationPolicyProvider and adding it to DI, you can dynamically add policies.

The default implementation of IAuthorizationPolicyProvider is DefaultAuthorizationPolicyProvider.

Implement a PolicyProvider as follows:

public class TestAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider, IAuthorizationPolicyProvider
{
    public Test2AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : base(options) {}

    public new Task<AuthorizationPolicy> GetDefaultPolicyAsync()
        => return base.GetDefaultPolicyAsync();

    public new Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
        return base.GetFallbackPolicyAsync();

    public new Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
    {
        if (policyName.StartsWith(UserPermission.User))
        {
            var policy = new AuthorizationPolicyBuilder("Bearer");
            policy.AddRequirements(new PermissionAuthorizationRequirement(policyName));
            return Task.FromResult<AuthorizationPolicy?>(policy.Build());
        }
        return base.GetPolicyAsync(policyName);
    }
}

Note: the customized TestAuthorizationPolicyProvider must implement IAuthorizationPolicyProvider, otherwise it will not take effect when added to DI.

In program CS, add the custom PolicyProvider to DI:

builder.Services.AddSingleton<IAuthorizationPolicyProvider, TestAuthorizationPolicyProvider>();

Note: only the last added PolicyProvider will take effect.

Supplement: Custom AuthorizationMiddleware

Custom AuthorizationMiddleware can:

  • Return customized response
  • Enhance (or change) the default challenge or forbid response

Please refer to the official documents for details Customize the behavior of AuthorizationMiddleware

Supplement: authorization of MiniApi

In MiniApi, almost all of them are branch nodes in the shape of MapGet(). Such endpoints cannot use the [Authorize] label and can be authorized by RequireAuthorization("Something"), such as:

app.MapGet("/helloworld", () => "Hello World!")
    .RequireAuthorization("AtLeast21");

Reference source

ASP.NET Core 6.0 official documents: Authorization policy provider

. NET 6 steps to use JWT Bearer authentication and authorization

ASP.NET Core authentication and authorization 6: how is the authorization policy implemented? (mark: this one is very strong!)

ASP.NET Core authentication and authorization 7: dynamic authorization

Posted by jdc44 on Sat, 16 Apr 2022 10:12:23 +0930