← Back to all articles

JWT Authentication Explained: Access Token vs Refresh Token

Published on August 24, 2026  |  12 views
#JWT #JWT Authentication #PHP JWT #Access Token #Refresh Token #REST API #API Authentication #PHP #Redis #Backend Development #Web Security #API Security #JSON Web Token

             

JWT Authentication Explained: Access Token vs Refresh Token

JWT authentication is one of the most common ways to secure modern APIs.

If you have worked with REST APIs, microservices, mobile applications or single-page applications, you have probably seen something like:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

But what exactly is this token?

Why do we need both an Access Token and a Refresh Token?

And what happens when the access token expires?

Let's understand the complete flow in a simple way.

What Is JWT?

JWT stands for JSON Web Token.

A JWT is a compact, signed token that can contain information about a user or session.

A JWT normally contains three parts:

HEADER.PAYLOAD.SIGNATURE

For example:

eyJhbGciOiJIUzI1NiJ9
.
eyJ1c2VyX2lkIjoxMjMsImV4cCI6MTc1NjAwMDAwMH0
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

1. Header

The header normally contains the signing algorithm and token type.

{
  "alg": "HS256",
  "typ": "JWT"
}

2. Payload

The payload contains claims about the user or token.

{
  "user_id": 123,
  "email": "user@example.com",
  "iat": 1756000000,
  "exp": 1756000900
}

3. Signature

The signature is used to verify that the token has not been modified.

The important point is that JWT payloads are encoded, not encrypted. Do not put passwords or other secrets inside the payload.

Why Do We Need Access and Refresh Tokens?

A common mistake is creating one JWT with a very long expiration time.

For example:

JWT expires in 30 days

If that token is stolen, an attacker may be able to use it for a long time.

A better approach is to use two tokens:

  • Access Token — short-lived and used to access APIs.
  • Refresh Token — longer-lived and used to obtain a new access token.

Access Token

The access token is sent with API requests.

GET /api/profile

Authorization: Bearer <access_token>

Keep the access token relatively short-lived.

For example:

Access Token
Lifetime: 15 minutes

If the access token is stolen, the attacker's window of opportunity is reduced.

Refresh Token

When the access token expires, the user doesn't necessarily need to log in again.

The client can send the refresh token to the authentication server.

POST /api/auth/refresh

{
  "refresh_token": "..."
}

The server validates the refresh token and issues a new access token.

New Access Token
        ↓
Use APIs again
        ↓
Eventually expires
        ↓
Use Refresh Token
        ↓
Get another Access Token

Complete Authentication Flow

Client
   |
   | 1. Login
   v
Authentication Server
   |
   | 2. Access Token + Refresh Token
   v
Client
   |
   | 3. API Request + Access Token
   v
API Server
   |
   | 4. Access Token expires
   v
Client
   |
   | 5. Refresh Token
   v
Authentication Server
   |
   | 6. New Access Token
   v
Client

PHP Example: Creating a JWT

One popular PHP library for JWT handling is firebase/php-jwt.

After installing the package:

composer require firebase/php-jwt

A simple JWT creation example:

<?php

use Firebase\JWT\JWT;

$secretKey = 'your-secret-key';

$issuedAt = time();
$expiresAt = $issuedAt + (15 * 60);

$payload = [
    'iss' => 'santoshc.in',
    'iat' => $issuedAt,
    'exp' => $expiresAt,
    'user_id' => 123,
    'email' => 'user@example.com'
];

$jwt = JWT::encode(
    $payload,
    $secretKey,
    'HS256'
);

echo $jwt;

The important part here is the expiration time.

$expiresAt = $issuedAt + (15 * 60);

This makes the access token valid for approximately 15 minutes.

PHP Example: Validating a JWT

When the client calls a protected API, the server can read the Authorization header.

<?php

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$secretKey = 'your-secret-key';

$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';

$jwt = str_replace('Bearer ', '', $header);

try {

    $decoded = JWT::decode(
        $jwt,
        new Key($secretKey, 'HS256')
    );

    echo json_encode([
        'success' => true,
        'user' => $decoded->user_id
    ]);

} catch (Exception $e) {

    http_response_code(401);

    echo json_encode([
        'success' => false,
        'message' => 'Invalid or expired token'
    ]);
}

What Happens When the Access Token Expires?

Suppose the access token expires after 15 minutes.

The API can return:

HTTP/1.1 401 Unauthorized

The client can then call the refresh endpoint:

POST /api/auth/refresh

The server validates the refresh token and returns a new access token.

The user can continue using the application without logging in again.

Where Should Refresh Tokens Be Stored?

This is an important security consideration.

For browser applications, a common secure approach is to store refresh tokens in an HttpOnly, Secure cookie.

For example:

Set-Cookie:
refresh_token=abc123;
HttpOnly;
Secure;
SameSite=Strict

HttpOnly prevents normal JavaScript code from directly reading the cookie.

Secure ensures the cookie is sent only over HTTPS.

Refresh Token Rotation

For stronger security, refresh tokens can be rotated.

Instead of repeatedly using the same refresh token:

Refresh Token A
      ↓
New Access Token
      ↓
Refresh Token A

the server can issue a new refresh token each time:

Refresh Token A
      ↓
New Access Token
      +
Refresh Token B
      ↓
Invalidate Refresh Token A

This makes stolen refresh tokens harder to reuse.

Where Does Redis Fit?

JWT access tokens are often designed to be stateless, which means the API doesn't need to query the database for every request.

However, refresh tokens are different.

You may want the ability to revoke them immediately.

Redis can be useful for storing refresh-token state.

Refresh Token
      |
      v
Redis
      |
      +-- User ID
      +-- Token ID
      +-- Expiration
      +-- Revoked?
      |
      v
Authentication Server

This gives you much better control over logout, token revocation and refresh-token rotation.

JWT Security Best Practices

  • Use HTTPS everywhere.
  • Keep access tokens short-lived.
  • Use strong signing keys.
  • Never store passwords inside JWT payloads.
  • Validate token expiration.
  • Validate the signing algorithm.
  • Protect refresh tokens carefully.
  • Rotate refresh tokens when appropriate.
  • Support logout and token revocation.
  • Keep secrets outside source code.

Simple Mental Model

LOGIN
  |
  v
Access Token + Refresh Token
  |
  +----------------------+
  |                      |
  v                      v
Access APIs          Wait...
  |                      |
  v                      v
Token Expires       Refresh Token
                         |
                         v
                   New Access Token
                         |
                         v
                    Access APIs

Final Thoughts

JWT authentication becomes much easier to understand when you separate the responsibilities of the two tokens.

Access Token:

Short-lived
Used for API requests
Limits the damage if stolen

Refresh Token:

Longer-lived
Used to obtain new access tokens
Should be stored and protected carefully

A practical architecture for many modern applications is:

Login
  ↓
Access Token + Refresh Token
  ↓
API Requests
  ↓
Access Token Expires
  ↓
Refresh Token
  ↓
New Access Token
  ↓
Continue Using API

JWT is not automatically secure just because it is a JWT. The security comes from how you generate, validate, store, expire, rotate and revoke your tokens.

For production applications, those details matter much more than simply generating a token.


Author: Santosh Chakraborty

Topics: JWT, PHP, REST API, Authentication, Access Token, Refresh Token, Redis, Backend Development