JWT Authentication: Access Tokens, Refresh Tokens, Cookies, and Authorization
This is my first-ever technical article, and I wanted to start with something I’ve been learning and implementing in my backend projects: JWT authentication. When we build a web application, we need a way for the server to know whether a request is coming from an authenticated user. There are different ways to solve this problem. Two common approaches are stateful authentication and stateless authentication. In this article, we’ll understand both approaches and then dive deeper into JWTs, access tokens, refresh tokens, cookies, and how the frontend and backend communicate during authentication.

1. Stateful vs Stateless Authentication
Before understanding JWT, we should understand the difference between stateful and stateless authentication.
Stateful Authentication
In stateful authentication, the server keeps information about the user's login session.
For example, after a successful login, the server can create a session and store it in a session store or database.
The client then receives a session ID, usually through a cookie.
Client
│
│ Login
▼
Server
│
│ Creates session
▼
Session Store
│
│ sessionId → user information
▼
Server sends session ID
│
▼
Client Cookie
For every subsequent request, the browser sends the session ID back to the server.
The server then looks up that session ID and determines which user is making the request.
For example:
Cookie:
sessionId=abc123
The server might have something like:
abc123 → User ID: 101
This is called stateful authentication because the server maintains the authentication state.
Frameworks such as Passport.js can be used with session-based authentication.
2. Stateless Authentication
JWT authentication is commonly used as a stateless authentication approach.
Instead of storing the complete session state on the server, the server issues a signed token to the client.
The client then sends that token with future requests.
The server verifies the token and can obtain the claims contained inside it.
Client
│
│ Login
▼
Server
│
│ Creates JWT
▼
Client
│
│ Sends JWT with future requests
▼
Server verifies JWT
The important idea is that the server does not necessarily need to look up a session for every request.
However, there is an important detail:
JWT does not mean that cookies cannot be used.
A JWT can be stored in a cookie, for example:
Cookie: accessToken=<JWT>
So cookies and statefulness are two different concepts.
A cookie is simply one way of transporting/storing information on the client. Whether an authentication system is stateful or stateless depends on where the authentication state is maintained.
3. What Exactly Is a JWT?
JWT stands for JSON Web Token.
A JWT is a compact, URL-safe string that can carry claims between two parties.
A typical JWT looks something like this:
xxxxx.yyyyy.zzzzz
It contains three parts:
Header.Payload.Signature
These three parts are separated by dots (.).
You can learn more about the JWT standard and inspect JWTs using the official JWT website:
4. Understanding the Three Parts of a JWT
A JWT consists of:
Header
Payload
Signature
Let's understand each one.
Header
The header contains information about the token.
For example:
{
"alg": "HS256",
"typ": "JWT"
}
Here:
algtells us which signing algorithm is being used.typtells us that this is a JWT.
The header is Base64URL-encoded as part of the JWT.
Payload
The payload contains claims.
For example:
{
"_id": "12345",
"email": "user@example.com",
"role": "user"
}
These claims can be used by the server to identify the user and understand information associated with the token.
However, there is a very important security point:
The JWT payload is not secret by default.
A normal JWT is encoded and signed, not encrypted.
Anyone who has the token can generally decode the header and payload.
Therefore, you should never put passwords, secret keys, or other sensitive information inside the JWT payload.
5. What Is the Signature?
The signature is what allows the server to verify that the token was signed by a trusted party and that its signed contents haven't been modified.
For an HMAC-based JWT such as HS256, conceptually it works like:
Signature =
HMAC(
encodedHeader + "." + encodedPayload,
secretKey
)
The server keeps the secret key securely on the backend.
For example:
JWT_SECRET=very-long-random-secret
The client should never receive this secret.
When the client sends the JWT back to the server, the server uses the appropriate verification key to verify the signature.
If the signature is valid, the server can trust that the token was signed with the expected key and that the signed contents have not been modified.
6. JWT Is Not Encryption
This is one of the most important things to understand when learning JWT.
A common misconception is:
"JWT is encrypted, so nobody can read it."
That's incorrect for a normal signed JWT.
A normal JWT is:
Encoded + Signed
not:
Encrypted
For example, someone who gets your JWT can decode its payload and potentially see:
{
"_id": "12345",
"email": "user@example.com"
}
But they cannot simply modify:
{
"role": "user"
}
to:
{
"role": "admin"
}
and expect the server to accept it.
Why?
Because changing the payload would cause the signature verification to fail.
7. The Client-Server Authentication Deal
You can think of authentication as an agreement between the client and server.
The client says:
"Here is my token."
The server checks:
"Was this token signed correctly?"
"Is it expired?"
"Are the claims acceptable?"
If everything checks out, the server treats the request as authenticated according to the application's rules.
If the token is invalid or expired, the server rejects the request or requires the client to obtain a new access token.
For example:
GET /api/v1/profile
Authorization: Bearer <access-token>
The server then verifies the token before allowing access to the protected route.
8. Why Do We Need Access Tokens and Refresh Tokens?
Now we come to one of the most useful concepts in modern token-based authentication:
Access tokens and refresh tokens.
Imagine that an access token never expires.
If an attacker gets hold of that token, they could potentially use it for a very long time.
So instead, applications commonly make access tokens short-lived.
For example:
Access Token → 15 minutes
Refresh Token → 7 days
These are only example values. The actual lifetime depends on the application's security requirements.
The main idea is:
Access Token → Short-lived → Used frequently
Refresh Token → Longer-lived → Used to obtain new access tokens
9. What Is an Access Token?
An access token is the token that the client uses to access protected resources.
For example:
GET /api/v1/user
Authorization: Bearer <access-token>
The backend verifies the access token.
If it is valid, the request can continue.
If it is expired, the client can use its refresh token to request a new access token.
10. What Is a Refresh Token?
A refresh token exists so that the user doesn't have to enter their username and password every time their short-lived access token expires.
For example:
User logs in
↓
Access Token + Refresh Token
↓
Access Token expires
↓
Client sends Refresh Token
↓
Server verifies Refresh Token
↓
New Access Token
↓
User continues using the application
This provides a better balance between security and user experience.
The access token can have a relatively short lifetime, while the refresh token allows the user to remain logged in for longer.
11. What Happens During Login?
Let's imagine a user enters:
{
"email": "user@example.com",
"password": "mypassword"
}
The backend first verifies the credentials.
If they are correct, the server generates tokens:
Access Token
Refresh Token
For example:
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
The server then sends the tokens to the client using the application's chosen storage/transport mechanism.
One common approach is to use HttpOnly cookies.
12. What Happens When the Access Token Expires?
Suppose the access token has a lifetime of one hour.
After one hour:
Access Token → Expired
The user doesn't necessarily need to log in again.
The client can send the refresh token to the refresh endpoint.
For example:
POST /api/v1/refresh-token
The server receives the refresh token and verifies it.
A server-side implementation may also store a refresh token or a related session/token record in the database.
Conceptually:
Client Refresh Token
│
▼
Server
│
├── Is token valid?
├── Is token expired?
├── Is token revoked?
└── Does it match the server-side record?
│
▼
Generate new Access Token
If everything is valid:
New Access Token → Client
The user can continue using the application without entering their password again.
13. What If the Refresh Token Is Invalid?
If the refresh token is:
expired
invalid
revoked
missing
or doesn't match the server-side record in a system that stores refresh tokens
then the server should not issue a new access token.
The user may need to log in again.
Refresh Token
↓
Server Verification
↓
Invalid
↓
No new Access Token
↓
User logs in again
14. Why Store the Refresh Token in the Database?
You might wonder:
"If JWT is stateless, why are we storing the refresh token in the database?"
This is an important distinction.
You can design a JWT system without storing refresh tokens server-side.
However, storing refresh tokens or a server-side representation of refresh-token sessions gives the backend more control.
For example, the server can revoke a refresh token.
Imagine a user logs out.
The server can invalidate the stored refresh-token record.
Then even if someone later tries to use that refresh token, the server can reject it.
This gives the application a way to manage long-lived authentication sessions.
15. Access Token vs Refresh Token
| Access Token | Refresh Token |
|---|---|
| Short-lived | Longer-lived |
| Used for API requests | Used to obtain new access tokens |
| Sent frequently | Sent less frequently |
| Should have limited lifetime | Usually has a longer lifetime |
| Gives access to protected resources | Used to continue the authenticated session |
The exact expiration times are not fixed.
For example, one application might use:
Access Token → 15 minutes
Refresh Token → 7 days
Another application might choose different lifetimes based on its security requirements.
16. Where Do Cookies Come Into This?
Cookies are often used to transport authentication tokens between the browser and server.
For example:
Set-Cookie: accessToken=<JWT>; HttpOnly; Secure
With an HttpOnly cookie, JavaScript running in the browser cannot directly read the cookie.
The browser can automatically send the cookie with requests to the appropriate domain.
This can be useful for protecting tokens from certain types of client-side token theft.
Cookies can therefore be used with both:
Session-based authentication
and:
JWT-based authentication
So remember:
Cookie ≠ Stateful authentication
and:
JWT ≠ Must be stored in localStorage
They are separate design decisions.
17. The Complete JWT Authentication Flow
Let's put everything together.
Step 1 — User Logs In
Client
│
│ Email + Password
▼
Backend
The backend verifies the credentials.
Step 2 — Server Generates Tokens
Backend
│
├── Access Token
└── Refresh Token
Step 3 — Client Receives Tokens
For example, tokens may be delivered using secure cookies.
Browser
│
├── accessToken
└── refreshToken
Step 4 — Client Requests Protected Resource
GET /api/v1/profile
The access token accompanies the request.
Step 5 — Backend Verifies Access Token
Access Token
↓
Verify signature
↓
Check expiration
↓
Read claims
↓
Authenticated request
Step 6 — Access Token Expires
Access Token → Expired
Step 7 — Client Uses Refresh Token
POST /api/v1/refresh-token
Step 8 — Backend Verifies Refresh Token
If the refresh token is valid, the backend generates a new access token.
Refresh Token
↓
Verified
↓
New Access Token
Step 9 — Refresh Token Eventually Expires or Is Revoked
When the refresh token is no longer valid:
Refresh Token → Invalid
↓
No new Access Token
↓
User must log in again
18. Final Takeaway
The main idea behind JWT authentication is not simply:
"JWT is a token."
It is about understanding how the different pieces work together.
LOGIN
│
▼
Verify Credentials
│
▼
┌─────────────────────┐
│ │
▼ ▼
Access Token Refresh Token
Short-lived Longer-lived
│ │
▼ │
API Requests │
│ │
▼ │
Token Expires ───────────────┘
│
▼
Request New Token
│
▼
New Access Token
JWTs are signed tokens, not automatically encrypted tokens. The header contains metadata such as the signing algorithm, the payload contains claims, and the signature allows the server to verify the token's integrity and authenticity.
Access tokens are generally short-lived and used to access protected resources, while refresh tokens can be used to obtain new access tokens without requiring the user to log in again.
And finally, cookies, JWTs, sessions, access tokens, and refresh tokens are different concepts that can be combined in different ways. Understanding that distinction makes authentication much easier to reason about when building real-world applications.
