JWT stands for JSON Web Token, which essentially is a token that does not require a database to be validated. A signed JWT is comprised of three parts:
- Header encoded in Base64
- Payload encoded in Base64
- A hashed signature (Header+Payload+Secret Key)
The header common describes the algorithm and type, while the payload is the interesting part. The payload is where we can find claims in the form of a JSON key value pair. Lastly, the signature is simply a hash of the header and payload along with a secret key ๐
Header: {
"alg": "HS256",
"typ": "JWT"
}
Payload: {
"FirstName": "Obama",
"LastName": "ObamasLastName",
"Expires": "Wed April 01 2022 13:30:00 GMT+0200",
"Hotel": "Trivago"
}
Signature: Hash(
Base64UrlEncode(Header) + "." +
Base64UrlEncode(Payload),
SecretKey
)
Header in Base64 ๐ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
Payload in Base64 ๐ eyJGaXJzdE5hbWUiOiJPYmFtYSIsIkxhc3ROYW1lIjoiT2JhbWFzTGFzdE5hbWUiLCJFeHBpcmVzIjoiV2VkIEFwcmlsIDAxIDIwMjIgMTM6MzA6MDAgR01UKzAyMDAiLCJIb3RlbCI6IlRyaXZhZ28ifQ.
Hash(HS256) of Header + Payload + Secret Key ๐ 42oWgS45G6zM0KxVyUbhqdXWIEwHdgCY5OodoFg-kpY
Complete JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJGaXJzdE5hbWUiOiJPYmFtYSIsIkxhc3ROYW1lIjoiT2JhbWFzTGFzdE5hbWUiLCJFeHBpcmVzIjoiV2VkIEFwcmlsIDAxIDIwMjIgMTM6MzA6MDAgR01UKzAyMDAiLCJIb3RlbCI6IlRyaXZhZ28ifQ.42oWgS45G6zM0KxVyUbhqdXWIEwHdgCY5OodoFg-kpY
Notice: The JWT payload is only base64 encoded, so anyone can decode and read the data. Choose what data to share carefully
The JWT is created and signed once the clients provided us with a correct username and password and is sent back to the client in the response. The token can now be stored by the client for future requests. The safest storage would be in-memory, but that doesn't persist, so sessionStorage along with fingerprints is the commonly used storage.
- Jwt.io - Allows you to decode, verify and generate JWT.
- RFC7519 - Essentially the specification for JWT.
- Stackoverflow - If you can decode JWT, how are they secure?
- Zalando Engineering - The Purpose of JWT: Stateless Authentication
- Owasp - Token Storage on Client Side
There is a package that is especially interesting called System.IdentityModel.Tokens.Jwt. The package provides the necessary tooling to create, serialize and validate tokens.
// Serializing and creating a token
public string CreateToken(User user)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(ClaimTypes.Name, String.Concat(user.FirstName, " ", user.LastName)),
new Claim(ClaimTypes.Email, user.Email)
};
var securityTokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims.ToArray()),
Expires = DateTime.UtcNow.AddMinutes(1),
Issuer = "Ex -> http://localhost:8000/ (The issuer of the token, optional)",
Audience = "Ex -> http://localhost:8001/ (The audience aka recipient of the token, optional)",
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.ASCII.GetBytes("SecretKey")),
SecurityAlgorithms.HmacSha256Signature),
};
var securityTokenHandler = new JwtSecurityTokenHandler();
var token = securityTokenHandler.CreateToken(securityTokenDescriptor);
return securityTokenHandler.WriteToken(token);
}
// Manually Validating a token
public bool ValidateToken(string token)
{
var tokenValidationParams = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("SecretKey")),
// As stated before Issuer and Audience params are optional
ValidateIssuer = false,
ValidateAudience = false,
// To deal with time variations between the client and server. Default value = 300 sec.
ClockSkew = TimeSpan.Zero,
};
var securityTokenHandler = new JwtSecurityTokenHandler();
try
{
securityTokenHandler.ValidateToken(token, tokenValidationParams, out SecurityToken validatedToken);
return true;
}
catch (Exception)
{
return false;
}
}
}
These methods can be provided as a service for the application. Keep in mind that we can automatically authenticate JWT by providing an authentication handler to the framework's provided middleware.
- Stackoverflow - ClaimTypes v JwtRegisteredClaimNames
- RFC7519 Sec. 4 - Issuer and Audience
To automatically authenticate JWT, we require an authentication handler to make use of the JWT that has been issued to clients. Thankfully, we can use the package Microsoft.AspNetCore.Authentication.JwtBearer for this purpose. By chaining AddJwtBearer onto the authenticationBuilder, we are providing the middleware with the tooling required to authenticate the JWT.
builder.Services.AddAuthentication((opt) =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer((opt) =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("TheKeyRequiresAtLeast128bits")),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero,
};
});
- Stackoverflow - What exactly is UseAuthentication for?
- joonasw.net - Creating an authentication scheme in ASP.NET Core 2.0 (In depth about schemes and handlers)
As I mentioned before, the JWT is something that is stored by the client and sent along with each request. If you are using Swagger, you might want to add the code below to store and use the token in the correct manner. The code not only adds an interface for you to save the token but also adds it to the header for future HTTP requests.
Don't forget to add "Bearer" before the token.
builder.Services.AddSwaggerGen(opt =>
{
opt.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please insert JWT into the field :)",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
});
opt.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
}
});
});
- Stackoverflow - Adding a Authorization header for swagger