[ASP.NET Core][Entity Framework Core] Try JWT 2

masanori_msl

Masui Masanori

Posted on March 27, 2022

[ASP.NET Core][Entity Framework Core] Try JWT 2

Intro

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

About JWT

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

{Header}.{Payload}.{Signature}
Enter fullscreen mode Exit fullscreen mode

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

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

ApplicationUserService.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");
    }
...
Enter fullscreen mode Exit fullscreen mode

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???
Enter fullscreen mode Exit fullscreen mode

I can read the specifications of Header and Claims in RFC7519.

Authentication

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

Program.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"])),
            };
        });
...
Enter fullscreen mode Exit fullscreen mode

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

Program.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();
    });
...
Enter fullscreen mode Exit fullscreen mode

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

...
    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)));
    }
...
Enter fullscreen mode Exit fullscreen mode

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.

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')
...
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
masanori_msl
Masui Masanori

Posted on March 27, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related