ASP. Net core application JWT for user authentication and token refresh scheme

Time:2022-5-9

This paper will use practical examples to demonstrate how to use ASP Net core using JWT for user authentication and token refresh scheme

1、 What is JWT?

Based on the open standard (RFC 7519), JWT (JSON web token) is a stateless distributed authentication method, which is mainly used to securely transfer claims between network application environments. It is based on JSON, so it can be used in the same way as JSON Net, Java, JavaScript, PHP and other languages.

Why use JWT?

Traditional web applications generally use cookies + session for authentication. However, for more and more apps, applets and other applications, their corresponding server is generally restful stateless API, so it is not very convenient to adopt such authentication method. JWT, a stateless distributed authentication method, just meets this requirement.

2、 Composition of JWT:

What does JWT look like? It is a string like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJuYmYiOjE1NjU5MjMxMjIsImV4cCI6MTU2NTkyMzI0MiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1NDIxNCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTQyMTUifQ.Mrta7nftmfXeo_igBVd4rl2keMmm0rg0WkqRXoVAeik

It consists of three “garbled” strings through two “.” Connected together. Official websitehttps://jwt.io/Provides its verification method

Its three strings correspond to the header, payload and signature on the right side of the figure above.

Header


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

The identification encryption method is hs256 and the token type is JWT. This JSON is encoded by base64url to form the first string of the above example

Payload

Payload is the information storage part of JWT, which contains many kinds of claims.

You can customize multiple declarations and add them to the payload. The system also provides some default types

  1. ISS (issuer): issuer
  2. Exp (expiration time): expiration time
  3. Sub (subject): subject
  4. Aud (audience): audience
  5. NBF (not before): effective time
  6. IAT (issued at): issuing time
  7. JTI (JWT ID): No

This part generates the second string through base64url encoding.

Signature

Signature is used for token verification. Its value is similar to the expression: Signature = hmacsha256 (base64urlencode (header) + “+ Base64urlencode (payload, secret), that is, it is a new string generated by encrypting the first two strings.

Therefore, only those who have the same encryption key can obtain the same string through the first two strings, which ensures the authenticity of the token.

3、 Certification process

The general process is as follows:

  • Authentication server: used for user login authentication and token issuance.
  • Application server: business data interface. Protected API.
  • Client: generally app, applet, etc.

Certification process:

  • First, the user logs in to the authentication server to obtain a token.
  • When accessing the API of the application server, place the obtained token in the request header.
  • The application server verifies the token and returns the corresponding result after passing.

Note: This is only an example scheme, which may be different in the actual project.

  • For small projects, authentication services and application services may be combined. This example is implemented separately, so that we can better understand the authentication process between the two.
  • For 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 introduce it too much here. The following is a practical example to see how it works in ASP Net core.

4、 Application examples

The figure in the previous section: “JWT authentication process” involves three parts: client, authentication server and application server. The following is an example to simulate these three parts:

  • Authentication server: create a new webapi solution called flylolo JWT. Server。
  • Application server: create a new webapi solution named flylolo JWT. API。
  • Client: Here we use Fiddler to send a request for testing.

Certification services

First, create an ASP Net core solution webapi solution

Name it flylolo JWT. Server。

First, create a tokencontroller for login and token issuance:


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

It has an action named get to receive the submitted user name and password and verify them. After the verification is passed, it calls the createtoken method of tokenhelper to generate a token return.

User and tokenhelper classes are involved here.

User related:


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

Since it is only a demo, the user class only contains the above three fields. The simulated data of user is made in the temporarydata class

/// <summary>
    ///Virtual data, which simulates reading users from the database or cache
    /// </summary>
    public static class TemporaryData
    {
        Private static list < user > users = new list < user > () {new user {code = "001", name = "Zhang San", password = "111111"}, new user {code = "002", name = "Li Si", password = "222222"}};

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

This is only simulation data, which should be read from the database or cache in the actual project.

TokenHelper:


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 };
        }
    }

Create a token through the createtoken method. Here are several key parameters:

  • Issuer token publisher
  • Audience token recipient
  • Expires # expiration time
  • Issuersigningkey: signature key

The corresponding token code is as follows:


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

        public DateTime Expires { get; set; }
    }

In this way, a token is generated through the createtoken method of tokenhelper and returned to the client. So far, it seems that all the work has been completed. No, we need to make some settings in the startup file.

public class Startup
{// 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();
    }
}

The configuration information is used here, which can be found in Appsettings The authentication information is configured in JSON as follows:


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

Run this project and access API / token through fidder in get mode? Code = 002 & PWD = 222, the returned result is as follows:

{“tokenContent”:”eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJuYmYiOjE1NjY3OTg0NzUsImV4cCI6MTU2NjgwMDI3NSwiaXNzIjoiRmx5TG9sbyIsImF1ZCI6IlRlc3RBdWRpZW5jZSJ9.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 business API.


[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 ...");
    }
}

The [authorize] flag 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 identified with [allowanonyous], indicating that the authentication can be skipped when accessing this action.

Configure authentication in the startup file:

public class Startup
{
//Omit some codes
    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();
    }
}

Configuration is also used here:


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

appsettings.json:


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

JWT certified, here Tokenvalidationparameters set the authentication information. The three parameters validussuer, valiaudience and issuersigningkey are used to verify the issuer, audience and issuersigningkey filled in when the token is generated, so the value should be consistent with the setting when the token is generated.

The default value of clockskew is 5 minutes. It is a buffer period. For example, the validity period of token is set to 30 minutes. It will not expire at 30 minutes. There will be such a buffer time, that is, 35 minutes. In order to facilitate the test (I don’t want to wait too long), I set 1 minute here.

Tokenvalidationparameters has some other parameters, which have been set by default in its construction method. The code is as follows:

public TokenValidationParameters()
{
    RequireExpirationTime = true;  
    RequireSignedTokens = true;    
    SaveSigninToken = false;
    ValidateActor = false;
    ValidateAudience = true;  // Verify recipient
    ValidateIssuer = true;   // Verify publisher
    ValidateIssuerSigningKey = false;  // Verify secret key
    ValidateLifetime = true; // Verify expiration time
    ValidateTokenReplay = false;
 }

Access API / book and return the result normally

[“ASP”,”C#”]

Access through post and return 401 error.

You need to use the obtained token to access it again, as shown in the following figure

Headers such as “authorization: bearer token content” are added and can be accessed normally.

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

There may be a question here, for example:

  • 1. What if the token is stolen?
    A: when HTTPS is enabled, it is relatively safe 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 valid period of the token for the web page development of wechat official account is 2 hours).
  • 2. How to handle the expired token?
    A: theoretically, when the token expires, you should jump to the login interface, but it’s too unfriendly. You can request a new token periodically in the background according to the expiration time of the token. The next section demonstrates the refresh scheme of token.

5、 Refresh of token

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

  • When the user logs in successfully, he will be given two tokens at one time, namely accesstoken and refreshtoken. Accesstoken is used for normal requests, that is, the original token in the above example. Refreshtoken is used as the voucher for refreshing accesstoken.
  • The validity period of accesstoken is short, for example, one hour, which is shorter and safer. The validity period of refreshtoken can be set to be longer, such as one day, one week, etc.
  • When the accesstoken is about to expire, for example, five minutes in advance, the client uses the API specified by the refreshtoken request to obtain a new accesstoken and update the accesstoken in the local storage.

So just modify flylolo JWT. Server.

First, modify the return scheme of token and add a new model


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

It includes accesstoken and refreshtoken, which are used to return the token result after the user logs in successfully.

Modify Appsettings JSON, add two configuration items:


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

Refreshtoken expiresminutes is used to set the expiration time of refreshtoken, which is set to 7 days. Refreshtokenaudience is used to set the receiver of refreshtoken, which is inconsistent with the original audience value. Its purpose is to make refreshtoken unable to be used to access the business API of application services, while accesstoken cannot be used to refresh token.

Modify tokenhelper:

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 user's claim token is used to test the following two roles, and the corresponding information is stored in the claim token JWT. The put methods of the two test controllers of the API 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, the expiration time of accesstoken and refreshtoken is different, and the claims content of them in [actual project] may be different.
        ///Because refreshtoken is only used to refresh accesstoken, its content can be simpler.
        ///The 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 times
            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;
            }
        }
    }

After logging in, two tokens are generated and returned to the client. A refreshtoken method is added to the tokenhelper to generate a new accesstoken. Correspondingly, add an action named post in tokencontroller to call this refreshtoken method to refresh the token


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

This method adds the [authorize] flag, which indicates that it needs refreshtoken authentication to call it. Now that authentication is enabled, you need to configure JWT authentication in the startup file like the business API in the above example.

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

Note that valiaudience here is assigned to config Refreshtokenaudience, and flylolo JWT. Inconsistencies in the API are used to prevent the mixing of accesstoken and refreshtoken.

Visit / API / token again? Code = 002 & PWD = 222, two tokens will be returned:

{“accessToken”:{“tokenContent”:”eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiVGVzdFB1dEJvb2tSb2xlIiwiVGVzdFB1dFN0dWRlbnRSb2xlIl0sIm5iZiI6MTU2NjgwNjQ3OSwiZXhwIjoxNTY2ODA4Mjc5LCJpc3MiOiJGbHlMb2xvIiwiYXVkIjoiVGVzdEF1ZGllbmNlIn0.wlMorS1V0xP0Fb2MDX7jI7zsgZbb2Do3u78BAkIIwGg”,”expires”:”2019-08-26T22:31:19.5312172+08:00″},

“refreshToken”:{“tokenContent”:”eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiVGVzdFB1dEJvb2tSb2xlIiwiVGVzdFB1dFN0dWRlbnRSb2xlIl0sIm5iZiI6MTU2NjgwNjQ3OSwiZXhwIjoxNTY3NDExMjc5LCJpc3MiOiJGbHlMb2xvIiwiYXVkIjoiUmVmcmVzaFRva2VuQXVkaWVuY2UifQ.3EDi6cQBqa39-ywq2EjFGiM8W2KY5l9QAOWaIDi8FnI”,”expires”:”2019-09-02T22:01:19.6143038+08:00″}}

You can use refreshtoken to request a new accesstoken

The accesstoken used in the test can normally access flylolo JWT. API, but not with refreshtoken.

So far, the refresh function transformation of token has been completed. Code address:https://github.com/FlyLolo/JWT.Demo/releases/tag/1.1

Question: the validity period of refreshtoken is so long. What if it is stolen? What’s the difference between directly extending the validity period of accesstoken?

Personally, I think:

  • 1. Refreshtoken is not used in most requests like accesstoken.
  • 2. There are many APIs of application classes, and there may be many corresponding servers (servers), so the probability of leakage is higher.

The above is the whole content of this article. I hope it will be helpful to your study, and I hope you can support developpaer.