JWT Authentication & Authorization

Reviewed & published by Brayan K

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.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 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.

PartWhat it holdsExample contents
HeaderThe signing algorithm and token type{ "alg": "HS256", "typ": "JWT" }
PayloadThe claims (facts about the user) — readable, not secret{ "sub": "user-123", "role": "Admin" }
SignatureProof the token wasn't altered, made with the secret keyHMACSHA256(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:

ClaimMeaning
subSubject — who the token is about (the user id)
issIssuer — who created and signed the token
audAudience — which API the token is meant for
expExpiry — a Unix timestamp (seconds) after which it's invalid
iatIssued-at — when the token was created
jtiJWT 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.

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 pieces.
        string[] parts = token.Split('.');

        Console.WriteLine($"Parts found: {parts.Length}");   // Parts found: 3
        Console.WriteLine($"Header:    {parts[0]}");         // eyJhbGciOiJIUzI1NiJ9
        Console.WriteLine($"Payload:   {parts[1]}");         // eyJzdWIiOiJ1c2VyLTEyMyJ9
        Console.WriteLine($"Signature: {parts[2]}");         // 3Tj8kKqHs2_signature_here

        // The header and payload are only ENCODED, not encrypted.
        // Anyone can read them — so never put secrets in the payload.
        // The signature is what proves the token wasn't tampered with.
    }
}

// ✅ Expected output:
//    Parts found: 3
//    Header:    eyJhbGciOiJIUzI1NiJ9
//    Payload:   eyJzdWIiOiJ1c2VyLTEyMyJ9
//    Signature: 3Tj8kKqHs2_signature_here

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.

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 each part. Index 0 is the header, 1 the payload, 2 the signature.
        Console.WriteLine($"Header:    {parts[0]}");
        Console.WriteLine($"Payload:   {parts[___]}");   // 👉 the payload is index 1
        Console.WriteLine($"Signature: {parts[2]}");

        // ✅ Expected output:
        //    Parts: 3
        //    Header:    header123
        //    Payload:   payload456
        //    Signature: signature789
    }
}

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.

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 void Main()
    {
        // === 1. ISSUE a token ===
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        // Claims = the facts about the user, baked into the payload.
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, "user-123"),   // subject (who)
            new Claim(ClaimTypes.Role, "Admin"),                  // their role
            new Claim(JwtRegisteredClaimNames.Jti, "abc-001")     // unique token id
        };

        var jwt = new JwtSecurityToken(
            issuer: Issuer,
            audience: Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddHours(1),   // short-lived!
            signingCredentials: creds);

        string token = new JwtSecurityTokenHandler().WriteToken(jwt);
        Console.WriteLine("Token issued (first 20 chars):");
        Console.WriteLine(token.Substring(0, 20) + "...");   // eyJhbGciOiJIUzI1NiIs...

        // === 2. VALIDATE the same token ===
        var handler = new JwtSecurityTokenHandler();
        var rules = new TokenValidationParameters
        {
            ValidateIssuer = true,   ValidIssuer = Issuer,        // who made it
            ValidateAudience = true, ValidAudience = Audience,    // who it's for
            ValidateLifetime = true,                              // is it expired?
            IssuerSigningKey = key,                               // verify signature
            ClockSkew = TimeSpan.Zero                             // no grace period
        };

        ClaimsPrincipal person = handler.ValidateToken(token, rules, out _);
        string sub  = person.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
        string role = person.FindFirst(ClaimTypes.Role)?.Value;

        Console.WriteLine($"Valid! Subject: {sub}, Role: {role}");
        // ✅ Expected output:
        //    Token issued (first 20 chars):
        //    eyJhbGciOiJIUzI1NiIs...
        //    Valid! Subject: user-123, Role: Admin
    }
}

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:

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(___);  // 👉 pass expUnixSeconds

        // 2) Compare it against now. If expiry is BEFORE now, it's expired.
        bool isExpired = expiry ___ DateTimeOffset.UtcNow;   // 👉 use the 'less than' operator: <

        Console.WriteLine($"Expires at: {expiry:yyyy-MM-dd}");
        Console.WriteLine($"Expired?    {isExpired}");

        // ✅ Expected output:
        //    Expires at: 2001-09-09
        //    Expired?    True
    }
}

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).

// 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.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "LearnCodingFast",
            ValidateAudience = true,
            ValidAudience = "LearnCodingFastAPI",
            ValidateLifetime = true,          // reject expired tokens
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]!)),
            ClockSkew = TimeSpan.Zero          // no 5-minute default tolerance
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

// ORDER MATTERS: authentication (who are you?) before authorization (allowed?).
app.UseAuthentication();
app.UseAuthorization();

// A protected endpoint — only requests with a VALID token get through.
app.MapGet("/profile", (HttpContext ctx) =>
{
    var name = ctx.User.Identity?.Name ?? "unknown";
    return Results.Ok($"Hello {name}, your token checked out.");
}).RequireAuthorization();

app.Run();

// ✅ Behaviour:
//    GET /profile  with no token        -> 401 Unauthorized
//    GET /profile  with a valid token   -> 200 "Hello ..., your token checked out."
//    GET /profile  with an expired token-> 401 Unauthorized

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

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNotes
Split a tokentoken.Split('.')3 parts: header, payload, signature
Signing keynew SymmetricSecurityKey(bytes)≥ 32 bytes for HS256
Add a claimnew Claim("role", "Admin")A fact in the payload
Write a tokenhandler.WriteToken(jwt)Returns the string
Validate a tokenhandler.ValidateToken(t, rules, out _)Returns a ClaimsPrincipal
Unix → dateDateTimeOffset.FromUnixTimeSeconds(exp)exp is in seconds
Wire up in APIAddAuthentication().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.

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
        //   - Use DateTimeOffset.FromUnixTimeSeconds(exp) and compare to UtcNow
        //   - Compare issuer to expectedIssuer with ==
        //   - Print "✅ Token accepted" if all pass, else "❌ Token rejected"

        string token = "header.payload.signature";
        long exp = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds(); // 1h ahead
        string issuer = "LearnCodingFast";
        string expectedIssuer = "LearnCodingFast";

        // ✅ Expected output (with the values above):
        //    ✅ Token accepted

        // your code here
    }
}

🎉 Lesson Complete

Practice quiz

What are the three parts of a JWT, joined by dots?

  • key.value.hash
  • user.role.expiry
  • header.payload.signature
  • issuer.audience.subject

Answer: header.payload.signature. A JWT is a single string of three Base64Url segments: header, payload, and signature, separated by dots.

Is the payload of a JWT encrypted?

  • No, it is only Base64Url-encoded and readable by anyone
  • Yes, only the server can read it
  • Yes, with the signing key
  • Only if HTTPS is used

Answer: No, it is only Base64Url-encoded and readable by anyone. The header and payload are only encoded (Base64Url), not encrypted — anyone with the token can read them, so never put secrets in the payload.

What is the purpose of the signature?

  • To encrypt the payload
  • To store the user's password
  • To compress the token
  • To prove the token wasn't tampered with

Answer: To prove the token wasn't tampered with. The signature proves the token wasn't altered; change one character of the payload and the signature no longer matches, so validation fails.

What does the 'sub' registered claim represent?

  • The signing algorithm
  • The subject — who the token is about (the user id)
  • The expiry time
  • The audience

Answer: The subject — who the token is about (the user id). The 'sub' (subject) claim identifies who the token is about, typically the user id.

What does the 'exp' claim store?

  • A Unix timestamp (seconds) after which the token is invalid
  • The issuer name
  • The encryption mode
  • The user's role

Answer: A Unix timestamp (seconds) after which the token is invalid. The 'exp' (expiry) claim is a Unix timestamp in seconds; after that moment the token is considered expired.

How does HMAC (HS256) signing differ from RSA (RS256)?

  • HMAC uses a key pair; RSA uses one shared secret
  • They are identical
  • HMAC uses one shared secret to sign and verify; RSA uses a private key to sign and a public key to verify
  • RSA cannot verify tokens

Answer: HMAC uses one shared secret to sign and verify; RSA uses a private key to sign and a public key to verify. HMAC uses a single shared secret for both signing and verifying; RSA uses a private key to sign and a separate public key to verify.

Why is RSA (RS256) preferred when multiple services validate tokens?

  • It is faster than HMAC
  • Each verifier only needs the public key and cannot forge tokens
  • It produces shorter tokens
  • It encrypts the payload

Answer: Each verifier only needs the public key and cannot forge tokens. With RSA, downstream services get the public key — they can verify a token is genuine but cannot create one, unlike HMAC's shared secret.

Where should a JWT be stored in a browser to reduce XSS theft?

  • In localStorage
  • In a global JavaScript variable
  • In the URL
  • In an HttpOnly cookie, invisible to JavaScript

Answer: In an HttpOnly cookie, invisible to JavaScript. An HttpOnly cookie can't be read by client-side JavaScript, shutting down the most common XSS token-theft path that localStorage exposes.

What is the difference between authentication and authorization?

  • They are the same thing
  • Authentication asks 'who are you?'; authorization asks 'are you allowed?'
  • Authentication asks 'are you allowed?'; authorization asks 'who are you?'
  • Authentication is only for APIs

Answer: Authentication asks 'who are you?'; authorization asks 'are you allowed?'. Authentication establishes identity (who you are); authorization decides permission (whether you may do something). UseAuthentication comes before UseAuthorization.

In ASP.NET Core, which middleware must come first in the pipeline?

  • app.UseAuthorization() before app.UseAuthentication()
  • Order doesn't matter
  • app.UseAuthentication() before app.UseAuthorization()
  • Both must be registered as services only

Answer: app.UseAuthentication() before app.UseAuthorization(). Authentication (who are you?) must run before authorization (are you allowed?), so UseAuthentication() comes before UseAuthorization().

Continue this course