JSON Web Tokens

A JSON Web Token is a signed note an API gives you at sign-in: header, payload and signature, joined with dots. Anyone can read the payload, but nobody can change it without the key, so the API can trust the claims in it, such as who you are and your role, until it expires.

Journeyman · Web Development · REST API · Updated

After this lesson you can

  • Explain the three parts of a JSON Web Token, and what its signature protects
  • Create a signed token with claims, and validate it

Key takeaways

  • A JWT is three parts joined with dots: a header, a payload of claims such as sub and exp, and a signature.
  • The payload is encoded, not encrypted, so anyone can read it: never put a secret, such as a password, in it.
  • The signature is made with the API's secret key, so a token whose payload was changed fails validation.
  • ValidateTokenAsync checks the signature, the issuer, the audience and the expiry; only then can the claims be trusted.
  • A token can't be taken back until it expires, so keep it short-lived and always send it over HTTPS.

In Cookie Authentication and [Authorize], the MVC site signed you in with a cookie: the browser sent it back with every request, and the site knew who you were. An Angular app calling a Web API usually works differently. At sign-in, the API hands the app a token, and the app sends the token with every request:

GET /api/orders
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZGEi...

The most common kind of token is the JSON Web Token, or JWT (said "jot"). It's like a wristband at a festival: the gate checks your ticket once, gives you a wristband, and from then on every stall just looks at the wristband.

Three parts

A JWT is three pieces of text joined with dots: header.payload.signature.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZGEiLCJyb2xlIjoiQWRtaW4iLCJleHAiOjE3OTMwMDAwMDB9.Xv3r...
Part Holds Decoded
Header How it's signed {"alg":"HS256","typ":"JWT"}
Payload The claims: facts about the user {"sub":"ada","role":"Admin","exp":1793000000}
Signature Proof that the header and payload haven't changed (bytes, not JSON)

The header and payload are JSON, written in Base64Url, a way of turning bytes into text that's safe in an address. That's encoding, not encryption: anyone can decode it. Paste a token into https://jwt.io, and you can read every claim.

Some claims have short standard names:

Claim Means
sub The subject: who the token is about, such as a user name or id
exp When it expires, in seconds since 1 January 1970
iat When it was issued
iss The issuer: who made it, your API
aud The audience: who it's for, your app

You can add your own, such as role.

The signature

If anyone can read the payload, what stops someone from changing "role":"User" to "role":"Admin"? The signature.

The API makes the signature by hashing the header and payload together with a secret key that only the API knows, with HMAC-SHA256 (the HS256 in the header). When a token comes back, the API works the signature out again. If one character of the payload changed, the signatures don't match, and the API rejects the token. Without the key, nobody can make a matching signature.

So a JWT is:

  • Readable by anyone. Never put a secret, such as a password, in a payload.
  • Unchangeable without the key. The API can trust its claims.
  • Self-contained. The API doesn't look anything up: the token itself says who you are, until it expires.

Making and checking one

The package Microsoft.IdentityModel.JsonWebTokens makes and checks tokens. Try it as a one-file program, dotnet run token.cs:

#:package Microsoft.IdentityModel.JsonWebTokens@8.22.0
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("for-this-demo-only-a-32-byte-key!"));
var handler = new JsonWebTokenHandler();

string token = handler.CreateToken(new SecurityTokenDescriptor
{
    Subject = new ClaimsIdentity([new Claim("sub", "ada"), new Claim("role", "User")]),
    Issuer = "northwind-api",
    Audience = "northwind-app",
    Expires = DateTime.UtcNow.AddHours(1),
    SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
});

string[] parts = token.Split('.');
Console.WriteLine(quot;{parts.Length} parts");
Console.WriteLine(Base64UrlEncoder.Decode(parts[0]));

var rules = new TokenValidationParameters
{
    ValidIssuer = "northwind-api",
    ValidAudience = "northwind-app",
    IssuerSigningKey = key
};

TokenValidationResult result = await handler.ValidateTokenAsync(token, rules);
Console.WriteLine(quot;Valid: {result.IsValid}, sub: {result.Claims["sub"]}");

string payload = Base64UrlEncoder.Decode(parts[1]).Replace("\"User\"", "\"Admin\"");
string forged = quot;{parts[0]}.{Base64UrlEncoder.Encode(payload)}.{parts[2]}";
TokenValidationResult forgedResult = await handler.ValidateTokenAsync(forged, rules);
Console.WriteLine(quot;Forged valid: {forgedResult.IsValid}");
3 parts
{"alg":"HS256","typ":"JWT"}
Valid: True, sub: ada
Forged valid: False
  • SymmetricSecurityKey is the secret key. HMAC-SHA256 needs at least 32 bytes. In a real API, it's kept outside the code: in the next lesson, you'll put it in user secrets.
  • SecurityTokenDescriptor describes the token: the claims, the issuer and audience, when it expires, and how to sign it.
  • ValidateTokenAsync checks the signature, the issuer, the audience, and that it hasn't expired. IsValid says whether it passed, and Claims holds what's inside.
  • The forged token has "role":"Admin" in its payload, but the old signature: it fails.

Expiry

A token can't be taken back: until it expires, it works, for whoever holds it. So tokens are short-lived: an hour, or even fifteen minutes, then the user signs in again (bigger sites hand out a second, longer-lived refresh token to get a new one quietly). And always send tokens over HTTPS, so nobody can copy one on its way.

One detail: by default, a token keeps working for up to five minutes after its exp. That's ClockSkew, a grace period for computers whose clocks differ slightly. Set ClockSkew = TimeSpan.Zero in the rules when a token must stop exactly at exp.

Check yourself

2 questions to try on the lesson page.

So what

A JWT is a signed, readable note: the payload says who you are, and the signature proves the API wrote it. The API checks the signature, the issuer, the audience and the expiry, and then trusts the claims, without a database lookup.

Key terms

Cheat sheets