Lesson 36 • Advanced Track
Authentication & Authorization with JWT
By the end of this lesson you'll understand exactly what a JSON Web Token is made of, be able to read a token apart in plain C#, check whether it has expired, and issue and validate real tokens in ASP.NET Core with JwtSecurityTokenHandler and AddJwtBearer — the foundation of stateless API security.
What You'll Learn
- Read a JWT's three parts: header, payload and signature
- Understand Base64Url encoding and why the payload is never secret
- Work with claims — the facts about a user stored in a token
- Issue and validate tokens with JwtSecurityTokenHandler
- Wire up AddAuthentication().AddJwtBearer(...) in ASP.NET Core
- Validate signature, expiry, issuer and audience — and avoid the classic mistakes
💡 Real-World Analogy
A JWT is a tamper-proof festival wristband. When you arrive, the box office checks your ticket once and snaps a wristband on you (the server checks your password once and issues a token). The band is printed with your details — your name, your zone, the day it expires — so anyone can read it (the payload is not secret). But it's also embossed with a special foil the festival prints (the signature): a guard at any stage can glance at it and know it's genuine and untampered without phoning the box office (no database lookup). Try to peel it off and re-stick it on a friend and the foil tears — the signature no longer matches. And the band expires at midnight: after that, no stage lets you in, and you'd need to go back to the box office for a fresh one.
The Three Parts of a JWT
A JWT is a single string: three Base64Url-encoded segments joined by dots — header.payload.signature. Base64Url is a URL-safe way of encoding bytes as text; it is encoding, not encryption, so the header and payload can be read by anyone who has the token. The signature is the only part that needs a secret to produce.
| Part | What it holds | Example contents |
|---|---|---|
| Header | The signing algorithm and token type | { "alg": "HS256", "typ": "JWT" } |
| Payload | The claims (facts about the user) — readable, not secret | { "sub": "user-123", "role": "Admin" } |
| Signature | Proof the token wasn't altered, made with the secret key | HMACSHA256(header.payload, secret) |
The claims in the payload come in two flavours: registered claims with standard short names, and custom claims you invent. Common registered claims:
| Claim | Meaning |
|---|---|
| sub | Subject — who the token is about (the user id) |
| iss | Issuer — who created and signed the token |
| aud | Audience — which API the token is meant for |
| exp | Expiry — a Unix timestamp (seconds) after which it's invalid |
| iat | Issued-at — when the token was created |
| jti | JWT id — a unique id, handy for revocation lists |
1. Reading a Token Apart
Before you reach for any library, it helps to see that a JWT really is just a string. Splitting it on the . character gives you the three Base64Url segments. The header and payload are only encoded, so you can read them — which is exactly why you must never put a password or secret in the payload. Read this worked example, run it, then you'll split a token yourself.
Worked example: a JWT is header.payload.signature
Run it and watch a token split into its three readable parts.
using System;
class Program
{
static void Main()
{
// A JWT is just a STRING with three parts joined by dots:
// header . payload . signature
// Each part is Base64Url text — readable, but NOT secret.
string token = "eyJhbGciOiJIUzI1NiJ9" + // header
".eyJzdWIiOiJ1c2VyLTEyMyJ9" + // payload
".3Tj8kKqHs2_signature_here"; // signature
// Split on the '.' to see the three
...Your turn. The program below is almost complete — fill in the two ___ blanks using the hints in the comments, then run it and check your output against the expected lines.
🎯 Your turn: split a token into three parts
Fill in the ___ blanks, then confirm you get three parts back.
using System;
class Program
{
static void Main()
{
// 🎯 YOUR TURN — split a token into its three parts, then print them.
string token = "header123.payload456.signature789";
// 1) Split the token string on the '.' character.
string[] parts = token.Split(___); // 👉 the dot character in 'single quotes': '.'
// 2) Print how many parts you got (a valid JWT always has 3).
Console.WriteLine($"Parts: {parts.Length}");
// 3) Print ea
...2. Claims and Signing
The payload carries claims — statements about the user such as "subject is user-123" and "role is Admin". The signature is what makes the token trustworthy: the server runs the header and payload through a one-way function together with a secret, and attaches the result. Change a single character of the payload and the signature no longer matches, so the token is rejected.
There are two families of signing. HMAC (e.g. HS256) uses one shared secret to both sign and verify — simple, fast, and ideal when the same service issues and checks tokens. RSA/ECDSA (e.g. RS256) uses a private key to sign and a separate public key to verify — so many services can validate tokens without ever holding the secret that creates them. That's the right choice for microservices and third-party APIs.
🔎 Deep Dive: HMAC vs RSA at a glance
HMAC (symmetric): one secret signs and verifies. If any verifier is compromised, the attacker can also forge tokens — because verifying and signing use the same key. Keep that key on the issuing service only.
RSA / ECDSA (asymmetric): a private key signs, a public key verifies. You can hand the public key to every downstream service freely — it can prove a token is genuine but cannot create one. This is why identity providers publish their public keys at a well-known URL.
HMAC HS256: sign(secret) verify(secret) // same key RSA RS256: sign(privateKey) verify(publicKey) // key pair
3. Issuing & Validating with JwtSecurityTokenHandler
In .NET, the type that builds and checks tokens is JwtSecurityTokenHandler. To issue a token you wrap your secret in a SymmetricSecurityKey, list your claims, set an expiry, and call WriteToken. To validate one you pass it through ValidateToken with a set of TokenValidationParameters describing what "valid" means — the right issuer, the right audience, an unexpired lifetime, and a signature that matches your key. This worked example does both in one run.
Worked example: issue then validate a real JWT
Create a signed token, then validate it and read its claims back out.
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
class Program
{
// The signing key must be SECRET and at least 32 bytes for HMAC-SHA256.
// In real apps this lives in configuration, never hard-coded.
private const string SecretKey = "ThisKeyMustBeAtLeast32BytesLong!!";
private const string Issuer = "LearnCodingFast";
private const string Audience = "LearnCodingFastAPI";
static vo
...4. Checking Expiry Yourself
The exp claim is stored as a Unix timestamp — the number of seconds since 1 January 1970. The library checks expiry for you, but knowing how to do it by hand makes the concept concrete and is useful when you decode a token outside ASP.NET. DateTimeOffset.FromUnixTimeSeconds(...) turns those seconds into a real date you can compare against DateTimeOffset.UtcNow.
Your turn. Fill in the two ___ blanks below to convert the timestamp and decide whether it's in the past:
🎯 Your turn: is this token expired?
Convert a Unix timestamp and compare it to now to detect expiry.
using System;
class Program
{
static void Main()
{
// 🎯 YOUR TURN — a JWT's "exp" claim is a Unix timestamp in SECONDS.
// Work out whether the token has already expired.
// This token claims to expire at this Unix time (seconds since 1970):
long expUnixSeconds = 1000000000; // that's the year 2001 — long gone!
// 1) Turn the Unix seconds into a DateTimeOffset.
DateTimeOffset expiry = DateTimeOffset.FromUnixTimeSeconds(___); // 👉 pa
...5. JWT Bearer Authentication in ASP.NET Core
In a real API you rarely call JwtSecurityTokenHandler by hand on every request. Instead you register JWT Bearer authentication once with AddAuthentication(...).AddJwtBearer(...) and the framework validates the Authorization: Bearer ... header automatically. Add app.UseAuthentication() before app.UseAuthorization(), then protect any endpoint with RequireAuthorization() (or the [Authorize] attribute on a controller).
Worked example: AddJwtBearer in Program.cs
Configure validation rules once and protect an endpoint with one line.
// Program.cs — wire up JWT Bearer authentication in ASP.NET Core.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// AddAuthentication(...).AddJwtBearer(...) tells the framework:
// "Read the Bearer token from the Authorization header and validate it."
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.T
...This snippet is a complete ASP.NET Core minimal API — paste it into a web project's Program.cs. The validation rules mirror the ones from section 3; the difference is the framework now applies them to every incoming request for you.
Pro Tips
- 💡 Keep the secret out of code: read it from configuration (
builder.Configuration["Jwt:Secret"]), backed by environment variables, Azure Key Vault, or AWS Secrets Manager. - 💡 Set
ClockSkew = TimeSpan.Zeroif you want exact expiry — the default allows a 5-minute grace window, which surprises people testing expiry. - 💡 Keep access tokens short-lived (15 minutes to 1 hour) and use a separate long-lived refresh token to get new ones, so a stolen access token is useless quickly.
- 💡 Use RSA (RS256) when multiple services validate tokens — they only need the public key, never the signing secret.
- 💡 Store tokens in an HttpOnly cookie, not
localStorage, so client-side JavaScript (and any injected script) can't read them.
Common Errors (and the fix)
- Storing the JWT in
localStorage: any cross-site script (XSS) can read it and impersonate the user. Use anHttpOnlycookie so JavaScript can't touch it. - Not validating the signature: decoding a token without verifying it (e.g.
ReadJwtTokenalone) trusts attacker-supplied data. Always callValidateTokenwithIssuerSigningKeyset. - Skipping issuer/audience checks: if
ValidateIssuerorValidateAudienceis off, a valid token meant for a different service is accepted by yours. SetValidIssuerandValidAudience. - "IDX10223: Lifetime validation failed. The token is expired": the
expclaim is in the past — the token must be reissued (this is the system working, not a bug). - Putting secrets in the payload: the payload is Base64Url, not encrypted — anyone can decode it. Never store passwords, card numbers, or anything sensitive in a claim.
- Long-lived access tokens: a 30-day access token that leaks gives an attacker 30 days of access. Keep them short and rotate via refresh tokens.
- "IDX10503: Signature validation failed": the secret used to validate doesn't match the one used to sign, or the token was altered. Confirm both ends use the same key.
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| Split a token | token.Split('.') | 3 parts: header, payload, signature |
| Signing key | new SymmetricSecurityKey(bytes) | ≥ 32 bytes for HS256 |
| Add a claim | new Claim("role", "Admin") | A fact in the payload |
| Write a token | handler.WriteToken(jwt) | Returns the string |
| Validate a token | handler.ValidateToken(t, rules, out _) | Returns a ClaimsPrincipal |
| Unix → date | DateTimeOffset.FromUnixTimeSeconds(exp) | exp is in seconds |
| Wire up in API | AddAuthentication().AddJwtBearer(...) | In Program.cs |
| Protect endpoint | .RequireAuthorization() | Or [Authorize] |
Frequently Asked Questions
Q: Is the data inside a JWT encrypted?
No. The header and payload are only Base64Url encoded, so anyone with the token can decode and read them. Only the signature uses a secret. Never put sensitive data in a claim.
Q: If anyone can read it, what stops someone editing the payload?
The signature. If you change even one character, the signature no longer matches the secret, and validation fails. That's how the server trusts a token without a database lookup.
Q: Where should I store the token in a browser?
In an HttpOnly, Secure cookie, not localStorage. An HttpOnly cookie is invisible to JavaScript, which shuts down the most common XSS theft path.
Q: What's the difference between authentication and authorization?
Authentication answers "who are you?" (the token is valid and identifies a user). Authorization answers "are you allowed to do this?" (the user's role or claims permit the action). In ASP.NET, UseAuthentication comes before UseAuthorization.
Q: When should I pick RSA over HMAC?
Use HMAC (HS256) when the same service issues and validates tokens. Use RSA (RS256) when separate services need to validate tokens — they get the public key and can verify without ever holding the signing secret.
Mini-Challenge: Validate a Fake Token
No blanks this time — just a brief and an outline. Using only plain logic (no libraries), validate the token: it must split into exactly three parts, its expiry must not be in the past, and its issuer must match the one you expect. Print ✅ Token accepted if all three pass, otherwise ❌ Token rejected. Run it and check it against the expected line in the comments.
🎯 Mini-Challenge: validate a fake token
Check 3 parts, not expired, and a matching issuer — all with plain logic.
using System;
class Program
{
static void Main()
{
// 🎯 MINI-CHALLENGE: validate a fake token with plain logic (no libraries).
// A token is VALID here only if ALL THREE checks pass:
// 1. It splits into exactly 3 parts on '.'
// 2. Its expiry (a Unix-seconds long below) is NOT in the past
// 3. Its issuer string matches the one you expect
//
// Steps:
// - Split the token on '.' and check parts.Length == 3
...🎉 Lesson Complete
- ✅ A JWT is a string of three Base64Url parts:
header.payload.signature - ✅ The payload is encoded, not encrypted — readable by anyone, so keep secrets out of it
- ✅ Claims (
sub,iss,aud,exp,role) carry the facts about the user - ✅ The signature proves the token wasn't tampered with — HMAC shares one secret, RSA uses a key pair
- ✅
JwtSecurityTokenHandlerissues tokens (WriteToken) and validates them (ValidateToken) - ✅ In ASP.NET Core,
AddJwtBearer(...)validates signature, expiry, issuer and audience for every request - ✅ Next lesson: Security Models — role-based, policy-based and claims-based authorization
Sign up for free to track which lessons you've completed and get learning reminders.