# 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 November 3, 2026

## 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:

```text
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**.

```text
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`:

```csharp
#: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($"{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($"Valid: {result.IsValid}, sub: {result.Claims["sub"]}");

string payload = Base64UrlEncoder.Decode(parts[1]).Replace("\"User\"", "\"Admin\"");
string forged = $"{parts[0]}.{Base64UrlEncoder.Encode(payload)}.{parts[2]}";
TokenValidationResult forgedResult = await handler.ValidateTokenAsync(forged, rules);
Console.WriteLine($"Forged valid: {forgedResult.IsValid}");
```

```text
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

- [JWT](https://www.ask-codey.com/glossary/jwt)
- [Claim](https://www.ask-codey.com/glossary/claim)
- [Token Signature](https://www.ask-codey.com/glossary/token-signature)

## Cheat sheets

- [JSON Web Tokens](https://www.ask-codey.com/cheat-sheets/security-basics-json-web-tokens-cheat-sheet)

Source: https://www.ask-codey.com/lessons/security-basics-json-web-tokens
