[ASP.NET Core][Entity Framework Core] Try JWT 2
Tags: csharp, aspnetcore
Published: 2022-03-27 16:57:40

## Intro
This time, I will see the details of my last project.

* [[ASP.NET Core][Entity Framework Core] Try JWT 1](https://dev.to/masanori_msl/aspnet-coreentity-framework-core-try-jwt-1-357a)

## About JWT
JWT(JSON Web Token) is made from 3 JSON values.

```
{Header}.{Payload}.{Signature}
```

Their values are Base64 encoded.

For example, "Header" has a Token type name and an Algorithm name.
(Signature can't be decoded directly because of encryption)

#### UserTokens.cs
```cs
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

namespace BookshelfSample.Users;

public class UserTokens: IUserTokens
{
...
    public string GenerateToken(ApplicationUser user)
    {
        return new JwtSecurityTokenHandler()
            .WriteToken(new JwtSecurityToken(this.config["Jwt:Issuer"],
                this.config["Jwt:Audience"],
                claims: new []
                {
                    new Claim(ClaimTypes.Email, user.Email)
                },
                expires: DateTime.Now.AddSeconds(30),
                signingCredentials: new SigningCredentials(
                    new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.config["Jwt:Key"])),
                    SecurityAlgorithms.HmacSha256)));
    }
    public IEnumerable<string> DecodeToken(string value)
    {
        foreach(var t in value.Split("."))
        {
            yield return Base64UrlEncoder.Decode(t);
        }
    }
}
```

#### ApplicationUserService.cs
```cs
...
    public async Task<UserActionResult> SigninAsync(SigninValue value, ISession session)
    {

        var target = await this.users.GetByEmailForSigninAsync(value.Email);
        if(target == null)
        {
            return ActionResultFactory.GetFailed("Invalid e-mail or password");
        }
        var result = await this.signInManager.PasswordSignInAsync(target, value.Password, false, false);
        if(result.Succeeded)
        {
            var token = this.userTokens.GenerateToken(target);
            session.SetString("user-token", token);
    
            logger.LogDebug($"Token: {token}");
            foreach(var t in this.userTokens.DecodeToken(token))
            {
                logger.LogDebug($"Decoded: {t}");
            }            
            return ActionResultFactory.GetSucceeded();
        }
        return ActionResultFactory.GetFailed("Invalid e-mail or password");
    }
...
```

#### Result
```
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJleGFtcGxlQG1haWwuY29tIiwiZXhwIjoxNjQ4MzEzMDY0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUxMTAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUxMTAifQ.AtzZ-RnSNG3SRLEsNEC3XI7OjiQ8Y8YWpRdBCTI5vNY
Decoded: {"alg":"HS256","typ":"JWT"}
Decoded: {"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress":"example@mail.com","exp":1648397836,"iss":"http://localhost:5110","aud":"http://localhost:5110"}
Decoded: ♀?▼?Q?   ←|?☻?L????YGn?Y~?$?1???
```

I can read the specifications of [Header](https://datatracker.ietf.org/doc/html/rfc7519/#section-3) and [Claims](https://datatracker.ietf.org/doc/html/rfc7519/#section-4) in RFC7519. 

## Authentication
For JWT bearer authentication, I added "AddJwtBearer" with options into AuthenticationBuilder.

#### Program.cs
```cs
...
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = builder.Configuration["Jwt:Issuer"],
                ValidAudience = builder.Configuration["Jwt:Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
            };
        });
...
```

If I set all of the "Validate~" properties "false", can I always use a same token value?
The answer is yes.

#### Program.cs
```cs
...
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateTokenReplay = false,
                ValidateActor = false,
                ValidateIssuer = false,
                ValidateAudience = false,
                ValidateLifetime = false,
                ValidateIssuerSigningKey = false,
                ValidIssuer = builder.Configuration["Jwt:Issuer"],
                ValidAudience = builder.Configuration["Jwt:Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
            };
        });
...
    app.Use(async (context, next) =>
    {
        var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJleGFtcGxlQG1haWwuY29tIiwiZXhwIjoxNjQ4MzEzMDY0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUxMTAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUxMTAifQ.AtzZ-RnSNG3SRLEsNEC3XI7OjiQ8Y8YWpRdBCTI5vNY";
        context.Request.Headers.Add("Authorization", $"Bearer {token}");
        await next();
    });
...
```

Because set tokens are encrypted with security keys, so I can't omit "IssuerSigningKey".

## Write Tokens
Next, I will see the creation of JWT.

#### UserTokens.cs
```cs
...
    public string GenerateToken(ApplicationUser user)
    {
        return new JwtSecurityTokenHandler()
            .WriteToken(new JwtSecurityToken(this.config["Jwt:Issuer"],
                this.config["Jwt:Audience"],
                claims: new []
                {
                    new Claim(ClaimTypes.Email, user.Email)
                },
                expires: DateTime.Now.AddSeconds(30),
                signingCredentials: new SigningCredentials(
                    new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.config["Jwt:Key"])),
                    SecurityAlgorithms.HmacSha256)));
    }
...
```

JwtSecurityToken has 5 overloads. But no one takes JwtHeader, JwtPayload, and JwtSecurityToken as arguments.

JwtSecurityToken.cs creates JwtHeader from SigningCredentials and JwtPayload from other arguments of the JwtSecurityToken constructor.

Header and Payload sections are Base64 encoded JSON.
But the Signature section is made from several Base64 encoded values.
So I can't decode directly.

* [JwtSecurityToken.cs - GitHub](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/System.IdentityModel.Tokens.Jwt/JwtSecurityToken.cs)
* [JwtSecurityTokenHandler.cs - GitHub](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs)
* [JwtHeader.cs - GitHub](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/039d09b7bc4c1671e1c4093d5a62d307c2f96935/src/System.IdentityModel.Tokens.Jwt/JwtHeader.cs)
* [JwtPayload.cs - GitHub](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/039d09b7bc4c1671e1c4093d5a62d307c2f96935/src/System.IdentityModel.Tokens.Jwt/JwtPayload.cs)
* [JwtTokenUtilities.cs - GitHub](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/039d09b7bc4c1671e1c4093d5a62d307c2f96935/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs)

## Security Key
Security Keys doesn't have any special requirements.

But because I use HMAC SHA256, the key value has to have at least 16 bits length.
If the key has less than 16 bits, I will get a runtime exception.

```
The encryption algorithm 'System.String' requires a key size of at least 'System.Int32' bits. Key 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey', is of size: 'System.Int32'. (Parameter 'key')
...
```

I can use symbols, Japanese, etc. in addition to alphabets and numbers.
Whether the key is long or short, the result is a 256-bit length.

## Resources
* [RFC 7516 - JSON Web Encryption (JWE)](https://datatracker.ietf.org/doc/html/rfc7516/)
* [RFC 7519 - JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519/)
* [RFC7521 - Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants](https://datatracker.ietf.org/doc/html/rfc7521)
* [RFC7523 - JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://datatracker.ietf.org/doc/html/rfc7523)
* [OAuth徹底入門 セキュアな認可システムを適用するための原則と実践(OAuth2 in Action)](https://www.seshop.com/product/detail/22242)
* [JwtBearerExtensions.AddJwtBearer Method - Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.jwtbearerextensions.addjwtbearer?view=aspnetcore-6.0)
* [Microsoft.IdentityModel.JsonWebTokens - GitHub](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/tree/039d09b7bc4c1671e1c4093d5a62d307c2f96935/src/Microsoft.IdentityModel.JsonWebTokens)
* [Secure Hash Standard - NIST](https://www.nist.gov/publications/secure-hash-standard?msclkid=0e9c642ca9f211ecbfa3bc2b2d413b41)