JWT Authentication Explained: Theory, Security, and a Complete Node.js & Express Example
JSON Web Tokens (JWT) are one of the most widely used technologies for implementing authentication in modern web applications. You will find JWT in: Angular applications React applications Node.js APIs Mobile applicat

JSON Web Tokens (JWT) are one of the most widely used technologies for implementing authentication in modern web applications.
You will find JWT in:
- Angular applications
- React applications
- Node.js APIs
- Mobile applications
- Microservices
- BFF architectures
- API gateways
- Distributed systems
But JWT is often misunderstood.
Many developers know how to write:
jwt.sign(...)
and:
jwt.verify(...)
without really understanding what a JWT is, what problem it solves, how it works internally, where it should be stored, and how to secure it properly.
This article goes beyond simply generating a token.
We will understand JWT from the ground up and build a complete authentication flow using Node.js, Express, TypeScript, and MongoDB-style concepts.
1. Authentication vs Authorization
Before discussing JWT, we need to understand two fundamental concepts.
Authentication
Authentication answers:
Who are you?
For example:
User → Email + Password
↓
Server
↓
Is the user valid?
↓
Yes
The server identifies the user.
Authorization
Authorization answers:
What are you allowed to do?
For example:
User
├── Read products ✅
├── Create products ❌
├── Delete products ❌
└── Manage users ❌
Authentication comes first.
Authorization comes after authentication.
2. What Is JWT?
JWT stands for:
JSON Web Token
It is a compact token format used to securely transmit claims between parties.
A JWT typically looks like this:
xxxxx.yyyyy.zzzzz
For example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NSIsInJvbGUiOiJ1c2VyIn0
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
A JWT contains three parts:
HEADER.PAYLOAD.SIGNATURE
3. JWT Structure
Let's break it down.
Header
The header describes the token.
Example:
{
"alg": "HS256",
"typ": "JWT"
}
alg represents the signing algorithm.
typ represents the token type.
Payload
The payload contains claims.
Example:
{
"sub": "12345",
"email": "[email protected]",
"role": "admin"
}
Claims are pieces of information about the token or its subject.
Common claims include:
sub
iss
aud
exp
iat
nbf
jti
For example:
{
"sub": "12345",
"role": "admin",
"iat": 1720000000,
"exp": 1720003600
}
Signature
The signature allows the server to verify that the token has not been modified.
Conceptually:
signature =
HMAC(
base64(header) + "." + base64(payload),
secret
)
So the final token becomes:
HEADER.PAYLOAD.SIGNATURE
4. Important: JWT Is Not Encryption
This is one of the most important JWT concepts.
A normal JWT payload is not encrypted.
It is encoded using Base64URL.
For example:
{
"email": "[email protected]",
"role": "admin"
}
can be decoded.
Therefore, you should never put sensitive information inside a JWT.
Don't do this:
{
"password": "MySuperSecretPassword"
}
Don't store:
credit card numbers
passwords
private secrets
sensitive personal data
inside the payload.
JWT provides integrity and authenticity through signing, not confidentiality.
If you need confidentiality, encryption is a different concept.
5. Why Do We Need JWT?
Imagine an API:
POST /login
The user sends:
{
"email": "[email protected]",
"password": "123456"
}
The server validates the credentials.
Now the user wants:
GET /api/profile
How does the server know who the user is?
One traditional solution is a server-side session.
Another solution is a JWT.
The server can issue:
Access Token
The client then sends the token with future requests.
For example:
Authorization: Bearer eyJhbGciOi...
The server verifies the token.
6. JWT Authentication Flow
A typical authentication flow looks like this:
Client
|
| email + password
v
POST /login
|
v
Server
|
Validate credentials
|
v
Generate JWT
|
v
Client
|
Store/use token
|
| Authorization: Bearer JWT
v
Protected API
|
Verify JWT
|
v
Response
The important idea is:
The server doesn't need to receive the user's password on every request.
7. Stateless Authentication
One major advantage of JWT is that authentication can be stateless.
With traditional sessions:
Client
|
Session ID
|
Server
|
Session Store
The server needs to maintain session state.
With JWT:
Client
|
JWT
|
Server
The token itself contains the claims required to identify the user.
The server can verify the signature without necessarily looking up a session.
This can be useful in distributed systems.
8. JWT in a Microservices Architecture
Consider:
Client
|
v
API Gateway
|
+----------+----------+
| | |
v v v
Users Orders Payments
Service Service Service
The client can send:
Authorization: Bearer <token>
Each service can validate the token.
This is one reason JWT is popular in distributed architectures.
However, "stateless" does not mean JWT automatically solves every distributed authentication problem.
Revocation, rotation, expiration, key management, and authorization still need to be designed carefully.
9. Creating JWTs with Node.js
Let's build a simple example.
Install:
npm install express jsonwebtoken bcrypt
For TypeScript:
npm install -D @types/express @types/jsonwebtoken @types/bcrypt
10. JWT Secret
Never hardcode your production secret:
const JWT_SECRET = "my-secret";
Instead:
JWT_SECRET=super-long-random-secret
Then:
const JWT_SECRET = process.env.JWT_SECRET!;
A production secret should be long, random, and protected.
For stronger architectures, asymmetric keys such as RSA or EC keys can be used instead of a shared HMAC secret.
11. Generating an Access Token
Example:
import jwt from "jsonwebtoken";
const token = jwt.sign(
{
sub: user.id,
role: user.role
},
process.env.JWT_SECRET!,
{
expiresIn: "15m"
}
);
The resulting token contains claims such as:
{
"sub": "12345",
"role": "user",
"iat": 1720000000,
"exp": 1720000900
}
12. Why Use sub?
sub means:
Subject
It commonly identifies the user.
Instead of putting:
{
"userId": "12345"
}
you can use:
{
"sub": "12345"
}
Then your application can interpret:
const userId = payload.sub;
13. Login Endpoint
A simplified login endpoint might look like this:
app.post("/auth/login", async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({
message: "Invalid credentials"
});
}
const validPassword = await bcrypt.compare(
password,
user.password
);
if (!validPassword) {
return res.status(401).json({
message: "Invalid credentials"
});
}
const accessToken = jwt.sign(
{
sub: user.id,
role: user.role
},
process.env.JWT_SECRET!,
{
expiresIn: "15m"
}
);
res.json({
accessToken
});
});
The process is:
Email
↓
Find user
↓
Compare password hash
↓
Generate access token
↓
Return token
14. Passwords Should Never Be Stored as Plain Text
Never do:
user.password = password;
Instead, hash the password.
Example:
const hashedPassword = await bcrypt.hash(password, 12);
Store:
hashedPassword
not:
password
During login:
const valid = await bcrypt.compare(
password,
hashedPassword
);
JWT does not replace password hashing.
They solve different problems.
bcrypt
↓
Password protection
JWT
↓
Authentication token
15. Protecting Routes with Middleware
Now we need to protect:
GET /api/profile
We can create authentication middleware.
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
export interface AuthRequest extends Request {
user?: {
id: string;
role: string;
};
}
export function authenticate(
req: AuthRequest,
res: Response,
next: NextFunction
) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
message: "Authentication required"
});
}
const [scheme, token] = authHeader.split(" ");
if (scheme !== "Bearer" || !token) {
return res.status(401).json({
message: "Invalid authorization header"
});
}
try {
const payload = jwt.verify(
token,
process.env.JWT_SECRET!
) as jwt.JwtPayload;
req.user = {
id: payload.sub as string,
role: payload.role as string
};
next();
} catch {
return res.status(401).json({
message: "Invalid or expired token"
});
}
}
16. Using the Middleware
Now:
app.get(
"/api/profile",
authenticate,
(req: AuthRequest, res) => {
res.json({
message: "Protected profile",
user: req.user
});
}
);
The request flow becomes:
Request
|
Authorization Header
|
Extract JWT
|
Verify Signature
|
Check Expiration
|
Extract User
|
next()
|
Controller
17. What Happens When the Token Is Modified?
Suppose the original payload is:
{
"sub": "123",
"role": "user"
}
An attacker changes it to:
{
"sub": "123",
"role": "admin"
}
The attacker cannot simply change the payload and create a valid signature.
The server calculates/verifies the signature using its trusted key.
The modified token fails verification.
Therefore:
Original Token
↓
Valid Signature
↓
Accepted
Modified Token
↓
Invalid Signature
↓
Rejected
That's the core security property of signed JWTs.
18. JWT Verification vs JWT Decoding
This distinction is extremely important.
You might see:
jwt.decode(token);
and think:
"The token is valid."
No.
decode() only decodes the token.
It does not prove that the token was signed by a trusted issuer.
Use:
jwt.verify(token, secret);
for authentication.
Conceptually:
decode()
↓
Read data
verify()
↓
Validate signature + token constraints
Never authenticate a user using only decode().
19. JWT Expiration
JWTs should normally expire.
Example:
jwt.sign(
{
sub: user.id
},
JWT_SECRET,
{
expiresIn: "15m"
}
);
Why?
Because if a token is stolen, a long-lived token gives an attacker a long window of opportunity.
Short-lived access tokens reduce this risk.
A common architecture is:
Access Token
15 minutes
Refresh Token
Longer lifetime
20. Access Token vs Refresh Token
This is one of the most important patterns in modern authentication.
Access Token
Used to access APIs.
Example:
Lifetime: 5–15 minutes
Refresh Token
Used to obtain a new access token.
Example:
Lifetime: days/weeks
The architecture becomes:
Login
|
+-------+-------+
| |
v v
Access Token Refresh Token
short-lived long-lived
|
v
API
When the access token expires:
Client
|
| refresh token
v
POST /auth/refresh
|
v
Server
|
v
New Access Token
21. Why Not Make the Access Token Last 30 Days?
You technically can.
But it's generally a bad security tradeoff.
Imagine:
Access Token
Expires in 30 days
If it gets stolen today, the attacker may have a valid credential for a long period.
Instead:
Access Token
Expires in 15 minutes
and:
Refresh Token
Longer lifetime
This limits the lifetime of the credential used for normal API access.
22. Refresh Token Rotation
A stronger architecture can rotate refresh tokens.
Example:
Refresh Token A
|
v
Refresh Endpoint
|
+----> Access Token B
|
+----> Refresh Token B
The old refresh token becomes invalid.
This can help detect replay attacks and reduce the usefulness of stolen refresh tokens.
A server-side store is often used to track refresh-token state.
For example:
Redis
refresh-token-id → user/session metadata
or a database.
23. JWT Storage: Where Should You Store It?
This is one of the most debated JWT topics.
Possible locations include:
localStorage
sessionStorage
cookies
memory
Each has trade-offs.
24. localStorage
Example:
localStorage.setItem(
"accessToken",
token
);
Then:
const token = localStorage.getItem(
"accessToken"
);
The problem is that JavaScript can access localStorage.
If your application has an XSS vulnerability, malicious JavaScript may potentially read the token.
Therefore:
Storing long-lived authentication credentials in localStorage requires careful consideration of XSS risk.
25. HttpOnly Cookies
A common alternative is:
Set-Cookie: refreshToken=...;
HttpOnly;
Secure;
SameSite=Lax
HttpOnly means client-side JavaScript cannot directly read the cookie.
This reduces the impact of token theft through straightforward JavaScript access.
However, cookies introduce another concern:
CSRF
Therefore, cookie-based authentication must also use appropriate CSRF protections and cookie configuration.
26. A Common Modern Architecture
A practical pattern is:
Access Token
↓
Short-lived
↓
Memory
Refresh Token
↓
Long-lived
↓
HttpOnly + Secure Cookie
For example:
Browser
|
+---- Access Token → memory
|
+---- Refresh Token → HttpOnly Cookie
When the page reloads:
Browser
|
| Refresh Cookie
v
/auth/refresh
|
v
New Access Token
This is often preferable to keeping a long-lived access token in JavaScript-readable storage.
The exact design depends on the application architecture and threat model.
27. JWT and XSS
JWT does not magically protect your application from XSS.
Suppose an attacker injects:
<script>
// malicious code
</script>
If your access token is available to JavaScript:
localStorage.getItem("accessToken");
the attacker may potentially access it.
Therefore, authentication security must include:
XSS protection
+
Secure token handling
+
Content Security Policy
+
Input/output handling
JWT is only one part of the security architecture.
28. JWT and CSRF
If you use cookies for authentication, browsers automatically attach cookies to matching requests.
This creates CSRF considerations.
For example:
Victim Browser
|
| Cookie automatically attached
v
Bank API
A malicious website may try to trigger an unwanted request.
Common defenses include:
SameSite cookies
CSRF tokens
Origin/Referer validation
Proper request design
The correct strategy depends on the architecture.
29. Role-Based Authorization
Authentication tells us:
Who is the user?
Authorization tells us:
What can the user do?
Suppose the JWT contains:
{
"sub": "123",
"role": "admin"
}
We can create an authorization middleware.
function authorize(...roles: string[]) {
return (
req: AuthRequest,
res: Response,
next: NextFunction
) => {
if (!req.user) {
return res.status(401).json({
message: "Authentication required"
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
message: "Forbidden"
});
}
next();
};
}
Use it:
app.delete(
"/api/users/:id",
authenticate,
authorize("admin"),
deleteUser
);
Now:
No Token
↓
401 Unauthorized
Valid Token + user role
↓
403 Forbidden
Valid Token + admin role
↓
Allowed
30. 401 vs 403
This distinction matters.
401 Unauthorized
Usually means:
The request does not have valid authentication credentials.
Examples:
Missing token
Expired token
Invalid token
403 Forbidden
Usually means:
The user is authenticated but does not have permission.
Example:
Authenticated user
+
role = user
↓
Admin endpoint
↓
403
31. Don't Trust Authorization Claims Blindly
Suppose the JWT says:
{
"sub": "123",
"role": "admin"
}
Should your application always trust the role forever?
Not necessarily.
Imagine:
User is admin
↓
JWT issued
↓
Admin account downgraded
↓
JWT still valid
The old token may continue carrying:
role = admin
until it expires.
This is one reason authorization design sometimes requires server-side state.
For highly sensitive operations, the application may re-check the user's current permissions.
32. JWT Claims
Let's examine common claims.
iss
Issuer:
{
"iss": "https://auth.example.com"
}
Identifies who issued the token.
aud
Audience:
{
"aud": "api.example.com"
}
Identifies the intended recipient.
sub
Subject:
{
"sub": "user-123"
}
Identifies the principal.
exp
Expiration:
{
"exp": 1720003600
}
After this time, the token should no longer be accepted.
iat
Issued-at time:
{
"iat": 1720000000
}
nbf
Not-before:
{
"nbf": 1720000100
}
The token should not be accepted before this time.
jti
JWT ID:
{
"jti": "unique-token-id"
}
Useful for token identification and certain revocation/replay-detection designs.
33. Algorithm Choice
JWT supports different signing algorithms.
Examples:
HS256
RS256
ES256
HS256
Uses one shared secret.
Private signing secret
+
Verification secret
Both sides need access to the same secret.
This can be simple for a single backend.
RS256
Uses asymmetric cryptography.
Private Key
↓
Sign
Public Key
↓
Verify
This is particularly useful in distributed architectures.
For example:
Authorization Server
|
Private Key
|
JWT
|
v
+-------+-------+-------+
| | | |
v v v v
API 1 API 2 API 3 API 4
|
Public Key
Services can verify tokens without possessing the private signing key.
34. Never Accept Arbitrary Algorithms
A dangerous implementation mistake is allowing the token itself to dictate a signing algorithm without enforcing what your application expects.
Your verification configuration should explicitly define trusted algorithms.
Conceptually:
jwt.verify(token, publicKey, {
algorithms: ["RS256"]
});
The server should have a clear trust policy.
35. Complete Authentication Architecture
A more production-oriented architecture could look like this:
Browser
|
v
Login Request
|
v
Authentication API
|
Validate credentials
|
+------------+------------+
| |
v v
Access Token Refresh Token
Short Lifetime Long Lifetime
| |
v v
API Calls HttpOnly Cookie
| |
v |
JWT Verify |
| |
+------------+------------+
|
v
API
|
Authorization
|
v
Resource
36. Refresh Endpoint
A simplified implementation might look like:
app.post("/auth/refresh", async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({
message: "Refresh token required"
});
}
try {
const payload = jwt.verify(
refreshToken,
process.env.REFRESH_TOKEN_SECRET!
) as jwt.JwtPayload;
const accessToken = jwt.sign(
{
sub: payload.sub,
role: payload.role
},
process.env.JWT_SECRET!,
{
expiresIn: "15m"
}
);
res.json({
accessToken
});
} catch {
res.status(401).json({
message: "Invalid refresh token"
});
}
});
A production implementation should go further.
For example:
Refresh Token
↓
Verify signature
↓
Check expiration
↓
Check session/token record
↓
Check revocation
↓
Rotate token
↓
Issue new access token
37. Logout with JWT
With traditional sessions:
DELETE session
and the server can immediately invalidate it.
JWT introduces an interesting problem.
If the access token is self-contained and valid for 15 minutes, how do you instantly revoke it?
There are several strategies.
Strategy 1: Short expiration
Use:
5–15 minute access token
and accept that it remains valid until expiration.
Strategy 2: Token Revocation
Store token identifiers:
jti → revoked
Then check them during authentication.
This introduces server-side state.
Strategy 3: Refresh Token Revocation
A common design is to invalidate the refresh session during logout.
Then:
Existing access token
↓
Expires soon
Refresh token
↓
Revoked
The user cannot obtain another access token.
38. JWT Blacklist
Another approach is a blacklist.
For example:
Redis
revoked:jti:12345
During authentication:
JWT
↓
Verify signature
↓
Read jti
↓
Check Redis
↓
Revoked?
├── Yes → Reject
└── No → Continue
This gives stronger revocation control but sacrifices some of JWT's stateless simplicity.
39. JWT Is Not Always the Best Choice
JWT is powerful, but it isn't automatically the correct authentication mechanism for every application.
For a traditional server-rendered application, a session-based approach can be simpler.
For example:
Browser
|
Session Cookie
|
Server
|
Session Store
For distributed APIs:
Client
|
JWT
|
API Gateway
|
Microservices
The correct choice depends on the architecture.
Don't choose JWT simply because:
"Everyone uses JWT."
Choose it because its properties fit the system.
40. Common JWT Mistakes
Here are some mistakes developers frequently make.
Mistake 1 — Putting passwords in JWT
Bad:
{
"email": "[email protected]",
"password": "123456"
}
Never do this.
Mistake 2 — Treating Base64 as encryption
Bad assumption:
Encoded = Encrypted
False.
JWT payloads are normally readable.
Mistake 3 — Using decode() for authentication
Bad:
const payload = jwt.decode(token);
Use verification instead:
jwt.verify(token, secret);
Mistake 4 — Very long-lived access tokens
Bad:
Access Token = 30 days
Prefer short-lived access tokens when appropriate.
Mistake 5 — Hardcoding secrets
Bad:
const secret = "123456";
Use secure secret management.
Mistake 6 — Storing everything in the token
Don't turn JWT into a database.
Bad:
{
"sub": "123",
"name": "...",
"address": "...",
"permissions": [ "...", "...", "..."],
"orders": [ "...", "...", "..." ]
}
JWTs should generally remain small.
Mistake 7 — Putting sensitive data in the payload
Remember:
JWT payload ≠ encrypted storage
Mistake 8 — Forgetting expiration
A token without appropriate expiration can become a long-lived credential.
Mistake 9 — Ignoring CSRF when using cookies
Cookie-based authentication requires appropriate CSRF defenses.
Mistake 10 — Assuming JWT solves logout
JWT does not automatically provide instant revocation.
41. Angular + JWT Example
Since JWT is frequently used with Angular, let's see the client-side concept.
An Angular HTTP interceptor can attach an access token:
export const authInterceptor: HttpInterceptorFn = (
req,
next
) => {
const token = authService.getAccessToken();
if (!token) {
return next(req);
}
const authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next(authReq);
};
The request becomes:
GET /api/profile HTTP/1.1
Authorization: Bearer eyJhbGciOi...
The backend then verifies the token.
42. Handling Expired Tokens in Angular
Suppose the API returns:
401 Unauthorized
The frontend can attempt a refresh flow.
Conceptually:
Angular
|
API Request
|
401
|
Refresh Token
|
/auth/refresh
|
New Access Token
|
Retry Original Request
RxJS is especially useful for implementing this flow safely.
But you must avoid creating infinite refresh loops.
For example:
API request
↓
401
↓
refresh
↓
401
↓
refresh
↓
401
↓
...
A production interceptor must carefully control refresh attempts.
43. JWT in BFF Architecture
JWT also appears in BFF architectures.
For example:
Browser
|
v
BFF
|
+---------+---------+
| | |
v v v
Users Orders Payments
The BFF can manage authentication while keeping backend credentials away from browser code.
A common architecture is:
Browser
|
Secure Cookie
|
BFF
|
Access Token
|
Backend APIs
This can simplify browser security and token management.
44. JWT in an API Gateway
Another architecture:
Client
|
| JWT
v
API Gateway
|
| verify
v
Microservices
The gateway may verify:
Signature
Issuer
Audience
Expiration
Then forward trusted identity information downstream.
However, individual services may still need to perform their own authorization checks.
Authentication at the gateway does not automatically mean:
Every service should trust every request from the gateway.
Trust boundaries must be designed explicitly.
45. A Better Production JWT Checklist
Before shipping JWT authentication, ask:
☑ Is the token signed with a strong algorithm?
☑ Are signing keys stored securely?
☑ Are tokens short-lived?
☑ Is refresh-token rotation implemented where appropriate?
☑ Are refresh tokens revocable?
☑ Are sensitive values excluded from the payload?
☑ Is JWT verification used instead of decode?
☑ Are issuer and audience validated where appropriate?
☑ Are accepted algorithms restricted?
☑ Is XSS protection implemented?
☑ If cookies are used, is CSRF addressed?
☑ Are cookies Secure?
☑ Are cookies HttpOnly when appropriate?
☑ Is SameSite configured appropriately?
☑ Are passwords hashed using a strong password hashing algorithm?
☑ Is authorization checked separately from authentication?
☑ Is logout behavior clearly defined?
☑ Is token revocation strategy documented?
☑ Are secrets/keys managed outside source control?
46. The Most Important Mental Model
Don't think of JWT as:
"A token that logs the user in."
Think of it as:
JWT
|
+-- Identity
|
+-- Claims
|
+-- Expiration
|
+-- Issuer/Audience
|
+-- Cryptographic Integrity
Then authentication becomes:
Request
|
v
Extract Token
|
v
Verify Signature
|
v
Validate Claims
|
+---- exp
+---- iss
+---- aud
+---- nbf
|
v
Identify User
|
v
Authorization
|
v
Allow / Reject
That mental model is much more useful than memorizing:
jwt.sign()
jwt.verify()
47. Complete Example
A simplified backend structure could look like:
src/
├── auth/
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ ├── auth.middleware.ts
│ └── auth.types.ts
│
├── users/
│ ├── user.model.ts
│ ├── user.controller.ts
│ └── user.service.ts
│
├── middleware/
│ └── error.middleware.ts
│
├── app.ts
└── server.ts
Authentication flow:
POST /auth/register
|
v
Hash Password
|
v
Create User
Then:
POST /auth/login
|
v
Validate Credentials
|
v
Create Access Token
|
v
Create Refresh Token
|
v
Return / Set Cookies
Then:
GET /api/profile
|
v
Authenticate Middleware
|
v
Verify JWT
|
v
Authorization
|
v
Controller
And finally:
POST /auth/refresh
|
v
Validate Refresh Token
|
v
Rotate Refresh Token
|
v
Issue New Access Token
48. Final Takeaway
JWT is not simply a replacement for sessions.
It is a token format with cryptographic signing capabilities that can be used as part of an authentication and authorization architecture.
The most important concepts to remember are:
JWT
│
├── Header
│
├── Payload
│
└── Signature
And:
Authentication
↓
Who are you?
Authorization
↓
What can you do?
For a production system:
Short-lived Access Token
+
Secure Refresh Token
+
Strong Key Management
+
HTTPS
+
XSS Protection
+
CSRF Protection when needed
+
Authorization
+
Revocation / Rotation Strategy
The most important lesson is this:
JWT itself is not the security architecture. JWT is one component inside the security architecture.
Understanding where tokens are stored, how they expire, how they are refreshed, how they are revoked, how keys are managed, and how authorization is enforced is far more important than simply knowing how to generate a token.
If you understand those concepts, you can confidently design JWT authentication for Angular + Node.js, React + Express, BFFs, API Gateways, and microservices architectures.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.