DEV Community

Cover image for How Does JWT Authentication Work? A Complete Guide!
Tanu Priya
Tanu Priya

Posted on

How Does JWT Authentication Work? A Complete Guide!

You enter your email and password, click Login, and suddenly the application knows who you are.

A few seconds later, you open your profile and the backend already knows which user's data to return.

But HTTP doesn't work like that by itself.

Every request is independent. So after the login request finishes, how does the server recognize the next request?

One popular solution is JWT authentication.

JWT stands for JSON Web Token. It gives the client a signed piece of information that can be sent with later requests so the server can verify the request's identity.

A simplified flow looks like this:

Login
  ↓
Verify credentials
  ↓
Create JWT
  ↓
Send JWT to client
  ↓
Client sends JWT with API requests
  ↓
Server verifies JWT
  ↓
Request continues
Enter fullscreen mode Exit fullscreen mode

The interesting part is what happens inside that flow.

Why Does Authentication Need a Token?

Imagine you log into an application:

POST /api/login
Enter fullscreen mode Exit fullscreen mode

with:

{
  "email": "alex@example.com",
  "password": "mypassword"
}
Enter fullscreen mode Exit fullscreen mode

The backend verifies the credentials.

At this point, it knows:

This is user 42.
Enter fullscreen mode Exit fullscreen mode

But the next request could be:

GET /api/profile
Enter fullscreen mode Exit fullscreen mode

There is nothing inherent in HTTP that tells the server:

"This is the same person who successfully logged in two seconds ago."

The application needs a way to carry that authentication information from one request to another.

That's where the token comes in.

After successful authentication, the server issues a JWT:

Login
  ↓
JWT issued
  ↓
Client keeps JWT
  ↓
JWT sent with future requests
Enter fullscreen mode Exit fullscreen mode

The token becomes part of the application's authentication mechanism.

What Is Inside a JWT?

A JWT usually looks like this:

xxxxx.yyyyy.zzzzz
Enter fullscreen mode Exit fullscreen mode

There are three sections:

Header.Payload.Signature
Enter fullscreen mode Exit fullscreen mode

For example:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiI0MiIsInJvbGUiOiJ1c2VyIn0
.
signature
Enter fullscreen mode Exit fullscreen mode

These three parts have different jobs.

Header

The header describes the token.

A simplified example:

{
  "alg": "HS256",
  "typ": "JWT"
}
Enter fullscreen mode Exit fullscreen mode

alg specifies the signing algorithm and typ identifies the token type.

Payload

The payload contains claims.

For example:

{
  "sub": "42",
  "role": "user",
  "iat": 1760000000,
  "exp": 1760003600
}
Enter fullscreen mode Exit fullscreen mode

Here:

sub → subject / user identifier
role → application-specific role
iat → issued-at time
exp → expiration time
Enter fullscreen mode Exit fullscreen mode

These claims allow the server to carry useful information about the token and its subject.

There is an important security detail here.

A normal JWT payload is not encrypted.

It is encoded so that the token can be represented as a compact string.

Anyone who obtains the token can generally decode its header and payload.

So don't put things like passwords, private keys, or other secrets inside the payload.

Signature

The signature is what protects the integrity of the signed token data.

Conceptually:

Header + Payload
       ↓
Signing algorithm
       +
Secret/private key
       ↓
Signature
Enter fullscreen mode Exit fullscreen mode

The server can later verify the signature.

If someone changes:

{
  "role": "user"
}
Enter fullscreen mode Exit fullscreen mode

to:

{
  "role": "admin"
}
Enter fullscreen mode Exit fullscreen mode

without having the appropriate signing key, the signature will no longer match.

The server can reject the token.

So the signature doesn't hide the payload.

It helps answer:

Has the signed token data been changed, and does it have a valid signature?

What Happens When You Log In?

Let's follow the request from beginning to end.

The client sends:

POST /api/login
Content-Type: application/json
Enter fullscreen mode Exit fullscreen mode

with:

{
  "email": "alex@example.com",
  "password": "mypassword"
}
Enter fullscreen mode Exit fullscreen mode

The backend might do something like:

Receive credentials
       ↓
Find user
       ↓
Verify password
       ↓
Credentials valid?
       ↓
Create JWT
       ↓
Sign JWT
       ↓
Return token
Enter fullscreen mode Exit fullscreen mode

The token might contain:

{
  "sub": "42",
  "exp": 1760003600
}
Enter fullscreen mode Exit fullscreen mode

The server signs it using the configured key.

The response could then contain:

{
  "accessToken": "eyJ..."
}
Enter fullscreen mode Exit fullscreen mode

The client can now use that access token when calling protected APIs.

The Next API Request

Now the frontend wants Alex's profile.

It sends:

GET /api/profile
Authorization: Bearer eyJ...
Enter fullscreen mode Exit fullscreen mode

The backend receives the request and extracts the token.

It then verifies it.

Conceptually:

Request
  ↓
Extract JWT
  ↓
Verify signature
  ↓
Check expiration
  ↓
Validate required claims
  ↓
Identify user
  ↓
Continue request
Enter fullscreen mode Exit fullscreen mode

If the token represents user 42, the application can use that identity when processing the request.

For example:

JWT
 ↓
sub = 42
 ↓
User 42
 ↓
Fetch profile
 ↓
Return profile
Enter fullscreen mode Exit fullscreen mode

Notice something important here.

The user doesn't send their password with every request.

The JWT is the credential being presented for that request.

JWT Authentication in Express

In a Node.js application, JWT verification is often placed in middleware.

A simplified example:

function authenticate(req, res, next) {
    const header = req.headers.authorization;

    if (!header || !header.startsWith("Bearer ")) {
        return res.status(401).json({
            message: "Authentication required"
        });
    }

    const token = header.slice(7);

    try {
        const payload = jwt.verify(
            token,
            process.env.JWT_SECRET
        );

        req.user = payload;
        next();
    } catch {
        return res.status(401).json({
            message: "Invalid or expired token"
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

A protected route can then use the middleware:

app.get(
    "/api/profile",
    authenticate,
    getProfile
);
Enter fullscreen mode Exit fullscreen mode

The request now has a predictable path:

GET /api/profile
       ↓
Authentication middleware
       ↓
Verify JWT
       ↓
Attach user to request
       ↓
Controller
       ↓
Database
       ↓
Response
Enter fullscreen mode Exit fullscreen mode

The controller doesn't need to repeatedly implement JWT verification.

That responsibility stays in the authentication layer.

Authentication Is Still Different From Authorization

This is where many beginners mix things up.

Suppose the JWT tells the application that the current user is:

User ID: 42
Role: user
Enter fullscreen mode Exit fullscreen mode

That establishes identity.

But now the user requests:

DELETE /api/users/100
Enter fullscreen mode Exit fullscreen mode

The backend still has another question:

Is user 42 allowed to delete another user?

That's authorization.

So the complete process is:

JWT verification
      ↓
Who is this?
      ↓
User 42
      ↓
Authorization check
      ↓
Can User 42 perform this action?
      ↓
Allow / Reject
Enter fullscreen mode Exit fullscreen mode

For example:

function requireAdmin(req, res, next) {
    if (req.user.role !== "admin") {
        return res.status(403).json({
            message: "Forbidden"
        });
    }

    next();
}
Enter fullscreen mode Exit fullscreen mode

And:

app.delete(
    "/api/users/:id",
    authenticate,
    requireAdmin,
    deleteUser
);
Enter fullscreen mode Exit fullscreen mode

JWT helps with authentication.

It does not automatically give the user permission to do everything.

What Does Stateless JWT Authentication Mean?

You've probably heard that JWT authentication is stateless.

The idea is that the server doesn't necessarily need to maintain a traditional server-side session for every access token.

With a session-based approach:

Client
  ↓
Session ID
  ↓
Server
  ↓
Session Store
  ↓
User
Enter fullscreen mode Exit fullscreen mode

With a self-contained JWT:

Client
  ↓
JWT
  ↓
Server verifies token
  ↓
Claims identify subject
Enter fullscreen mode Exit fullscreen mode

This can be convenient in systems with multiple backend instances.

Imagine:

                 Load Balancer
                      ↓
          ┌───────────┼───────────┐
          ↓           ↓           ↓
       Server A    Server B    Server C
          ↑           ↑           ↑
          └──── Verify JWT ───────┘
Enter fullscreen mode Exit fullscreen mode

Each server can verify the token using the appropriate key.

There is no requirement for every request to first find a traditional session record.

But there's a catch.

Stateless doesn't mean there is no state anywhere in the authentication system.

Applications may still maintain state for refresh tokens, revocation, device sessions, security events, or other security controls.

Access Tokens and Refresh Tokens

Access tokens are often short-lived.

For example:

Access Token
↓
Expires in 15 minutes
Enter fullscreen mode Exit fullscreen mode

If the access token expires, the user shouldn't necessarily have to log in again.

A common design is to use a refresh token.

Login
  ↓
Access Token + Refresh Token
  ↓
Access Token used for APIs
  ↓
Access Token expires
  ↓
Refresh Token
  ↓
New Access Token
Enter fullscreen mode Exit fullscreen mode

This gives the application a way to keep a user signed in while limiting the lifetime of an individual access token.

Refresh tokens need careful handling because a stolen refresh token can have a much longer useful lifetime.

That's why real authentication systems often add things like refresh-token rotation and revocation.

What Happens When the JWT Expires?

JWTs commonly contain an expiration claim:

{
  "sub": "42",
  "exp": 1760003600
}
Enter fullscreen mode Exit fullscreen mode

Once that time has passed, the token should no longer be accepted as a valid access token.

The API might respond with:

401 Unauthorized
Enter fullscreen mode Exit fullscreen mode

The client can then use its refresh mechanism if one exists:

API request
    ↓
Access token expired
    ↓
401
    ↓
Refresh authentication
    ↓
New access token
    ↓
Retry request
Enter fullscreen mode Exit fullscreen mode

This is one reason short-lived access tokens are useful.

If an access token is stolen, its useful lifetime can be limited.

It doesn't make theft harmless, but it can reduce the window of exposure.

Where Should You Store a JWT?

There isn't one universal answer.

JWTs are commonly transported using:

Authorization header
Enter fullscreen mode Exit fullscreen mode

or:

Cookie
Enter fullscreen mode Exit fullscreen mode

For browser applications, cookies can provide useful security controls.

For example:

Set-Cookie: access_token=...;
HttpOnly;
Secure;
SameSite=Lax
Enter fullscreen mode Exit fullscreen mode

HttpOnly prevents normal page JavaScript from directly reading the cookie.

Secure tells the browser to send it only over HTTPS.

SameSite controls when the browser sends the cookie in cross-site situations.

Cookies aren't automatically safer in every situation, though. They introduce considerations such as CSRF protection and cross-origin configuration.

The right approach depends on the application's architecture and threat model.

What If Someone Steals the JWT?

A valid access token should be treated like a credential.

If an attacker gets one, they may be able to make authenticated API requests until the token expires or is otherwise invalidated.

For example:

Attacker
   ↓
Stolen JWT
   ↓
Authorization: Bearer <token>
   ↓
API
   ↓
Authenticated request
Enter fullscreen mode Exit fullscreen mode

That's why protecting the token matters.

Common security practices include:

HTTPS
Short-lived access tokens
Secure refresh-token handling
Signature verification
Expiration checks
Issuer/audience validation when applicable
Restricted signing algorithms
Avoiding sensitive information in payloads
Enter fullscreen mode Exit fullscreen mode

JWT doesn't provide all of these protections automatically.

The application has to implement the authentication system correctly.

JWT Is Not Encryption

This is probably the most common JWT misconception.

People see:

Header.Payload.Signature
Enter fullscreen mode Exit fullscreen mode

and assume the contents are hidden.

They're not.

A normal signed JWT provides integrity, not confidentiality.

Think of it this way:

Encoding
→ Makes data representable as a token

Signing
→ Helps detect modification

Encryption
→ Hides the data
Enter fullscreen mode Exit fullscreen mode

JWT signing and encryption solve different problems.

If the application needs sensitive information to remain confidential, it needs an appropriate encryption mechanism rather than simply putting that information into a signed JWT.

401 vs 403

JWT authentication also makes the difference between 401 and 403 easier to understand.

A 401 Unauthorized response generally means the request doesn't have valid authentication credentials.

For example:

No token
Invalid token
Expired token
Enter fullscreen mode Exit fullscreen mode

In simple terms:

401
→ "I can't authenticate you."
Enter fullscreen mode Exit fullscreen mode

A 403 Forbidden response means the server knows who the user is, but the user isn't allowed to perform the requested action.

Authenticated user
        ↓
Not enough permissions
        ↓
403
Enter fullscreen mode Exit fullscreen mode

So:

401 → Authentication problem
403 → Authorization problem
Enter fullscreen mode Exit fullscreen mode

JWT Doesn't Replace Good API Security

It can be tempting to think:

"We're using JWT, so our API is secure."

That's not how it works.

JWT only solves part of the problem.

A production authentication system still needs to think about:

Password storage
HTTPS
Token lifetime
Refresh tokens
Token theft
Revocation
Key management
Authorization
CSRF
XSS
Rate limiting
Account recovery
Brute-force protection
Enter fullscreen mode Exit fullscreen mode

The exact list depends on the application.

JWT is a tool, not the entire security architecture.

JWT vs Sessions

JWT and sessions are often compared as if one has to replace the other.

In reality, both are valid approaches.

A session-based system might look like:

Login
  ↓
Create session
  ↓
Session ID in cookie
  ↓
Server looks up session
Enter fullscreen mode Exit fullscreen mode

A JWT-based system might look like:

Login
  ↓
Create signed JWT
  ↓
Client receives token
  ↓
Client sends token
  ↓
Server verifies token
Enter fullscreen mode Exit fullscreen mode

Sessions make some server-side operations, such as immediate session invalidation, straightforward.

JWTs can be useful when authentication information needs to be verified across multiple services without relying on a shared session store.

But JWT systems can become complicated once you add refresh tokens, revocation, key rotation, and multiple token types.

So don't choose JWT simply because it is popular.

Start with the requirements of the application.

The Complete JWT Flow

Putting everything together:

                    LOGIN
                      ↓
             Email + Password
                      ↓
                  Backend
                      ↓
             Verify credentials
                      ↓
                 Create JWT
                      ↓
                 Sign JWT
                      ↓
              Send to client
                      ↓
             Future API request
                      ↓
              Send JWT with request
                      ↓
               Verify JWT
                      ↓
             Validate its claims
                      ↓
               Identify user
                      ↓
             Check permissions
                      ↓
               Business logic
                      ↓
                  Database
                      ↓
                  Response
Enter fullscreen mode Exit fullscreen mode

There are really two separate decisions happening:

Authentication
      ↓
"Who is making this request?"

Authorization
      ↓
"What is this user allowed to do?"
Enter fullscreen mode Exit fullscreen mode

A Practical Mental Model

When you encounter JWT authentication in a project, think about it in this order:

1. User logs in
       ↓
2. Backend verifies credentials
       ↓
3. Backend issues a signed JWT
       ↓
4. Client sends the JWT with API requests
       ↓
5. Backend verifies the token
       ↓
6. Backend identifies the user
       ↓
7. Authorization checks permissions
       ↓
8. Request is allowed or rejected
Enter fullscreen mode Exit fullscreen mode

And remember what each part actually means:

JWT
├── Header
│   └── Token metadata
│
├── Payload
│   └── Claims
│
└── Signature
    └── Integrity/authenticity check
Enter fullscreen mode Exit fullscreen mode

The easiest mistake to make is thinking that JWT is simply a secure replacement for a password.

It isn't.

A JWT is a signed token carrying claims. The server uses it to authenticate requests without requiring the user's password on every request.

Once you understand that, the rest of the system starts making sense.

Login creates the authentication context.
The JWT carries that context between requests.
The server verifies the JWT.
Authorization decides what the authenticated user can actually do.

That's the core of JWT authentication.

Top comments (0)