Before you start, if you don’t understand cookie-based identity authentication, it is recommended that you read “Cookie-based Identity Authentication” before reading this article.

2025/04/3001:38:50 hotcomm 1896

Before you start, if you don’t understand the identity authentication based on cookie, it is recommended that you read "Cookie-based Identity Authentication" before reading this article.

Jwt Overview What is

Jwt

Jwt is an open industry standard (RFC7519), which is json Web Token in English, translated as "Json Network Token", which can pass claims between parties in a compact and URL-safe way.

In Jwt, the declaration will be encoded as a Json object, used as a payload for the Jws(Json Web Signature) structure, or as a plaintext for the Jwe(Json Web Encryption) structure, which allows the declaration to be digitally signed or integrity protection and encryption using MAC (Message Authentication Code).

For more information, please visit https://jwt.io/
If you have doubts about jwt, jws, and jwe, please refer to "A post to tell you what JWT, JWS and JWE"

Jwt solves what problems

Cross-site

Traditional cookies can only implement cross-domain, but not cross-site (such as my.abc.com and you.xyz.com). Jwt natively supports cross-domain and cross-site, because it requires that each request, it must carry tokens in the request header.

cross-server

When the current application is basically clustered, if the traditional cookie + session authentication method is used, in order to realize session cross-server sharing, it is also necessary to introduce distributed cache middleware. Jwt does not require distributed cache middleware because it can not be stored on the server side.

Native App-friendly

For native platforms (such as iOS, Android, WP), cookies lose their advantages, and using Jwt is very simple.

Jwt structure

First look at a Jwt example:

plaintextml2

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwO i8vbG9jYWxob3N0OjUwMDAiLCJpYXQiOjE2NDI3NDg5OTIsIm5iZiI6MTY0Mjc0ODk5MiwiZXhwIjoxNjQyNzQ4OTkyLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJuYW1lIjoieGlhb3h pYW90YW5rIn0.nqJpZl48gnP4fv7NdsSD9JOn0VWq045Zcbmb91HMhwY

seems like a very long piece of meaningless garbled code, but be careful, you will find that it is marked (.) Separated into 3 parts, it looks like this:

xxxxx.yyyyy.zzzz

From left to right, these three parts are called: header (Header), payload (Payload) and signature (Signature).

header (Header)

Header is mainly used to illustrate the token type and signature algorithm.

json

{ "alg": "HS256","typ": "JWT",}
  • alg: Signature algorithm, here is HMAC SHA256
  • typ: token type, here is JWTh

After removing all newlines and spaces to the header, you get: {"alg":"HS256","typ":"JWT"}, and then Base64Url encoding is performed. You can get the part 1 of the token :

plaintext tml2

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

payload (Payload)

Payload is the core and is mainly used to store declaration information, such as token issuer, user ID, user role, etc.

json

{ "iss": "http://localhost:5000", "iat": 1642748992, "nbf": 1642748992, "exp": 1642748992, "aud": "http://localhost:5000", "name": "xiaoxiaotank"}

Among them, the first five are predefined:

  • iss: Issuer, that is, the issuer of the token.
  • iat: Issued At, that is, the issuance time of the token
  • exp: Expiration Time, that is, the expiration time of the token
  • aud: Audience, that is, the audience, refers to which group the token serves (group scope), or which piece of the authorized resource granted by the token (uri of the resource)
  • nbf: Not Before, that is, the token is not available before the specified time point

In fact, the declarations in Jwt can be divided into the following three types:

  • Registered Claim: Predefined declarations, although not mandatory, they are recommended, including iss(Issuer), sub(Subject), aud(Audience), exp(Expiration Time), nbf(Not Before), iat(Issued At) and jti(JWT ID). As you can see, these declaration names are very short, because Jwt's core goal is to make the representation compact.
  • Public Claim: Public declaration, users of Jwt can define it at will, but they should avoid conflicts with predefined declarations.
  • Private Claim: Private statement, unlike public statements, private statement names may conflict and should be used with caution.

Base64Url encoding for Payload (remember to remove all newlines and spaces) can be obtained token's part 2 :

plaintextml2

eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJpYXQiOjE2NDI3NDg 5OTIsIm5iZiI6MTY0Mjc0ODk5MiwiZXhwIjoxNjQyNzQ4OTkyLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJuYW1lIjoieGlhb3hpYW90YW5rIn0

Do not store any sensitive information in the Payload, because Base64Url is not encryption, it is just encoding, so this part is plaintext for the client.

signature (Signature)

Signature is mainly used to prevent tokens from being tampered with. When the server obtains the token, the signature will be calculated according to the following algorithm. If the calculated signature is consistent with the signature in the token, it is believed that the token has not been tampered with.

Signature algorithm:

  • First connect the header and Payload through dots (.), that is, the header encoded by Base64Url. Payload is denoted as texttml17
  • and then encrypt the text using the signature algorithm specified in the header to obtain a binary array, denoted as signBytes
  • and finally signBytes Perform Base64Url encoding and obtain signature, that is, the third part of token

plaintextml2

nqJpZl48gnP4fv7NdsSD9JOn0VWq045Zcbmb91HMhwY

Jwt What problems does

not safe

The so-called "unsafe" means that Jwt's Payload is plaintext (Base64Url encoding), so it cannot store sensitive data.

However, we can encrypt the generated token again, which will be relatively safer. However, in any case, it is better to save data on the server.

length is too long

Through the previous example, you also see that although we only store a small amount of necessary information in the token, the generated token string is still very long. Every time a user sends a request, he will carry this token. To a certain extent, the overhead is relatively large, but we can generally ignore this performance overhead.

stateless & one-time

jwt The biggest feature of this is stateless and one-time, which leads to the fact that if we want to modify the content inside, we must reissue a new token. Therefore, two other problems are also drawn:

  • cannot manually expire
    . If we want to invalidate the issued jwt, we cannot manually invalidate it unless it expires.
  • cannot renew
    Suppose we have issued a token with a valid duration of 30 minutes, and the user continues to operate within these 30 minutes. When the validity period of the token is reached, we hope to be able to extend the validity period of the token instead of letting the user log in again. Obviously, to achieve this effect, a new token must be reissued instead of operating on the original token.

Bearer Overview

HTTP provides a standard identity authentication solution: when the identity authentication fails, the server can send challenge (challenge) to the client, and the client provides authentication credentials to answer according to the challenge. The specific workflow of

challenge and response is as follows: When the identity authentication fails, the server returns the HTTP status code 01 (Unauthorized, unauthorized) to the client, and adds information on how to provide authentication credentials in the WWW-Authenticate header, which contains at least one challenge method. Then, according to the challenge, the client adds Authorization in the request header, and its value is the credential for identity authentication.

In the HTTP standard authentication scheme, the ones you may be more familiar with are Basic and Digest. Basic encodes the user name and password as the authentication credential, while Digest has upgraded security based on Basic, making the user password more secure. The cookie authentication introduced above is Form authentication and does not belong to the HTTP standard authentication scheme.

. The Bearer mentioned today is also one of the HTTP protocol standard authentication solutions. For details, please refer to: RFC 6570

plaintextml2

 +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Authorization Grant --| Authorization | | Client | | Server| ||-(D)----- access token -------| | || +---------------+ || || +---------------+ ||--(E)----- Access Token ------|Resource | || | Server| ||-(F)--- Protected Resource ---| | +--------+ +---------------+ Abstract Protocol The credentials in Flow

Bearer authentication are called Bearer Token, or access token, the standard request format is (added to the HTTP request header):

pgsql

Authorization: Bearer [Access Token]

In addition, if you are interested in Basic and Digest, we recommend reading the following articles:

  • HTTP authentication basic certification--Basic (I)
  • HTT Basic authentication of P authentication - Basic (II) Abstract authentication of
  • HTTP authentication - Digest authentication (I) Abstract authentication of
  • HTTP authentication - Digest authentication (II)

Identity authentication (Authentication)

I will not elaborate on the identity authentication middleware mentioned in the previous article, let's go directly to JwtBearer.

First, install the following three packages through Nuget:

mathematicah

Install-Package IdentityModelInstall-Package System.IdentityModel.Tokens.JwtInstall-Package Microsoftml2.AspNetCore.Authentication.JwtBearer

Next, add the JwtBearer authentication scheme through the AddJwtBearer extension method:

csharpp

public class Startup{public voidConfigureServices(IServiceCollection services){services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options ={// The scheme is configured in detail here });}}

is similar to CookieAuthenticationDefaults. JwtBearer also provides JwtBearerDefaults, but it is relatively simple, there is only one AuthenticationScheme:

csharp

public static class JwtBearerDefaults{public const string AuthenticationScheme = "Bearer";}

Similarly, we can use options to configure Jwt's verification parameters, verification processor, event callback, etc. Its type is JwtBearerOptions, inherited from AuthenticationSchemeOptions. The following will provide a detailed explanation of some common parameters (this article only introduces the simplest jwt issuance and verification, and does not involve authentication authorization Certification Center ).

Before starting, customize an option class JwtOptions and configure common parameters:

csharp

public class JwtOptions{public const string Name = "Jwt";public readonly static Encoding DefaultEncoding = Encoding.UTF8;public readonly static double DefaultExpiresMinutes = 30d;public string Audience { get; set; }public string Issuer { get; set; }public double ExpiresMinutes { get; set; } = DefaultExpiresMinutes;public Encoding Encoding { get; set; } = DefaultEncoding;public string SymmetricSecurityKey { get; set; }public SymmetricSecurityKey SymmetricSecurityKey = new(Encoding.GetBytes(SymmetricSecurityKeyString));}

Now, we don’t need to pay attention to the specific values ​​of each parameter, just look at the scheme configuration below:

csharp

public class Startup{public void ConfigureServices(IServiceCollection services){services.ConfigureJwtOptions(Configuration.GetSection(JwtOptions.Name));var jwtOptions = Configuration.GetSection(JwtOptions.Name).GetJwtOptions();services.AddSingleton(sp = new SigningCredentials(jwtOptions.SymmetricSecurityKey, SecurityAlgorithms.HmacSha256Signature));services.AddScopedAppJwtBearerEvents();services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options ={options.TokenValidationParameters = new TokenValidationParameters{ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256, SecurityAlgorithms.RsaSha256 },ValidTypes = new[] { JwtConstants.HeaderType },ValidIssuer = jwtOptions.Issuer,ValidateIssuer = true,ValidAudience = jwtOptions.Audience,ValidateAudience = true,IssuerSigningKey = jwtOptions.SymmetricSecurityKey,ValidateIssuerSigningKey = true,ValidateLifetime = true,RequireSignedTokens = true,RequireExpirationTime = true,NameClaimType = JwtClaimTypes.Name,RoleClaimType = JwtClaimTypes.Role,ClockSkew = TimeSpan.Zero,};options.SaveToken = true;options.SecurityTokenValidators.Clear();options.SecurityTokenValidators.Add(new JwtSecurityTokenHandler());options.EventsType = typeof(AppJwtBearerEvents);});}}

where, TokenValidationParameters is a parameter configuration related to token verification. It is required to use it when performing token verification. See the detailed description below:

  • TokenValidationParameters.ValidAlgorithms: A valid signature algorithm list, that is, alg for verification of the header part of Jwt. The default is null, that is, all algorithms are OK.
  • TokenValidationParameters.ValidTypes: A valid token type list, that is, typ for verifying the header part of Jwt. The default is null, even if there is an algorithm.
  • TokenValidationParameters.ValidIssuer: The valid issuer, that is, iss that verifies the Payload part of Jwt. The default is null.
  • TokenValidationParameters.ValidIssuers: A valid issuer list, multiple issuers can be specified.
  • TokenValidationParameters.ValidateIssuer: Whether to verify the issuer. The default is true. Note that if TokenValidationParameters.IssuerValidator is set, then the parameter will be executed regardless of its value.
  • TokenValidationParameters.ValidAudience: A valid audience, i.e. aud that verifies the Payload part of Jwt. The default is null.
  • TokenValidationParameters.ValidAudiences: Valid audience list, multiple audiences can be specified.
  • TokenValidationParameters.ValidateAudience: Whether to verify the audience. The default is true. Note that if TokenValidationParameters.AudienceValidator is set, then the parameter will be executed regardless of its value.
  • TokenValidationParameters.IssuerSigningKey: The key used to verify Jwt signature. For symmetric encryption, both sign-up and sign-up verification are used; for asymmetric encryption, sign-ups are used to add private key , and then sign-ups are used to verify the sign-up using public key.
  • TokenValidationParameters.ValidateIssuerSigningKey: Whether to use the verification key to verify the signature. The default is false. Note that if TokenValidationParameters.IssuerSigningKeyValidator is set, then the parameter will be executed regardless of its value.
  • TokenValidationParameters.ValidateLifetime: Whether to verify whether token is within the valid period, that is, verify the nbf and exp of the Payload part of Jwt.
  • TokenValidationParameters.RequireSignedTokens: Whether to require the token must be signed. The default is true, that is, the token must be signed to be valid.
  • TokenValidationParameters.RequireExpirationTime: Whether tokens must include expiration time. The default is true, that is, the Payload part of Jwt must contain exp and have a valid value.
  • TokenValidationParameters.NameClaimType: Set HttpContext.User.Identity.NameClaimType to facilitate HttpContext.User.Identity.Name to get the correct value
  • TokenValidationParameters.RoleClaimType: Set HttpContext.User.Identity.RoleClaimType to facilitate HttpContext.User.Identity.IsInRole(xxx) Get the correct value
  • TokenValidationParameters.ClockSkew: Set the clock drift, and a certain time error can be allowed when verifying the validity period of the token (such as the time has just reached the exp in the token, but the token is allowed to be still valid within the next 5 minutes). The default is 300s, which is 5 minutes. In this example, the issuance and verification of jwt are both the same server, so there is no need to set the clock drift here.
  • SaveToken: When the token verification is passed, whether to save it to Microsoft.AspNetCore.Authentication.AuthenticationProperties, the default true. This operation occurs after executing JwtBearerEvents.TokenValidated.
  • SecurityTokenValidators: Token validator list, you can specify the processor that verifies the token. By default, it contains 1 JwtSecurityTokenHandler.
  • EventsType: Here I rewritten JwtBearerEvents.

Look at the event callback below:

csharp

public class AppJwtBearerEvents: JwtBearerEvents{public override Task MessageReceived(MessageReceivedContext context){// Get Authorizationstring authorization = context.Request.Headers[HeaderNames.Authorization];if (string.IsNullOrEmpty(authorization)){context.NoResult();return Task.CompletedTask;}// Must be for the Bearer authentication scheme if (authorization.StartsWith("Bearer", StringComparison.OrdinalIgnoreCase)){// Assign tokencontext.Token = authorization["Bearer ".Length..].Trim();}if (string.IsNullOrEmpty(context.Token)){context.NoResult();return Task.CompletedTask;}return Task.CompletedTask;}public override Task TokenValidated(TokenValidatedContext context){return Task.CompletedTask;}public override Task AuthenticationFailed(AuthenticationFailedContext context){Console.WriteLine($"Exception: {context.Exception}");return Task.CompletedTask;}public override Task Challenge(JwtBearerChallengeContext context){Console.WriteLine($"Authenticate Failure: {context.AuthenticateFailure}");Console.WriteLine($"Error: {context.Error}");Console.WriteLine($"Error Description: {context.ErrorDescription}");Console.WriteLine($"Error Uri: {context.ErrorUri}");return Task.CompletedTask;}public override Task Forbidden(ForbiddenContext context){return Task.CompletedTask;}}
  • MessageReceived: Callback when receiving the request, note that the token has not been obtained yet. We can customize the way to get the token in this method, and then assign the obtained token to context.Token (not including Scheme). As long as the token we get is neither Null nor Empty, then the subsequent verification will use the token
  • TokenValidated: the callback after the token verification is passed.
  • AuthenticationFailed: Due to an exception being thrown during the authentication process, the callback after the identity authentication fails.
  • Challenge: Callback during challenge.
  • Forbidden: Callback when 403 (Forbidden, prohibited) appears.

where, in MessageReceived, the logic of obtaining the token by default is simulated.

user login and logout

user login

Now, we will implement the user login function, and when the login is successful, issue a token to the client.

csharp

[Route("api/[controller]")][ApiController]public class AccountController: ControllerBase{private readonly JwtBearerOptions _jwtBearerOptions;private readonly JwtOptions _jwtOptions;private readonly SigningCredentials _signingCredentials;public AccountController(IOptionsSnapshotJwtBearerOptions jwtBearerOptions,IOptionsSnapshotJwtOptions jwtOptions,SigningCredentials signingCredentials){_jwtBearerOptions = jwtBearerOptions.Get(JwtBearerDefaults.AuthenticationScheme);_jwtOptions = jwtOptions.Value;_signingCredentials = signingCredentials;}[AllowAnonymous][HttpPost("login")]public IActionResult Login([FromBody] LoginDto dto){if (dto.UserName != dto.Password){return Unauthorized();}var user = new UserDto(){Id = Guid.NewGuid().ToString("N"),UserName = dto.UserName};var token = CreateJwtToken(user);return Ok(new { token });}[NonAction]private string CreateJwtToken(UserDto user){var tokenDescriptor = new SecurityTokenDescriptor{Subject = new ClaimsIdentity(new ListClaim{new Claim(JwtClaimTypes.Id, user.Id),new Claim(JwtClaimTypes.Name, user.UserName)}),Issuer = _jwtOptions.Issuer,Audience = _jwtOptions.Audience,Expires = DateTime.UtcNow.AddMinutes(_jwtOptions.ExpiresMinutes),SigningCredentials = _signingCredentials};var handler = _jwtBearerOptions.SecurityTokenValidators.OfTypeJwtSecurityTokenHandler().FirstOrDefault()?? new JwtSecurityTokenHandler();var securityToken = handler.CreateJwtSecurityToken(tokenDescriptor);var token = handler.WriteToken(securityToken);return token;}}

Our eyes go directly to the CreateJwtToken method, you can see the familiar Subject, Issuer, Audience, Expires, etc. Among them, Subject can load multiple custom declarations. When generating tokens, all the loaded declarations will be expanded and tiled. Another thing to note is Expires, which must use UTC-based time, and the default validity period is 1 hour.

Next, we generate a token together:

Before you start, if you don’t understand cookie-based identity authentication, it is recommended that you read “Cookie-based Identity Authentication” before reading this article. - DayDayNews

Then we add authorization to WeatherForecastController (the detailed configuration process is omitted), and bring the token to request:

Before you start, if you don’t understand cookie-based identity authentication, it is recommended that you read “Cookie-based Identity Authentication” before reading this article. - DayDayNews

User logout

When using JwtBearer authentication scheme, due to Jwt's "one-time" and "stateless" characteristics, user logout is generally not implemented on the server side, but is implemented through the client, such as the client deletes the token from localstorage (of course, this is just an implementation method of "save the country through curves").

In addition, if you can accept it, the server can add Jwt to the cache blacklist when the user logs out and set the cache expiration time to the expiration time of Jwt.

optimization improvement

instead uses asymmetric encryption for Jwt signature and signature verification

In the previous example, we used the symmetric encryption algorithm HmacSha256 to calculate the signature. Just imagine that multiple business projects in the company will use this token. Therefore, in order for each project to be authenticated, the key needs to be distributed to all projects, which poses a greater risk. Therefore, using asymmetric encryption to calculate signatures is a more reasonable choice: we use the private key to sign, and then we only need to expose the public key for signature verification to verify that the token is valid (not tampered with). Next, we will use RsaSha256 as an example to improve our program.

First, we become Rsa's key pair, refer to the following example code (can be found in the source code AccountController):

csharp

public void GenerateRsaKeyParies(IWebHostEnvironment env){RSAParameters privateKey, publicKey;// = 2048 Otherwise, the length is too short and unsafe using (var rsa = new RSACryptoServiceProvider(2048)){try{privateKey = rsa.ExportParameters(true);publicKey = rsa.ExportParameters(false);} finally{rsa.PersistKeyInCsp = false;}}var dir = Path.Combine(env.ContentRootPath, "Rsa");if (!Directory.Exists(dir)){Directory.CreateDirectory(dir);}System.IO.File.WriteAllText(Path.Combine(dir, "key.private.json"), JsonConvert.SerializeObject(privateKey));System.IO.File.WriteAllText(Path.Combine(dir, "key.public.json"), JsonConvert.SerializeObject(publicKey));}

specific details, then we will improve our JwtOptions:

csharp

public class JwtOptions{public const string Name = "Jwt";public readonly static double DefaultExpiresMinutes = 30d;public string Audience { get; set; }public string Issuer { get; set; }public double ExpiresMinutes { get; set; } = DefaultExpiresMinutes;}

Since the private key and public key of the RSA signature algorithm are stored in another file, and generally this will not be changed easily, they will not be added to the options.

Next, modify our signature algorithm and signature verification algorithm:

csharp

public class Startup{public Startup(IConfiguration configuration, IWebHostEnvironment env){Configuration = configuration;Env = env;}public IConfiguration Configuration { get; }public IWebHostEnvironment Env { get; set; }public void ConfigureServices(IServiceCollection services){services.ConfigureJwtOptions(Configuration.GetSection(JwtOptions.Name));var jwtOptions = Configuration.GetSection(JwtOptions.Name).GetJwtOptions();var rsaSecurityPrivateKeyString = File.ReadAllText(Path.Combine(Env.ContentRootPath, "Rsa", "key.private.json"));var rsaSecurityPublicKeyString = File.ReadAllText(Path.Combine(Env.ContentRootPath, "Rsa", "key.public.json"));RsaSecurityKey rsaSecurityPrivateKey = new(JsonConvert.DeserializeObjectRSAParameters(rsaSecurityPrivateKeyString));RsaSecurityKey rsaSecurityPublicKey = new(JsonConvert.DeserializeObjectRSAParameters(rsaSecurityPublicKeyString));// Sign services.AddSingleton(sp = new SigningCredentials(rsaSecurityPrivateKey, SecurityAlgorithms.RsaSha256Signature));services.AddScopedAppJwtBearerEvents();services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options ={options.TokenValidationParameters = new TokenValidationParameters{// ...// Use public key to verify signature IssuerSigningKey = rsaSecurityPublicKey,}}}}

At this point, it's OK, nothing else needs to be changed. The following is a Jwt example issued. The disadvantage is that the signature part will be much longer than the symmetric encryption (after all, it's safe, we can tolerate O(∩_∩)O haha~):

apache

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ijk 4NTUxMDE3YjBjYTRjOTU5NzNmMTM3Mjk2MWZlZWM2IiwibmFtZSI6InN0cmluZyIsIm5iZiI6MTY0MzIwOTIwNiwiZXhwIjoxNjQzMjA5ODA2LCJpYXQiOjE2NDMyMDkyMDYsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCJ9.GU CYTBytxv5yqGQFB6B6rlARF3F37CJh27e-qBCKApJShSr8vq-RkPu_o0dtCONKx0y1mb2Aq5hddFQYRFaMICQMeUeCJfaVoi96chsvwahnvx1_Snz4vvaiHSmTGCXm-WAkMJdpFny0zsicegLOrJJyHFecHGENGfWee28xYSi9R70bFJjVLxR965UJzOisi5pIXjemdlipaRhdITAWz-B4iK H_2-sv6j_drkJv2CNsEjOdHxHITN6oVUpP3i4i4PmXhRM7x4O0lKeKGQE9ezZIBtXa16nUCJo0VWDD2QAwWr1akzu99wtOSoJf2MoRETwK7vOOKIbTrNQOQ1WYUQ

Encrypt jwt

We know that the Header and Payload in Jwt are both plaintext, especially in Payload, we must not place sensitive information.If you think Jwt is inappropriate in plain text, you can choose to add a layer of encryption to it, which is another implementation of Jwt standard Jwe.

The following is part of the code implementation:

csharp

private string CreateJwtToken(UserDto user){var tokenDescriptor = new SecurityTokenDescriptor{// ...EncryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Total Bytes Length At Least 256!")), JwtConstants.DirectKeyUseAlg, SecurityAlgorithms.Aes128CbcHmacSha256)};// ...}public class Startup{public void ConfigureServices(IServiceCollection services){// ...services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options ={options.TokenValidationParameters = new TokenValidationParameters{// ...// If set ValidAlgorithms, add Aes128CbcHmacSha256ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256, SecurityAlgorithms.RsaSha256, SecurityAlgorithms.Aes128CbcHmacSha256 },// token decryption key TokenDecryptionKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Total Bytes Length At Least 256!"))}}}}}

is a Jwe example:

plaintextml2

eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU 2IiwidHlwIjoiSldUIn0..KsIPh-Wx8TOpgNBZ5xINSA.zgqErSkpnTaWJ1TsPoIKrgpP_2uR-Orjbn54Wo4FeGmIPczk2X8N8qx4zWe9CGztrFLxeoWvYLlfRwclfglmKE9372de lByVwK_C-u7cFN2TaZ183JTWYTyJVPANTC1WtuEzSe3NEKjfRoC9QN7SN4z9cJ-CtIPb1t17XB0gG0fc7T9UARZ1eIUIfnCXROAyX96qB6ABJ5Xy8wrrYkA2m5OqqLyAd8FbZfcK_ rii_lbXNZsbcfgNPBQGEO6lOdBg4I3nQv9A6cqGj9qTnsIH89Dx7mBnkx0W7C9UHtZQsNTG71VSzG8g_KVifC-oO62wrOYeh48y5l4czeIWlAl4GCZpnUQmq4Y_2cw2brgG4WV7FR YPch4RMeTB6y9qrm6Rj8TvZbf_hZ51yvDYvPPVUjMiM1xo5_KLXVZa3w5aEGB4jGynVXwuGDV8XwS8sTjEkziFfA85TWPq_N-ENm4R9K_HUzwfgpGYzM-Nrf54GV8BXpnpapTc-jW ij3MOpsjeyzqXdG5t-JB9_Xt7-BadjMakiU1WihihiigiYMGQBmkG30r8e6bGcoL58Ytb6PQZ3NfHGCakV5LRGWFOjRUSP7X_xC0xWhrH2R6LhD1QESoE8GsTU-YS9JUREECcD2b9gX x0JxYp2mGdCkKRspajhEj4b04PV-hpr0bNSf59GkSMu_KhHuF5AcWfLSqwzACMvsvW6QvIQTzm6gXy8Ui2N80JCGkp_LzW23RFwCPSlQQ7c7S3A-Ltd_AaDQJ9C5B-To_PHESy9bU KhU-MV2tbfSST-vBeJkSn4kz4feEWcG59A.KULA_w3_XEIIKhAHKuFpsw

Its header is:

json

{"alg": "dir","enc": "A128CBC-HS256","typ": "JWT"}

Enhanced Jwt authentication scheme with the help of the server

Although stateless Jwt is very convenient and fast to use, the applicable scenarios are very limited. In order to achieve more functions, it is necessary to use the server side, which will cause the statelessness of Jwt to be destroyed.

Before entering this topic, please confirm that the usage of Jwt mentioned above has fully met your requirements. If so, then congratulations, Jwt is definitely the most suitable solution. If not, and you think you need a server, then you should consider whether you really need a server. This will make the authentication behavior tend to cookie + session, which greatly increases the complexity of the authentication scheme.

Jwt silently refresh to achieve automatic renewal of

Imagine the following scenario: After logging in, the user gets a token with a validity period of 30 minutes, and then when filling out a form, it took 40 minutes. After clicking to submit, the system asked him to log in again and fill in the form again. Do you guess he will be very happy? Therefore, just like when we used cookies to authenticate identity, in Jwt-based authentication schemes, we also need a mechanism similar to sliding expiration to achieve automatic lease renewal.

So how should we design this automatic renewal plan? You may think of the following scheme:

  • Solution 1: Every time a request passed the authentication, Jwt will be reissued to reset the expiration time. Although this solution can solve the problem, it is too violent and has serious performance problems.
  • Solution 2: Jwt will be reissued only when jwt is about to expire. At first glance, this solution looks feasible, but in fact, whether Jwt can be refreshed depends entirely on luck.Assuming a Jwt with a validity period of 30 minutes is issued, we intend to reissue it when it has only 5 minutes left. If the user requests within the last 5 minutes, the Jwt will be refreshed, but if there is no request, the user needs to log in again, which will greatly reduce the experience.
  • Solution 3: ignore the expiration time in the issued Jwt, record the Jwt (or JwtId) in the distributed cache on the server, and set the expiration time. Then, when performing Jwt checking for the first time, the default verification device is not used to check the expiration time. After the verification is passed, it is compared with the expiration time in the cache. If it is valid, the expiration time will be reset. This solution is indeed feasible, but this requires that Jwt be refreshed within the validity period.

The most widely used way at present is to introduce a parameter called refresh token. The rough process is to generate an access token at the same time when issuing refresh token, and the validity period of refresh token is much longer than access token. Then, the client saves both tokens. When the client requests the server to use it, if you find that the server returns an error of "access token expired", then add the previously saved refresh token and request the server to refresh the token. The server will issue a new set of access token and refresh token and refresh token to the client.

Among them, in order to ensure the security and effectiveness of refresh token, in addition to sending it to the client, one copy is also needed to store on the server and set the expiration time. This actually undermines Jwt's "stateless" nature to a certain extent (personally considers acceptable).

Before you start, if you don’t understand the identity authentication based on cookie, it is recommended that you read "Cookie-based Identity Authentication" before reading this article.

Jwt Overview What is

Jwt

Jwt is an open industry standard (RFC7519), which is json Web Token in English, translated as "Json Network Token", which can pass claims between parties in a compact and URL-safe way.

In Jwt, the declaration will be encoded as a Json object, used as a payload for the Jws(Json Web Signature) structure, or as a plaintext for the Jwe(Json Web Encryption) structure, which allows the declaration to be digitally signed or integrity protection and encryption using MAC (Message Authentication Code).

For more information, please visit https://jwt.io/
If you have doubts about jwt, jws, and jwe, please refer to "A post to tell you what JWT, JWS and JWE"

Jwt solves what problems

Cross-site

Traditional cookies can only implement cross-domain, but not cross-site (such as my.abc.com and you.xyz.com). Jwt natively supports cross-domain and cross-site, because it requires that each request, it must carry tokens in the request header.

cross-server

When the current application is basically clustered, if the traditional cookie + session authentication method is used, in order to realize session cross-server sharing, it is also necessary to introduce distributed cache middleware. Jwt does not require distributed cache middleware because it can not be stored on the server side.

Native App-friendly

For native platforms (such as iOS, Android, WP), cookies lose their advantages, and using Jwt is very simple.

Jwt structure

First look at a Jwt example:

plaintextml2

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwO i8vbG9jYWxob3N0OjUwMDAiLCJpYXQiOjE2NDI3NDg5OTIsIm5iZiI6MTY0Mjc0ODk5MiwiZXhwIjoxNjQyNzQ4OTkyLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJuYW1lIjoieGlhb3h pYW90YW5rIn0.nqJpZl48gnP4fv7NdsSD9JOn0VWq045Zcbmb91HMhwY

seems like a very long piece of meaningless garbled code, but be careful, you will find that it is marked (.) Separated into 3 parts, it looks like this:

xxxxx.yyyyy.zzzz

From left to right, these three parts are called: header (Header), payload (Payload) and signature (Signature).

header (Header)

Header is mainly used to illustrate the token type and signature algorithm.

json

{ "alg": "HS256","typ": "JWT",}
  • alg: Signature algorithm, here is HMAC SHA256
  • typ: token type, here is JWTh

After removing all newlines and spaces to the header, you get: {"alg":"HS256","typ":"JWT"}, and then Base64Url encoding is performed. You can get the part 1 of the token :

plaintext tml2

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

payload (Payload)

Payload is the core and is mainly used to store declaration information, such as token issuer, user ID, user role, etc.

json

{ "iss": "http://localhost:5000", "iat": 1642748992, "nbf": 1642748992, "exp": 1642748992, "aud": "http://localhost:5000", "name": "xiaoxiaotank"}

Among them, the first five are predefined:

  • iss: Issuer, that is, the issuer of the token.
  • iat: Issued At, that is, the issuance time of the token
  • exp: Expiration Time, that is, the expiration time of the token
  • aud: Audience, that is, the audience, refers to which group the token serves (group scope), or which piece of the authorized resource granted by the token (uri of the resource)
  • nbf: Not Before, that is, the token is not available before the specified time point

In fact, the declarations in Jwt can be divided into the following three types:

  • Registered Claim: Predefined declarations, although not mandatory, they are recommended, including iss(Issuer), sub(Subject), aud(Audience), exp(Expiration Time), nbf(Not Before), iat(Issued At) and jti(JWT ID). As you can see, these declaration names are very short, because Jwt's core goal is to make the representation compact.
  • Public Claim: Public declaration, users of Jwt can define it at will, but they should avoid conflicts with predefined declarations.
  • Private Claim: Private statement, unlike public statements, private statement names may conflict and should be used with caution.

Base64Url encoding for Payload (remember to remove all newlines and spaces) can be obtained token's part 2 :

plaintextml2

eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJpYXQiOjE2NDI3NDg 5OTIsIm5iZiI6MTY0Mjc0ODk5MiwiZXhwIjoxNjQyNzQ4OTkyLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJuYW1lIjoieGlhb3hpYW90YW5rIn0

Do not store any sensitive information in the Payload, because Base64Url is not encryption, it is just encoding, so this part is plaintext for the client.

signature (Signature)

Signature is mainly used to prevent tokens from being tampered with. When the server obtains the token, the signature will be calculated according to the following algorithm. If the calculated signature is consistent with the signature in the token, it is believed that the token has not been tampered with.

Signature algorithm:

  • First connect the header and Payload through dots (.), that is, the header encoded by Base64Url. Payload is denoted as texttml17
  • and then encrypt the text using the signature algorithm specified in the header to obtain a binary array, denoted as signBytes
  • and finally signBytes Perform Base64Url encoding and obtain signature, that is, the third part of token

plaintextml2

nqJpZl48gnP4fv7NdsSD9JOn0VWq045Zcbmb91HMhwY

Jwt What problems does

not safe

The so-called "unsafe" means that Jwt's Payload is plaintext (Base64Url encoding), so it cannot store sensitive data.

However, we can encrypt the generated token again, which will be relatively safer. However, in any case, it is better to save data on the server.

length is too long

Through the previous example, you also see that although we only store a small amount of necessary information in the token, the generated token string is still very long. Every time a user sends a request, he will carry this token. To a certain extent, the overhead is relatively large, but we can generally ignore this performance overhead.

stateless & one-time

jwt The biggest feature of this is stateless and one-time, which leads to the fact that if we want to modify the content inside, we must reissue a new token. Therefore, two other problems are also drawn:

  • cannot manually expire
    . If we want to invalidate the issued jwt, we cannot manually invalidate it unless it expires.
  • cannot renew
    Suppose we have issued a token with a valid duration of 30 minutes, and the user continues to operate within these 30 minutes. When the validity period of the token is reached, we hope to be able to extend the validity period of the token instead of letting the user log in again. Obviously, to achieve this effect, a new token must be reissued instead of operating on the original token.

Bearer Overview

HTTP provides a standard identity authentication solution: when the identity authentication fails, the server can send challenge (challenge) to the client, and the client provides authentication credentials to answer according to the challenge. The specific workflow of

challenge and response is as follows: When the identity authentication fails, the server returns the HTTP status code 01 (Unauthorized, unauthorized) to the client, and adds information on how to provide authentication credentials in the WWW-Authenticate header, which contains at least one challenge method. Then, according to the challenge, the client adds Authorization in the request header, and its value is the credential for identity authentication.

In the HTTP standard authentication scheme, the ones you may be more familiar with are Basic and Digest. Basic encodes the user name and password as the authentication credential, while Digest has upgraded security based on Basic, making the user password more secure. The cookie authentication introduced above is Form authentication and does not belong to the HTTP standard authentication scheme.

. The Bearer mentioned today is also one of the HTTP protocol standard authentication solutions. For details, please refer to: RFC 6570

plaintextml2

 +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Authorization Grant --| Authorization | | Client | | Server| ||-(D)----- access token -------| | || +---------------+ || || +---------------+ ||--(E)----- Access Token ------|Resource | || | Server| ||-(F)--- Protected Resource ---| | +--------+ +---------------+ Abstract Protocol The credentials in Flow

Bearer authentication are called Bearer Token, or access token, the standard request format is (added to the HTTP request header):

pgsql

Authorization: Bearer [Access Token]

In addition, if you are interested in Basic and Digest, we recommend reading the following articles:

  • HTTP authentication basic certification--Basic (I)
  • HTT Basic authentication of P authentication - Basic (II) Abstract authentication of
  • HTTP authentication - Digest authentication (I) Abstract authentication of
  • HTTP authentication - Digest authentication (II)

Identity authentication (Authentication)

I will not elaborate on the identity authentication middleware mentioned in the previous article, let's go directly to JwtBearer.

First, install the following three packages through Nuget:

mathematicah

Install-Package IdentityModelInstall-Package System.IdentityModel.Tokens.JwtInstall-Package Microsoftml2.AspNetCore.Authentication.JwtBearer

Next, add the JwtBearer authentication scheme through the AddJwtBearer extension method:

csharpp

public class Startup{public voidConfigureServices(IServiceCollection services){services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options ={// The scheme is configured in detail here });}}

is similar to CookieAuthenticationDefaults. JwtBearer also provides JwtBearerDefaults, but it is relatively simple, there is only one AuthenticationScheme:

csharp

public static class JwtBearerDefaults{public const string AuthenticationScheme = "Bearer";}

Similarly, we can use options to configure Jwt's verification parameters, verification processor, event callback, etc. Its type is JwtBearerOptions, inherited from AuthenticationSchemeOptions. The following will provide a detailed explanation of some common parameters (this article only introduces the simplest jwt issuance and verification, and does not involve authentication authorization Certification Center ).

Before starting, customize an option class JwtOptions and configure common parameters:

csharp

public class JwtOptions{public const string Name = "Jwt";public readonly static Encoding DefaultEncoding = Encoding.UTF8;public readonly static double DefaultExpiresMinutes = 30d;public string Audience { get; set; }public string Issuer { get; set; }public double ExpiresMinutes { get; set; } = DefaultExpiresMinutes;public Encoding Encoding { get; set; } = DefaultEncoding;public string SymmetricSecurityKey { get; set; }public SymmetricSecurityKey SymmetricSecurityKey = new(Encoding.GetBytes(SymmetricSecurityKeyString));}

Now, we don’t need to pay attention to the specific values ​​of each parameter, just look at the scheme configuration below:

csharp

public class Startup{public void ConfigureServices(IServiceCollection services){services.ConfigureJwtOptions(Configuration.GetSection(JwtOptions.Name));var jwtOptions = Configuration.GetSection(JwtOptions.Name).GetJwtOptions();services.AddSingleton(sp = new SigningCredentials(jwtOptions.SymmetricSecurityKey, SecurityAlgorithms.HmacSha256Signature));services.AddScopedAppJwtBearerEvents();services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options ={options.TokenValidationParameters = new TokenValidationParameters{ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256, SecurityAlgorithms.RsaSha256 },ValidTypes = new[] { JwtConstants.HeaderType },ValidIssuer = jwtOptions.Issuer,ValidateIssuer = true,ValidAudience = jwtOptions.Audience,ValidateAudience = true,IssuerSigningKey = jwtOptions.SymmetricSecurityKey,ValidateIssuerSigningKey = true,ValidateLifetime = true,RequireSignedTokens = true,RequireExpirationTime = true,NameClaimType = JwtClaimTypes.Name,RoleClaimType = JwtClaimTypes.Role,ClockSkew = TimeSpan.Zero,};options.SaveToken = true;options.SecurityTokenValidators.Clear();options.SecurityTokenValidators.Add(new JwtSecurityTokenHandler());options.EventsType = typeof(AppJwtBearerEvents);});}}

where, TokenValidationParameters is a parameter configuration related to token verification. It is required to use it when performing token verification. See the detailed description below:

  • TokenValidationParameters.ValidAlgorithms: A valid signature algorithm list, that is, alg for verification of the header part of Jwt. The default is null, that is, all algorithms are OK.
  • TokenValidationParameters.ValidTypes: A valid token type list, that is, typ for verifying the header part of Jwt. The default is null, even if there is an algorithm.
  • TokenValidationParameters.ValidIssuer: The valid issuer, that is, iss that verifies the Payload part of Jwt. The default is null.
  • TokenValidationParameters.ValidIssuers: A valid issuer list, multiple issuers can be specified.
  • TokenValidationParameters.ValidateIssuer: Whether to verify the issuer. The default is true. Note that if TokenValidationParameters.IssuerValidator is set, then the parameter will be executed regardless of its value.
  • TokenValidationParameters.ValidAudience: A valid audience, i.e. aud that verifies the Payload part of Jwt. The default is null.
  • TokenValidationParameters.ValidAudiences: Valid audience list, multiple audiences can be specified.
  • TokenValidationParameters.ValidateAudience: Whether to verify the audience. The default is true. Note that if TokenValidationParameters.AudienceValidator is set, then the parameter will be executed regardless of its value.
  • TokenValidationParameters.IssuerSigningKey: The key used to verify Jwt signature. For symmetric encryption, both sign-up and sign-up verification are used; for asymmetric encryption, sign-ups are used to add private key , and then sign-ups are used to verify the sign-up using public key.
  • TokenValidationParameters.ValidateIssuerSigningKey: Whether to use the verification key to verify the signature. The default is false. Note that if TokenValidationParameters.IssuerSigningKeyValidator is set, then the parameter will be executed regardless of its value.
  • TokenValidationParameters.ValidateLifetime: Whether to verify whether token is within the valid period, that is, verify the nbf and exp of the Payload part of Jwt.
  • TokenValidationParameters.RequireSignedTokens: Whether to require the token must be signed. The default is true, that is, the token must be signed to be valid.
  • TokenValidationParameters.RequireExpirationTime: Whether tokens must include expiration time. The default is true, that is, the Payload part of Jwt must contain exp and have a valid value.
  • TokenValidationParameters.NameClaimType: Set HttpContext.User.Identity.NameClaimType to facilitate HttpContext.User.Identity.Name to get the correct value
  • TokenValidationParameters.RoleClaimType: Set HttpContext.User.Identity.RoleClaimType to facilitate HttpContext.User.Identity.IsInRole(xxx) Get the correct value
  • TokenValidationParameters.ClockSkew: Set the clock drift, and a certain time error can be allowed when verifying the validity period of the token (such as the time has just reached the exp in the token, but the token is allowed to be still valid within the next 5 minutes). The default is 300s, which is 5 minutes. In this example, the issuance and verification of jwt are both the same server, so there is no need to set the clock drift here.
  • SaveToken: When the token verification is passed, whether to save it to Microsoft.AspNetCore.Authentication.AuthenticationProperties, the default true. This operation occurs after executing JwtBearerEvents.TokenValidated.
  • SecurityTokenValidators: Token validator list, you can specify the processor that verifies the token. By default, it contains 1 JwtSecurityTokenHandler.
  • EventsType: Here I rewritten JwtBearerEvents.

Look at the event callback below:

csharp

public class AppJwtBearerEvents: JwtBearerEvents{public override Task MessageReceived(MessageReceivedContext context){// Get Authorizationstring authorization = context.Request.Headers[HeaderNames.Authorization];if (string.IsNullOrEmpty(authorization)){context.NoResult();return Task.CompletedTask;}// Must be for the Bearer authentication scheme if (authorization.StartsWith("Bearer", StringComparison.OrdinalIgnoreCase)){// Assign tokencontext.Token = authorization["Bearer ".Length..].Trim();}if (string.IsNullOrEmpty(context.Token)){context.NoResult();return Task.CompletedTask;}return Task.CompletedTask;}public override Task TokenValidated(TokenValidatedContext context){return Task.CompletedTask;}public override Task AuthenticationFailed(AuthenticationFailedContext context){Console.WriteLine($"Exception: {context.Exception}");return Task.CompletedTask;}public override Task Challenge(JwtBearerChallengeContext context){Console.WriteLine($"Authenticate Failure: {context.AuthenticateFailure}");Console.WriteLine($"Error: {context.Error}");Console.WriteLine($"Error Description: {context.ErrorDescription}");Console.WriteLine($"Error Uri: {context.ErrorUri}");return Task.CompletedTask;}public override Task Forbidden(ForbiddenContext context){return Task.CompletedTask;}}
  • MessageReceived: Callback when receiving the request, note that the token has not been obtained yet. We can customize the way to get the token in this method, and then assign the obtained token to context.Token (not including Scheme). As long as the token we get is neither Null nor Empty, then the subsequent verification will use the token
  • TokenValidated: the callback after the token verification is passed.
  • AuthenticationFailed: Due to an exception being thrown during the authentication process, the callback after the identity authentication fails.
  • Challenge: Callback during challenge.
  • Forbidden: Callback when 403 (Forbidden, prohibited) appears.

where, in MessageReceived, the logic of obtaining the token by default is simulated.

user login and logout

user login

Now, we will implement the user login function, and when the login is successful, issue a token to the client.

csharp

[Route("api/[controller]")][ApiController]public class AccountController: ControllerBase{private readonly JwtBearerOptions _jwtBearerOptions;private readonly JwtOptions _jwtOptions;private readonly SigningCredentials _signingCredentials;public AccountController(IOptionsSnapshotJwtBearerOptions jwtBearerOptions,IOptionsSnapshotJwtOptions jwtOptions,SigningCredentials signingCredentials){_jwtBearerOptions = jwtBearerOptions.Get(JwtBearerDefaults.AuthenticationScheme);_jwtOptions = jwtOptions.Value;_signingCredentials = signingCredentials;}[AllowAnonymous][HttpPost("login")]public IActionResult Login([FromBody] LoginDto dto){if (dto.UserName != dto.Password){return Unauthorized();}var user = new UserDto(){Id = Guid.NewGuid().ToString("N"),UserName = dto.UserName};var token = CreateJwtToken(user);return Ok(new { token });}[NonAction]private string CreateJwtToken(UserDto user){var tokenDescriptor = new SecurityTokenDescriptor{Subject = new ClaimsIdentity(new ListClaim{new Claim(JwtClaimTypes.Id, user.Id),new Claim(JwtClaimTypes.Name, user.UserName)}),Issuer = _jwtOptions.Issuer,Audience = _jwtOptions.Audience,Expires = DateTime.UtcNow.AddMinutes(_jwtOptions.ExpiresMinutes),SigningCredentials = _signingCredentials};var handler = _jwtBearerOptions.SecurityTokenValidators.OfTypeJwtSecurityTokenHandler().FirstOrDefault()?? new JwtSecurityTokenHandler();var securityToken = handler.CreateJwtSecurityToken(tokenDescriptor);var token = handler.WriteToken(securityToken);return token;}}

Our eyes go directly to the CreateJwtToken method, you can see the familiar Subject, Issuer, Audience, Expires, etc. Among them, Subject can load multiple custom declarations. When generating tokens, all the loaded declarations will be expanded and tiled. Another thing to note is Expires, which must use UTC-based time, and the default validity period is 1 hour.

Next, we generate a token together:

Before you start, if you don’t understand cookie-based identity authentication, it is recommended that you read “Cookie-based Identity Authentication” before reading this article. - DayDayNews

Then we add authorization to WeatherForecastController (the detailed configuration process is omitted), and bring the token to request:

Before you start, if you don’t understand cookie-based identity authentication, it is recommended that you read “Cookie-based Identity Authentication” before reading this article. - DayDayNews

User logout

When using JwtBearer authentication scheme, due to Jwt's "one-time" and "stateless" characteristics, user logout is generally not implemented on the server side, but is implemented through the client, such as the client deletes the token from localstorage (of course, this is just an implementation method of "save the country through curves").

In addition, if you can accept it, the server can add Jwt to the cache blacklist when the user logs out and set the cache expiration time to the expiration time of Jwt.

optimization improvement

instead uses asymmetric encryption for Jwt signature and signature verification

In the previous example, we used the symmetric encryption algorithm HmacSha256 to calculate the signature. Just imagine that multiple business projects in the company will use this token. Therefore, in order for each project to be authenticated, the key needs to be distributed to all projects, which poses a greater risk. Therefore, using asymmetric encryption to calculate signatures is a more reasonable choice: we use the private key to sign, and then we only need to expose the public key for signature verification to verify that the token is valid (not tampered with). Next, we will use RsaSha256 as an example to improve our program.

First, we become Rsa's key pair, refer to the following example code (can be found in the source code AccountController):

csharp

public void GenerateRsaKeyParies(IWebHostEnvironment env){RSAParameters privateKey, publicKey;// = 2048 Otherwise, the length is too short and unsafe using (var rsa = new RSACryptoServiceProvider(2048)){try{privateKey = rsa.ExportParameters(true);publicKey = rsa.ExportParameters(false);} finally{rsa.PersistKeyInCsp = false;}}var dir = Path.Combine(env.ContentRootPath, "Rsa");if (!Directory.Exists(dir)){Directory.CreateDirectory(dir);}System.IO.File.WriteAllText(Path.Combine(dir, "key.private.json"), JsonConvert.SerializeObject(privateKey));System.IO.File.WriteAllText(Path.Combine(dir, "key.public.json"), JsonConvert.SerializeObject(publicKey));}

specific details, then we will improve our JwtOptions:

csharp

public class JwtOptions{public const string Name = "Jwt";public readonly static double DefaultExpiresMinutes = 30d;public string Audience { get; set; }public string Issuer { get; set; }public double ExpiresMinutes { get; set; } = DefaultExpiresMinutes;}

Since the private key and public key of the RSA signature algorithm are stored in another file, and generally this will not be changed easily, they will not be added to the options.

Next, modify our signature algorithm and signature verification algorithm:

csharp

public class Startup{public Startup(IConfiguration configuration, IWebHostEnvironment env){Configuration = configuration;Env = env;}public IConfiguration Configuration { get; }public IWebHostEnvironment Env { get; set; }public void ConfigureServices(IServiceCollection services){services.ConfigureJwtOptions(Configuration.GetSection(JwtOptions.Name));var jwtOptions = Configuration.GetSection(JwtOptions.Name).GetJwtOptions();var rsaSecurityPrivateKeyString = File.ReadAllText(Path.Combine(Env.ContentRootPath, "Rsa", "key.private.json"));var rsaSecurityPublicKeyString = File.ReadAllText(Path.Combine(Env.ContentRootPath, "Rsa", "key.public.json"));RsaSecurityKey rsaSecurityPrivateKey = new(JsonConvert.DeserializeObjectRSAParameters(rsaSecurityPrivateKeyString));RsaSecurityKey rsaSecurityPublicKey = new(JsonConvert.DeserializeObjectRSAParameters(rsaSecurityPublicKeyString));// Sign services.AddSingleton(sp = new SigningCredentials(rsaSecurityPrivateKey, SecurityAlgorithms.RsaSha256Signature));services.AddScopedAppJwtBearerEvents();services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options ={options.TokenValidationParameters = new TokenValidationParameters{// ...// Use public key to verify signature IssuerSigningKey = rsaSecurityPublicKey,}}}}

At this point, it's OK, nothing else needs to be changed. The following is a Jwt example issued. The disadvantage is that the signature part will be much longer than the symmetric encryption (after all, it's safe, we can tolerate O(∩_∩)O haha~):

apache

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ijk 4NTUxMDE3YjBjYTRjOTU5NzNmMTM3Mjk2MWZlZWM2IiwibmFtZSI6InN0cmluZyIsIm5iZiI6MTY0MzIwOTIwNiwiZXhwIjoxNjQzMjA5ODA2LCJpYXQiOjE2NDMyMDkyMDYsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMCJ9.GU CYTBytxv5yqGQFB6B6rlARF3F37CJh27e-qBCKApJShSr8vq-RkPu_o0dtCONKx0y1mb2Aq5hddFQYRFaMICQMeUeCJfaVoi96chsvwahnvx1_Snz4vvaiHSmTGCXm-WAkMJdpFny0zsicegLOrJJyHFecHGENGfWee28xYSi9R70bFJjVLxR965UJzOisi5pIXjemdlipaRhdITAWz-B4iK H_2-sv6j_drkJv2CNsEjOdHxHITN6oVUpP3i4i4PmXhRM7x4O0lKeKGQE9ezZIBtXa16nUCJo0VWDD2QAwWr1akzu99wtOSoJf2MoRETwK7vOOKIbTrNQOQ1WYUQ

Encrypt jwt

We know that the Header and Payload in Jwt are both plaintext, especially in Payload, we must not place sensitive information.If you think Jwt is inappropriate in plain text, you can choose to add a layer of encryption to it, which is another implementation of Jwt standard Jwe.

The following is part of the code implementation:

csharp

private string CreateJwtToken(UserDto user){var tokenDescriptor = new SecurityTokenDescriptor{// ...EncryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Total Bytes Length At Least 256!")), JwtConstants.DirectKeyUseAlg, SecurityAlgorithms.Aes128CbcHmacSha256)};// ...}public class Startup{public void ConfigureServices(IServiceCollection services){// ...services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options ={options.TokenValidationParameters = new TokenValidationParameters{// ...// If set ValidAlgorithms, add Aes128CbcHmacSha256ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256, SecurityAlgorithms.RsaSha256, SecurityAlgorithms.Aes128CbcHmacSha256 },// token decryption key TokenDecryptionKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Total Bytes Length At Least 256!"))}}}}}

is a Jwe example:

plaintextml2

eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU 2IiwidHlwIjoiSldUIn0..KsIPh-Wx8TOpgNBZ5xINSA.zgqErSkpnTaWJ1TsPoIKrgpP_2uR-Orjbn54Wo4FeGmIPczk2X8N8qx4zWe9CGztrFLxeoWvYLlfRwclfglmKE9372de lByVwK_C-u7cFN2TaZ183JTWYTyJVPANTC1WtuEzSe3NEKjfRoC9QN7SN4z9cJ-CtIPb1t17XB0gG0fc7T9UARZ1eIUIfnCXROAyX96qB6ABJ5Xy8wrrYkA2m5OqqLyAd8FbZfcK_ rii_lbXNZsbcfgNPBQGEO6lOdBg4I3nQv9A6cqGj9qTnsIH89Dx7mBnkx0W7C9UHtZQsNTG71VSzG8g_KVifC-oO62wrOYeh48y5l4czeIWlAl4GCZpnUQmq4Y_2cw2brgG4WV7FR YPch4RMeTB6y9qrm6Rj8TvZbf_hZ51yvDYvPPVUjMiM1xo5_KLXVZa3w5aEGB4jGynVXwuGDV8XwS8sTjEkziFfA85TWPq_N-ENm4R9K_HUzwfgpGYzM-Nrf54GV8BXpnpapTc-jW ij3MOpsjeyzqXdG5t-JB9_Xt7-BadjMakiU1WihihiigiYMGQBmkG30r8e6bGcoL58Ytb6PQZ3NfHGCakV5LRGWFOjRUSP7X_xC0xWhrH2R6LhD1QESoE8GsTU-YS9JUREECcD2b9gX x0JxYp2mGdCkKRspajhEj4b04PV-hpr0bNSf59GkSMu_KhHuF5AcWfLSqwzACMvsvW6QvIQTzm6gXy8Ui2N80JCGkp_LzW23RFwCPSlQQ7c7S3A-Ltd_AaDQJ9C5B-To_PHESy9bU KhU-MV2tbfSST-vBeJkSn4kz4feEWcG59A.KULA_w3_XEIIKhAHKuFpsw

Its header is:

json

{"alg": "dir","enc": "A128CBC-HS256","typ": "JWT"}

Enhanced Jwt authentication scheme with the help of the server

Although stateless Jwt is very convenient and fast to use, the applicable scenarios are very limited. In order to achieve more functions, it is necessary to use the server side, which will cause the statelessness of Jwt to be destroyed.

Before entering this topic, please confirm that the usage of Jwt mentioned above has fully met your requirements. If so, then congratulations, Jwt is definitely the most suitable solution. If not, and you think you need a server, then you should consider whether you really need a server. This will make the authentication behavior tend to cookie + session, which greatly increases the complexity of the authentication scheme.

Jwt silently refresh to achieve automatic renewal of

Imagine the following scenario: After logging in, the user gets a token with a validity period of 30 minutes, and then when filling out a form, it took 40 minutes. After clicking to submit, the system asked him to log in again and fill in the form again. Do you guess he will be very happy? Therefore, just like when we used cookies to authenticate identity, in Jwt-based authentication schemes, we also need a mechanism similar to sliding expiration to achieve automatic lease renewal.

So how should we design this automatic renewal plan? You may think of the following scheme:

  • Solution 1: Every time a request passed the authentication, Jwt will be reissued to reset the expiration time. Although this solution can solve the problem, it is too violent and has serious performance problems.
  • Solution 2: Jwt will be reissued only when jwt is about to expire. At first glance, this solution looks feasible, but in fact, whether Jwt can be refreshed depends entirely on luck.Assuming a Jwt with a validity period of 30 minutes is issued, we intend to reissue it when it has only 5 minutes left. If the user requests within the last 5 minutes, the Jwt will be refreshed, but if there is no request, the user needs to log in again, which will greatly reduce the experience.
  • Solution 3: ignore the expiration time in the issued Jwt, record the Jwt (or JwtId) in the distributed cache on the server, and set the expiration time. Then, when performing Jwt checking for the first time, the default verification device is not used to check the expiration time. After the verification is passed, it is compared with the expiration time in the cache. If it is valid, the expiration time will be reset. This solution is indeed feasible, but this requires that Jwt be refreshed within the validity period.

The most widely used way at present is to introduce a parameter called refresh token. The rough process is to generate an access token at the same time when issuing refresh token, and the validity period of refresh token is much longer than access token. Then, the client saves both tokens. When the client requests the server to use it, if you find that the server returns an error of "access token expired", then add the previously saved refresh token and request the server to refresh the token. The server will issue a new set of access token and refresh token and refresh token to the client.

Among them, in order to ensure the security and effectiveness of refresh token, in addition to sending it to the client, one copy is also needed to store on the server and set the expiration time. This actually undermines Jwt's "stateless" nature to a certain extent (personally considers acceptable).

For specific code, please refer to XXTk.Auth.Samples.JwtBearerWithRefresh.HttpApi

First, define the data type to be returned to the client:

csharp

public class AuthTokenDto{// jwt tokenpublic string AccessToken { get; set; }// Refresh token used to refresh token public string RefreshToken { get; set; }}

Next define the service interface of token IAuthTokenService and service implementation AuthTokenService:

csharp

public interface IAuthTokenService{TaskAuthTokenDto CreateAuthTokenAsync(UserDto user);TaskAuthTokenDto RefreshAuthTokenAsync(AuthTokenDto token);}public class AuthTokenService : IAuthTokenService{private const string RefreshTokenIdClaimType = "refresh_token_id";private readonly JwtBearerOptions _jwtBearerOptions;private readonly JwtOptions _jwtOptions;private readonly SigningCredentials _signingCredentials;private readonly IDistributedCache _distributedCache;private readonly ILoggerAuthTokenService _logger;public AuthTokenService( IOptionsSnapshotJwtBearerOptions jwtBearerOptions, IOptionsSnapshotJwtOptions jwtOptions, SigningCredentials signingCredentials, IDistributedCache distributedCache, ILoggerAuthTokenService logger){_jwtBearerOptions = jwtBearerOptions.Get(JwtBearerDefaults.AuthenticationScheme);_jwtOptions = jwtOptions.Value;_signingCredentials = signingCredentials;_distributedCache = distributedCache;_logger = logger;}}

Next, let's implement CreateAuthTokenAsynctml2 method:

csharp

public async TaskAuthTokenDto CreateAuthTokenAsync(UserDto user){var result = new AuthTokenDto();// Create refresh tokenvar first (refreshTokenId, refreshToken) = await CreateRefreshTokenAsync(user.Id);result.RefreshToken = refreshToken;// Issue Jwtresult.AccessToken = CreateJwtToken(user, refreshTokenId);return result;}private async Task(string refreshTokenId, string refreshToken) CreateRefreshTokenAsync(string userId){// refresh token id as cache Keyvar tokenId = Guid.NewGuid().ToString("N");// Generate refresh tokenvar rnBytes = new byte[32]; using var rng = RandomNumberGenerator.Create();rng.GetBytes(rnBytes);var token = Convert.ToBase64String(rnBytes);// Set the expiration time of the refresh token var options = new DistributedCacheEntryOptions();options.SetAbsoluteExpiration(TimeSpan.FromDays(_jwtOptions.RefreshTokenExpiresDays));// cache refresh tokenawait _distributedCache.SetStringAsync(GetRefreshTokenKey(userId, tokenId), token, options);return (tokenId, token);}private string CreateJwtToken(UserDto user, string refreshTokenId){if (user is null) throw new ArgumentNullException(nameof(user));if (string.IsNullOrEmpty(refreshTokenId)) throw new ArgumentNullException(nameof(refreshTokenId));var tokenDescriptor = new SecurityTokenDescriptor{Subject = new ClaimsIdentity(new ListClaim{new Claim(JwtClaimTypes.Id, user.Id),new Claim(JwtClaimTypes.Name, user.UserName),// Record the refresh token id new Claim(RefreshTokenIdClaimType, refreshTokenId)}),Issuer = _jwtBearerOptions.TokenValidationParameters.ValidIssuer,Audience = _jwtBearerOptions.TokenValidationParameters.ValidAudience,Expires = DateTime.UtcNow.AddMinutes(_jwtOptions.AccessTokenExpiresMinutes),SigningCredentials = _signingCredentials,};var handler = _jwtBearerOptions.SecurityTokenValidators.OfTypeJwtSecurityTokenHandler().FirstOrDefault()?? new JwtSecurityTokenHandler();var securityToken = handler.CreateJwtSecurityToken(tokenDescriptor);var token = handler.WriteToken(securityToken);return token;}private string GetRefreshTokenKey(string userId, string refreshTokenId){if (string.IsNullOrEmpty(userId)) throw new ArgumentNullException(nameof(userId));if (string.IsNullOrEmpty(refreshTokenId)) throw new ArgumentNullException(nameof(refreshTokenId)); return $"{userId}:{refreshTokenId}";}

Let's look at the effect below:

Before you start, if you don’t understand cookie-based identity authentication, it is recommended that you read “Cookie-based Identity Authentication” before reading this article. - DayDayNews

Next, implement RefreshAuthTokenAsynctml2 method:

csharp

public async TaskAuthTokenDto RefreshAuthTokenAsync(AuthTokenDto token){var validationParameters = _jwtBearerOptions.TokenValidationParameters.Clone();// No verification life cycle validationParameters.ValidateLifetime = false;var handler = _jwtBearerOptions.SecurityTokenValidators.OfTypeJwtSecurityTokenHandler().FirstOrDefault()?? new JwtSecurityTokenHandler(); ClaimsPrincipal principal = null;try{// Verify first whether jwt is really valid principal = handler.ValidateToken(token.AccessToken, validationParameters, out _);}catch (Exception ex){_logger.LogWarning(ex.ToString()); throw new BadHttpRequestException("Invalid access token");}var identity = principal.Identities.First();var userId = identity.Claims.FirstOrDefault(c = c.Type == JwtClaimTypes.Id).Value;var refreshTokenId = identity.Claims.FirstOrDefault(c = c.Type == RefreshTokenIdClaimType).Value;var refreshTokenKey = GetRefreshTokenKey(userId, refreshTokenId);var refreshToken = await _distributedCache.GetStringAsync(refreshTokenKey);// Verify whether the refresh token is valid if (refreshToken != token.RefreshToken){throw new BadHttpRequestException("Invalid refresh token");}// refresh If token is used, remember to clear await _distributedCache.RemoveAsync(refreshTokenKey);// Here you should get user information from the database based on userId var user = new UserDto(){Id = userId,UserName = principal.Identity.Name}; return await CreateAuthTokenAsync(user);}

Let’s take a look at the effect below:

Before you start, if you don’t understand cookie-based identity authentication, it is recommended that you read “Cookie-based Identity Authentication” before reading this article. - DayDayNews

Note: After introducing the refresh token, remember to clear the refresh token of the current Jwt after the user logs out, or modify the password to clear the refresh token of the user.

Finally, explain several questions:

  • Why is refresh token id saved in Jwt? Isn't it possible to save refresh tokens directly?
  • save refresh token id to achieve multiple refresh tokens for one user, which is suitable for the same user login on multiple clients.
  • cannot save the refresh token directly. Since Jwt is plain text, this can easily lead to refresh token leakage, which causes others to apply for access token without the user's knowledge.
  • Why should we design to correspond to multiple refresh tokens for one user?
  • This is suitable for the situation where the same user logs in on multiple clients, preventing one of the clients from refreshing the token, causing the other clients to fail to refresh.

handles different system requirements. Different field information is stored in Jwt authentication information.

Assume that the following scenario is: the mall procurement system and the delivery system belong to the same e-commerce platform, and both use the same set of JwtBearer-based authentication scheme. Now, the delivery system needs to add role information and the maximum daily delivery times information to the certification information, for quick access.

solutions may be diverse. For example, when Jwt is issued, the role information and the maximum daily receipts are stored in Jwt. Although this can solve the problem, it will obviously make Jwt store a lot of redundant data, which will seem unacceptable when the system is increasing.

The following is a more reasonable solution I thought of: First of all, role information is more general and will be used by most systems, so it is recommended to add role information to Jwt to store. For the maximum daily receipts, it is more inclined to use the receipt system. Therefore, this information is maintained by the receipt system on the server side, for example, the user ID is used as the Key and recorded in the distributed cache.

Jwt+server vs Cookie + Session

Many people will say that I use Jwt because of its statelessness. Since it also needs to be combined with the server, why don’t I just use Cookie + Session?

Indeed, if your system front end is H5, the clients are browsers, and it is basically impossible to change in the future, then you can shave two big ears and kick it out of the house, because Cookie + Session is definitely your first choice.

However, if your system includes H5, mini programs, Native Apps, etc., since some of the clients do not support cookies, cookies lose their advantages. It seems that there is no big difference between using cookies or Jwt at this time, but Jwt can automatically renew the lease. In fact, the more recommended method I would like to use Jwt + Cookie, that is, to save Jwt in a cookie. In this way, in H5 applications, the authentication information is still used to use the cookie mechanism, while in other clients that do not support cookies, Jwt is directly used (through the Authorization Header), which can ensure the unity of authentication behavior.

prevents Jwt from leaking

At the end of the article, let's take a look at how to prevent Jwt from leaking.

Assuming that Jwt is leaked, others can use your identity to access the server for sensitive operations. However, this is relatively good, because Jwt expires and becomes invalid. However, if the refresh token is also leaked, it will have more serious consequences, and others can obtain the latest token without restrictions through the refresh token.

After reading the above paragraph, do you dare not use Jwt? Don't be afraid, any authentication scheme will have the possibility of this situation. For example, when logging in with a username and password, it is not the possibility of the username and password being stolen during the request process.

Since there is no absolute security protection measure, we can only try to make it safe as much as possible. The following are two suggestions:

  • Use the Https protocol
  • Set a shorter Jwt validity period

Finally, explain several questions:

  • Why is refresh token id saved in Jwt? Isn't it possible to save refresh tokens directly?
  • save refresh token id to achieve multiple refresh tokens for one user, which is suitable for the same user login on multiple clients.
  • cannot save the refresh token directly. Since Jwt is plain text, this can easily lead to refresh token leakage, which causes others to apply for access token without the user's knowledge.
  • Why should we design to correspond to multiple refresh tokens for one user?
  • This is suitable for the situation where the same user logs in on multiple clients, preventing one of the clients from refreshing the token, causing the other clients to fail to refresh.

handles different system requirements. Different field information is stored in Jwt authentication information.

Assume that the following scenario is: the mall procurement system and the delivery system belong to the same e-commerce platform, and both use the same set of JwtBearer-based authentication scheme. Now, the delivery system needs to add role information and the maximum daily delivery times information to the certification information, for quick access.

solutions may be diverse. For example, when Jwt is issued, the role information and the maximum daily receipts are stored in Jwt. Although this can solve the problem, it will obviously make Jwt store a lot of redundant data, which will seem unacceptable when the system is increasing.

The following is a more reasonable solution I thought of: First of all, role information is more general and will be used by most systems, so it is recommended to add role information to Jwt to store. For the maximum daily receipts, it is more inclined to use the receipt system. Therefore, this information is maintained by the receipt system on the server side, for example, the user ID is used as the Key and recorded in the distributed cache.

Jwt+server vs Cookie + Session

Many people will say that I use Jwt because of its statelessness. Since it also needs to be combined with the server, why don’t I just use Cookie + Session?

Indeed, if your system front end is H5, the clients are browsers, and it is basically impossible to change in the future, then you can shave two big ears and kick it out of the house, because Cookie + Session is definitely your first choice.

However, if your system includes H5, mini programs, Native Apps, etc., since some of the clients do not support cookies, cookies lose their advantages. It seems that there is no big difference between using cookies or Jwt at this time, but Jwt can automatically renew the lease. In fact, the more recommended method I would like to use Jwt + Cookie, that is, to save Jwt in a cookie. In this way, in H5 applications, the authentication information is still used to use the cookie mechanism, while in other clients that do not support cookies, Jwt is directly used (through the Authorization Header), which can ensure the unity of authentication behavior.

prevents Jwt from leaking

At the end of the article, let's take a look at how to prevent Jwt from leaking.

Assuming that Jwt is leaked, others can use your identity to access the server for sensitive operations. However, this is relatively good, because Jwt expires and becomes invalid. However, if the refresh token is also leaked, it will have more serious consequences, and others can obtain the latest token without restrictions through the refresh token.

After reading the above paragraph, do you dare not use Jwt? Don't be afraid, any authentication scheme will have the possibility of this situation. For example, when logging in with a username and password, it is not the possibility of the username and password being stolen during the request process.

Since there is no absolute security protection measure, we can only try to make it safe as much as possible. The following are two suggestions:

  • Use the Https protocol
  • Set a shorter Jwt validity period

hotcomm Category Latest News