Dev.to Security 🔐 Cybersecurity 👁 0 📖 16 min read

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

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

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.