Jwt identity authentication in ASP.NET CORE

Why use JWT?
Traditional Web applications generally use Cookies + Session for authentication. However, for more and more applications, applets and other applications, their corresponding servers are generally RestFul type stateless APIs. It is not very convenient to adopt such authentication methods. The JWT stateless distributed authentication method just meets this need.

 

Differences between HS256 and RS256 signature algorithms:

In the JWT signature algorithm, there are generally two options, one uses HS256, and the other uses RS256.
The signature is actually an encrypted process, generating a piece of identification (also part of the JWT) as a basis for the recipient to verify whether the information has been tampered with.

RS256 (RSA signature with SHA-256) is an asymmetric algorithm that uses a public / private key pair: the identity provider uses the private key to generate the signature, and the user of the JWT obtains the public key to verify the signature. Since the public key (compared to the private key) does not require protection, most identification providers make it easy for users to obtain and use (usually through a metadata URL).
On the other hand, HS256 (HMAC with SHA-256 is a symmetric algorithm, and only one key is shared between the two parties. Since the same key is used to generate and verify signatures, care must be taken to ensure that the key is not compromised.

Enable JWT when developing applications, use RS256 is more secure, you can control who can use what type of key. In addition, if you can't control the client and cannot keep the key completely secret, RS256 will be a better choice. The user of JWT only needs to know the public key.

Second, the composition of JWT:

What does JWT look like? It is a string such as the following:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJuYmYiOjE1NjU5MjMxMjIsImV4cCI6MTU2NTkyMzI0MiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1NDIxNCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTQyMTUifQ.Mrta7nftmfXeo_igBVd4rl2keMmm0rg0WkqRXoVAeik
"." It is composed of three segments "distortion" connected together to form two strings. The official website https://jwt.io/ provides its verification method

 

 


Its three strings correspond to the three parts of Header, Payload and Signature on the right side of the figure above.

Header:

Header:
{
"alg": "HS256", 
"typ": "JWT"
}

The logo encryption method is HS256, and the token type is JWT. This JSON is encoded by Base64Url to form the first string in the above example.

Payload

Payload is the information storage part of JWT, which contains many kinds of claims.
You can add multiple declarations to Payload. The system also provides some default types
iss (issuer): issuer
exp (expiration time): expiration time
sub (subject): subject
aud (audience): audience
nbf (Not Before) ): Effective time
iat (Issued At): Issuing time
jti (JWT ID): Number

This part generates the second string through Base64Url encoding.

Signature

Signature is used for token verification. Its value is similar to this expression: Signature = HMACSHA256 (base64UrlEncode (header) + "." + Base64UrlEncode (payload), secret), that is, it is a new character generated by encrypting the first two strings string.

Therefore, only people with the same encryption key can obtain the same string through the first two strings, which guarantees the authenticity of the token.

Third, the certification process

The general process is this:

  1. Authentication server: used for user login verification and token issuance.
  2. Application server: business data interface. Protected API.
  3. Client: generally APP, applet, etc.

Certification process:

  1.  The user first obtains a token by logging into the authentication server.
  2. When accessing the API of the application server, the obtained Token is placed in the request header.
  3. The application server verifies the token and returns the corresponding result after passing.

Note: This is only an example solution, and may differ in actual projects.

  1. For small projects, the authentication service and application service may be together. This example is implemented in a separate way, so that we can better understand the authentication process between the two.
  2. For more complex projects, there may be multiple application services, and the tokens obtained by users can be authenticated in multiple distributed services, which is also one of the advantages of JWT.

 

There are many articles about JWT, so I won't do too much introduction here. Let's see how it is applied in ASP.NET Core through practical examples.

4. Application Examples

The figure in the previous section: "JWT authentication process" involves three parts: client, authentication server, and application server. The following three examples are used to simulate these three parts:

  1. Authentication server: Create a new WebApi solution named FlyLolo.JWT.Server.
  2. Application server: Create a new WebApi solution called FlyLolo.JWT.API.
  3. Client: Fiddler is used here to send a request for testing.

Certification Services

First create a new ASP.NET Core solution WebApi solution 

Name it FlyLolo.JWT.Server.

First create a new TokenController for login and token issuance:

Copy code
[Route("api/[controller]")]
public class TokenController : Controller
{
    private ITokenHelper tokenHelper = null;
    public TokenController(ITokenHelper _tokenHelper)
    {
        tokenHelper = _tokenHelper;
    }
    [HttpGet]
    public IActionResult Get(string code, string pwd)
    {
        User user = TemporaryData.GetUser(code);
        if (null != user && user.Password.Equals(pwd))
        {
            return Ok(tokenHelper.CreateToken(user));
        }
        return BadRequest();
    }
}
Copy code

 It has an Action named Get to receive the submitted user name and password, and verify it. After verification, call the CreateToken method of TokenHelper to generate a Token return.

Two classes, User and TokenHelper, are involved here.

User related:

Copy code
public class User
{
    public string Code { get; set; }
    public string Name { get; set; }
    public string Password { get; set; }
}
Copy code

Because it is only Demo, the User class contains only the above three fields. In the TemporaryData class did the simulated data of User

Copy code
    /// <summary>
    /// Virtual data, which simulates reading users from a database or cache
    /// </summary>
    public static class TemporaryData
    {
        private static List<User> Users = new List<User>() { new User { Code = "001", Name = "张三", Password = "111111" }, new User { Code = "002", Name = "李四", Password = "222222" } };

        public static User GetUser(string code)
        {
            return Users.FirstOrDefault(m => m.Code.Equals(code));
        }
    }
Copy code

This is just simulated data, and it should be read from the database or cache in the actual project.

TokenHelper:

Copy code
public class TokenHelper : ITokenHelper
    {
        private IOptions<JWTConfig> _options;
        public TokenHelper(IOptions<JWTConfig> options)
        {
            _options = options;
        }

        public Token CreateToken(User user)
        {
            Claim[] claims = { new Claim(ClaimTypes.NameIdentifier,user.Code),new Claim(ClaimTypes.Name,user.Name) };

            return CreateToken(claims);
        }
        private Token CreateToken(Claim[] claims)
        {
            var now = DateTime.Now;var expires = now.Add(TimeSpan.FromMinutes(_options.Value.AccessTokenExpiresMinutes));
            var token = new JwtSecurityToken(
                issuer: _options.Value.Issuer,
                audience: _options.Value.Audience,
                claims: claims,
                notBefore: now,
                expires: expires,
                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256));
            return new Token { TokenContent = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires };
        }
    }
Copy code

    Create a Token through the CreateToken method, here are a few key parameters:

  1. issuer Token issuer
  2. Audience Token recipient
  3. expires
  4. IssuerSigningKey signature key

The corresponding Token code is as follows:

Copy code
    public class Token
    {
        public string TokenContent { get; set; }

        public DateTime Expires { get; set; }
    }
Copy code

In this way, a token is generated by the CreateToken method of TokenHelper and returned to the client. By now, it seems that all the work has been completed. Not so, we also need to do some settings in the Startup file.

Copy code
public class Startup
{ 
//. . . . . . Part of the code is omitted here
public void ConfigureServices(IServiceCollection services) {
// Read configuration information services.AddSingleton<ITokenHelper, TokenHelper>(); services.Configure<JWTConfig>(Configuration.GetSection("JWT")); // Enable JWT services.AddAuthentication(Options => { Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }). AddJwtBearer(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); }
// Enable authentication middleware app.UseAuthentication(); app.UseMvc(); } }
Copy code

 The configuration information is used here. Configure the authentication information in appsettings.json as follows:

Copy code
  "JWT": {
    "Issuer": "FlyLolo",
    "Audience": "TestAudience",
    "IssuerSigningKey": "FlyLolo1234567890",
    "AccessTokenExpiresMinutes": "30"
  }
Copy code

 

Run this project, and access api / token? Code = 002 & pwd = 222222 through Getter through Getter.

{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8
yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL
3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJuYmYiOjE1NjY3OTg0NzUsImV4cCI6MTU2NjgwMDI
3NSwiaXNzIjoiRmx5TG9sbyIsImF1ZCI6IlRlc3RBdWRpZW5jZSJ9.BVf3gOuW1E9RToqKy8XXp8uIvZKL-lBA-q9fB9QTEZ4",
"expires":"2019-08-26T21:17:55.1183172+08:00"}

 

 The client successfully logs in and returns a token, and the authentication service is created

Application service

Create a new WebApi solution called FlyLolo.JWT.API.

Add BookController as a business API.

Copy code
[Route("api/[controller]")]
[Authorize]
public class BookController : Controller
{
    // GET: api/<controller>
    [HttpGet]
    [AllowAnonymous]
    public IEnumerable<string> Get()
    {
        return new string[] { "ASP", "C#" };
    }

    // POST api/<controller>
    [HttpPost]
    public JsonResult Post()
    {
        return new JsonResult("Create  Book ...");
    }
}
Copy code

 The [Authorize] logo is added to this Controller, indicating that the Action of this Controller needs to be authenticated when it is accessed, and its Action named Get is marked [AllowAnonymous], indicating that access to this Action can skip authentication.

Configure authentication in the Startup file:

Copy code
public class Startup
{
// Omit part of the code
    public void ConfigureServices(IServiceCollection services)
    {
        #region Read configuration
        JWTConfig config = new JWTConfig();
        Configuration.GetSection("JWT").Bind(config);
        #endregion

        #region Enable JWT authentication
        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).
        AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = config.Issuer,
                ValidAudience = config.Audience,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.IssuerSigningKey)),
                ClockSkew = TimeSpan.FromMinutes (1)
            };
        });
        #endregion

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    }

    // 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();
        }
        app.UseAuthentication();
        app.UseMvc();
    }
}
Copy code

 The configuration is also used here:

Copy code
    public class JWTConfig
    {
        public string Issuer { get; set; }
        public string Audience { get; set; }
        public string IssuerSigningKey { get; set; }
        public int AccessTokenExpiresMinutes { get; set; }
    }
Copy code

 appsettings.json:

Copy code
  "JWT": {
    "Issuer": "FlyLolo",
    "Audience": "TestAudience",
    "IssuerSigningKey": "FlyLolo1234567890",
    "AccessTokenExpiresMinutes": "30"
  }
Copy code

 Regarding JWT authentication, the authentication information is set here through options.TokenValidationParameters. The three parameters ValidIssuer, ValidAudience, and IssuerSigningKey are used to verify the Issuer, Audience, and IssuerSigningKey that are filled in when the Token is generated, so the value must be the same as the setting when generating the Token .

The default value of ClockSkew is 5 minutes, which is a buffer period. For example, the validity period of the Token setting is 30 minutes, and it will not expire after 30 minutes. There will be such a buffer time, which is 35 minutes. For the convenience of testing (I don't want to wait too long), I set it here for 1 minute.

TokenValidationParameters has some other parameters. The default settings have been made in its constructor. The code is as follows:

Copy code
public TokenValidationParameters()
{
    RequireExpirationTime = true;  
    RequireSignedTokens = true;    
    SaveSigninToken = false;
    ValidateActor = false;
    ValidateAudience = true; // Whether to validate the recipient
    ValidateIssuer = true; // Whether to verify the publisher
    ValidateIssuerSigningKey = false; // Whether to verify the secret key
    ValidateLifetime = true; // Whether to validate the expiration time
    ValidateTokenReplay = false;
 }
Copy code

 Visit api / book and return the result normally

["ASP","C#"]

 Access by POST, return 401 error.

This requires using the obtained Toke, and visit again as shown below

Header like "Authorization: bearer Token content" has been added and can be accessed normally.

At this point, a simple JWT authentication example is completed, the code address is https://github.com/FlyLolo/JWT.Demo/releases/tag/1.0 .

There may be a question here, for example:

   1. What should I do if the token is stolen?

    Answer: When Https is enabled, it is safer to put the token in the header. In addition, the validity period of the token should not be set too long. For example, it can be set to 1 hour (the validity period of the Token developed on the WeChat public account is 2 hours).

   2. How to deal with the expired token?

   Answer: Theoretically, the token expired should jump to the login interface, but this is too unfriendly. You can periodically request new tokens in the background according to the token expiration time. The next section will demonstrate the Token refresh plan.

V. Token refresh

   In order to enable the client to obtain a new token, the above example is modified, the general idea is as follows:

  1. When the user logs in successfully, he is given two Tokens at a time, namely AccessToken and RefreshToken. AccessToken is used for normal request, which is the original Token in the above example, and RefreshToken is used as the certificate to refresh AccessToken.
  2. The validity period of AccessToken is short, for example, one hour, a little shorter and safer. RefreshToken validity period can be set longer, such as one day, one week, etc.
  3. When AccessToken is about to expire, for example, 5 minutes in advance, the client uses RefreshToken to request the specified API to obtain a new AccessToken and update the AccessToken in the local storage.

So only need to modify FlyLolo.JWT.Server.

First modify the token return scheme, add a Model

    public class ComplexToken
    {
        public Token AccessToken { get; set; }
        public Token RefreshToken { get; set; }
    }

Contains AccessToken and RefreshToken, used to return the Token result after the user login successfully.

Modify appsettings.json and add two configuration items:

    "RefreshTokenAudience": "RefreshTokenAudience", 
    "RefreshTokenExpiresMinutes": "10080" //60*24*7

 

RefreshTokenExpiresMinutes is used to set the expiration time of RefreshToken, which is set here for 7 days. RefreshTokenAudience is used to set the recipient of RefreshToken, which is inconsistent with the original Audience value. The function is to prevent RefreshToken from being used to access the business API of the application service, and AccessToken cannot be used to refresh the Token.

Modify TokenHelper:

Copy code
    public enum TokenType
    {
        AccessToken = 1,
        RefreshToken = 2
    }
    public class TokenHelper : ITokenHelper
    {
        private IOptions<JWTConfig> _options;
        public TokenHelper(IOptions<JWTConfig> options)
        {
            _options = options;
        }

        public Token CreateAccessToken(User user)
        {
            Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Code), new Claim(ClaimTypes.Name, user.Name) };

            return CreateToken(claims, TokenType.AccessToken);
        }

        public ComplexToken CreateToken(User user)
        {
            Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Code), new Claim(ClaimTypes.Name, user.Name)
                // The following two Claims are used to test the storage of the user's role information in the Token, corresponding to the Put method of the two test Controllers tested in FlyLolo.JWT.API, which can be deleted if they are not used
                , new Claim(ClaimTypes.Role, "TestPutBookRole"), new Claim(ClaimTypes.Role, "TestPutStudentRole")
            };

            return CreateToken(claims);
        }

        public ComplexToken CreateToken(Claim[] claims)
        {
            return new ComplexToken { AccessToken = CreateToken(claims, TokenType.AccessToken), RefreshToken = CreateToken(claims, TokenType.RefreshToken) };
        }

        /// <summary>
        /// Used to create AccessToken and RefreshToken.
        /// Here AccessToken and RefreshToken are only different in expiration time, the contents of the claims in [Actual Project] may be different.
        /// Because RefreshToken is only used to refresh AccessToken, its content can be simpler.
        /// And AccessToken may attach some other Claims.
        /// </summary>
        /// <param name="claims"></param>
        /// <param name="tokenType"></param>
        /// <returns></returns>
        private Token CreateToken(Claim[] claims, TokenType tokenType)
        {
            var now = DateTime.Now;
            var expires = now.Add (TimeSpan.FromMinutes (tokenType.Equals (TokenType.AccessToken)? _options.Value.AccessTokenExpiresMinutes: _options.Value.RefreshTokenExpiresMinutes)); // Set different expiration
            var token = new JwtSecurityToken(
                issuer: _options.Value.Issuer,
                audience: tokenType.Equals (TokenType.AccessToken)? _options.Value.Audience: _options.Value.RefreshTokenAudience, // Set different recipients
                claims: claims,
                notBefore: now,
                expires: expires,
                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256));
            return new Token { TokenContent = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires };
        }

        public Token RefreshToken(ClaimsPrincipal claimsPrincipal)
        {
            var code = claimsPrincipal.Claims.FirstOrDefault(m => m.Type.Equals(ClaimTypes.NameIdentifier));
            if (null != code )
            {
                return CreateAccessToken(TemporaryData.GetUser(code.Value.ToString()));
            }
            else
            {
                return null;
            }
        }
    }
Copy code

 

After logging in, two tokens are generated and returned to the client. Added a RefreshToken method in TokenHelper to generate a new AccessToken. Corresponding to add an Action named Post in TokenController, used to call this RefreshToken method to refresh Token

Copy code
[HttpPost]
[Authorize]
public IActionResult Post()
{
    return Ok(tokenHelper.RefreshToken(Request.HttpContext.User));
}
Copy code

This method adds the [Authorize] logo, indicating that calling it requires RefreshToken authentication. Now that authentication is enabled, JWT authentication configuration needs to be done in the Startup file like the business API in the example above.

Copy code
        public void ConfigureServices(IServiceCollection services)
        {
            #region Read configuration information
            services.AddSingleton<ITokenHelper, TokenHelper>();
            services.Configure<JWTConfig>(Configuration.GetSection("JWT"));
            JWTConfig config = new JWTConfig();
            Configuration.GetSection("JWT").Bind(config);
            #endregion

            #region Enable JWT
            services.AddAuthentication(Options =>
            {
                Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).
             AddJwtBearer(options =>
             {
                 options.TokenValidationParameters = new TokenValidationParameters
                 {
                     ValidIssuer = config.Issuer,
                     ValidAudience = config.RefreshTokenAudience,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.IssuerSigningKey))
                 };
             });
            #endregion

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }
Copy code

 

 Note that the ValidAudience is assigned to config.RefreshTokenAudience, which is inconsistent with FlyLolo.JWT.API, used to prevent the mixed use of AccessToken and RefreshToken.

Visit / api / token? Code = 002 & pwd = 222222 again and two tokens will be returned:

Copy code
{"accessToken":{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8y
MDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUva
WRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW
1zL3JvbGUiOlsiVGVzdFB1dEJvb2tSb2xlIiwiVGVzdFB1dFN0dWRlbnRSb2xlIl0sIm5iZiI6MTU2NjgwNjQ3OSwiZXhwIjoxNTY2ODA4Mjc5LCJ
pc3MiOiJGbHlMb2xvIiwiYXVkIjoiVGVzdEF1ZGllbmNlIn0.wlMorS1V0xP0Fb2MDX7jI7zsgZbb2Do3u78BAkIIwGg",
"expires":"2019-08-26T22:31:19.5312172+08:00"},

"refreshToken":{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8y
MDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUva
WRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW
1zL3JvbGUiOlsiVGVzdFB1dEJvb2tSb2xlIiwiVGVzdFB1dFN0dWRlbnRSb2xlIl0sIm5iZiI6MTU2NjgwNjQ3OSwiZXhwIjoxNTY3NDExMjc5LCJ
pc3MiOiJGbHlMb2xvIiwiYXVkIjoiUmVmcmVzaFRva2VuQXVkaWVuY2UifQ.3EDi6cQBqa39-ywq2EjFGiM8W2KY5l9QAOWaIDi8FnI",
"expires":"2019-09-02T22:01:19.6143038+08:00"}}
Copy code

 

 You can use RefreshToken to request a new AccessToken

 

AccessToken for testing can access FlyLolo.JWT.API normally, but RefreshToken is not.

At this point, the refresh function of Token is completed. Code address: https://github.com/FlyLolo/JWT.Demo/releases/tag/1.1

Question: The validity period of RefreshToken is so long, what should I do if it is stolen, and what is the difference between extending the validity period of AccessToken directly?

Personally think: 1. RefreshToken is not used in most requests like AccessToken. 2. There are many application APIs, and there may be many corresponding services (devices), so the probability of leakage is greater.

 

turn:

https://www.cnblogs.com/FlyLolo/p/ASPNETCore2_26.html

https://www.cnblogs.com/FlyLolo/p/ASPNETCore2_27.html

Guess you like

Origin www.cnblogs.com/fanfan-90/p/12747382.html