A JSON Web Token (JWT) is a compact, signed token that encodes claims about an authenticated subject so a server can verify identity without querying a session store on every request.
In simpler words
A JWT is a signed ID card: once someone logs in, the server hands them a token that proves who they are without looking anything up again for every request.
This API issues JWTs on login and reads them from an httpOnly cookie on every request. Bearer-header JWTs are the same token format used by mobile apps and API tooling — a useful mental model even though the browser flow here relies on cookies.
Trace a JWT from login to a request that reuses it
Explain why the payload stays small (sub, email, role)
Compare cookie transport with the bearer-header mental model
How this API issues and reads a JWT
Definition
AuthService.login verifies credentials, asks JwtService.signAsync for a signed token carrying sub, email, and role, and stores that token in an httpOnly access_token cookie; JwtStrategy later extracts and verifies the same token on every request.
In simpler words
Login proves identity once; the cookie carries a compact, tamper-evident version of that decision on every following request.
The signature (not encryption) is what makes a JWT trustworthy: the server can detect any edit to the payload, but the payload itself is base64, not secret.
Because verification only needs the signing secret, Nest can authenticate a request without a database round trip for the token itself — it still loads the user record to get the current role and confirm the account still exists.
Only sub, email, and role travel in the token — enough to authorize, not a full user profile.
Cookie session here, bearer header as the wider mental model
Definition
Cookie-based JWT delivery lets the browser attach the token automatically on same-site credentialed requests, while bearer-header delivery requires the client to read the token and set an Authorization: Bearer header on every request.
In simpler words
The browser never touches the token string here; other JWT clients — mobile apps, curl, Swagger — typically carry the same token by hand in a header.
JwtStrategy in this repo accepts both: it tries the access_token cookie first, then falls back to an Authorization Bearer header — that fallback exists mainly for Swagger and tooling, not for the web app.
A cookie flow needs credentials: true on both the client and Nest CORS; a bearer flow needs the client to store and attach the token itself, which is exactly what an httpOnly cookie is designed to avoid.
Cookie wins when present; bearer is the fallback used by Swagger’s Authorize flow and other non-browser clients.
Signed ≠ encrypted; expiry and CSRF notes
Definition
A JWT signature proves integrity and authenticity of the claims under a shared secret (or private key); it does not hide the payload. Cookie-based APIs must also consider cross-site request risks.
In simpler words
Anyone can decode the payload; only the server with the secret can forge a valid signature. Treat claims as public-ish, not secret.
Do not put passwords, tokens, or PII beyond what the API truly needs in the JWT. Prefer ids and role.
Expiry (exp) bounds damage if a token leaks. This course uses a multi-day cookie maxAge — production teams often shorten access tokens and add refresh flows.
httpOnly + Secure (in HTTPS) + SameSite=Lax/Strict reduces XSS token theft and some CSRF classes. State-changing cookie APIs still need CSRF strategy when cross-site posts are possible.
@Public() routes skip JwtAuthGuard — only mark truly public endpoints (login, health, register).
Mistake: localStorage JWT
// Wrong — any XSS can steal the token
localStorage.setItem('token', jwt);
// Right in this monorepo — httpOnly cookie set by Nest
res.cookie(AUTH_COOKIE_NAME, token, { httpOnly: true, sameSite: 'lax', secure });
If JavaScript can read the credential, an XSS bug becomes a session theft bug.
Mistake: huge JWT payload
// Wrong — entire user profile + permissions tree in the token
signAsync({ ...userEntity, permissions: fullTree })
// Right — minimal claims; load fresh user/role server-side when needed
signAsync({ sub: user.id, email: user.email, role: user.role })
Large tokens bloat every request and go stale when roles change; this API reloads the user in JwtStrategy.
Keep in mind
Keep JWT payloads small and non-sensitive — sub, email, role, nothing else.
Treat the cookie as the transport used here; the bearer header is the same token format used elsewhere.
Never store a JWT in localStorage when an httpOnly cookie is available.
Signed means tamper-evident, not secret — do not put secrets in claims.
Test
Check your understanding
At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥ 80% (enforced by the API) to save progress.