How to Implement Secure JWT Double‑Token (Access & Refresh) Authentication in PHP

This guide explains the JWT standard, its three‑part structure, the authentication flow, and a double‑token solution using short‑lived access tokens and long‑lived refresh tokens, with full PHP code examples, middleware interception, configuration, and client‑side renewal logic.

Open Source Tech Hub
Open Source Tech Hub
Open Source Tech Hub
How to Implement Secure JWT Double‑Token (Access & Refresh) Authentication in PHP

JWT Overview

Concept

JSON Web Token (JWT) is an open standard (RFC 7519) that provides a compact, self‑contained way to transmit JSON‑encoded claims securely between parties. The token is digitally signed using either a secret (HMAC) or an asymmetric key pair (RSA/ECDSA), enabling verification and trust.

Structure

A JWT consists of three Base64‑URL‑encoded parts separated by dots ( .): Header, Payload, and Signature. Combined they form Header.Payload.Signature.

Authentication Flow

Browser sends login request with username and password.

Server validates credentials and issues a signed token.

Server returns the JWT to the browser; the token contains no sensitive data.

Browser includes the token in the Authorization header for subsequent API calls.

Server extracts the token, verifies its signature and expiration.

If valid, the server processes the request; otherwise it rejects it.

Double‑Token Solution

In a front‑end/back‑end separated architecture, the server issues an access token (short‑lived) and a refresh token (long‑lived). The access token is stored in LocalStorage and sent in the request header. When the access token expires (e.g., after 2 hours), the client can use the refresh token (e.g., valid for 7 days) to obtain a new access token without requiring the user to log in again.

Installation

composer require tinywan/jwt
Plugin URL: https://www.workerman.net/plugin/10

Configuration

return [
    'enable' => true,
    'jwt' => [
        // Algorithm (HS256, RS256, etc.)
        'algorithms' => 'HS256',
        // Secret keys
        'access_secret_key' => '2024d3d3LmJq',
        'refresh_secret_key' => '2022KTxigxc9o50c',
        // Expiration times (seconds)
        'access_exp' => 7200,   // 2 hours
        'refresh_exp' => 604800, // 7 days
        'refresh_disable' => false,
        'iss' => 'webman.tinywan.cn',
        // ... other options
    ],
];

Set access_exp to 7200 seconds (2 hours).

Set refresh_exp to 604800 seconds (7 days).

Generating Tokens

$user = [
    'id'    => 2024,
    'name'  => 'Tinywan',
    'email' => '[email protected]',
];
$token = Tinywan\Jwt\JwtToken::generateToken($user);
var_dump(json_encode($token));

Typical JSON response:

{
    "token_type": "Bearer",
    "expires_in": 36000,
    "access_token": "eyJ0eXAiOi...",
    "refresh_token": "eyJ0eXAiOi..."
}

Middleware Interceptor

declare(strict_types=1);
namespace app\middleware;
use Tinywan\ExceptionHandler\Exception\ForbiddenHttpException;
use Tinywan\ExceptionHandler\Exception\UnauthorizedHttpException;
use Tinywan\Jwt\JwtToken;
use Webman\Http\Request;
use Webman\Http\Response;
use Webman\MiddlewareInterface;

class AuthorizationMiddleware implements MiddlewareInterface
{
    public function process(Request $request, callable $handler): Response
    {
        $request->userId = JwtToken::getCurrentId();
        if (0 === $request->userId) {
            throw new UnauthorizedHttpException();
        }
        return $handler($request);
    }
}

Token Validation Responses

Invalid token (HTTP 401)
HTTP/1.1 401 Unauthorized
Content-Type: application/json;charset=UTF-8

{"code":0,"msg":"Token session has expired, please log in again!","data":{}}
Valid token (HTTP 200)
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8

{"code":0,"msg":"success","data":{"id":202801,"username":"Tinywan"}}

Refreshing the Token

public function refreshToken(): Response
{
    $res = \Tinywan\Jwt\JwtToken::refreshToken();
    return response_json(0, 'success', $res);
}

Example CURL request:

curl --request GET \
  --url http://127.0.0.1:8888/oauth/refresh-token \
  --header 'Accept: */*' \
  --header 'Authorization: Bearer ACCESS_OR_REFRESH_TOKEN' \
  --header 'Connection: keep-alive'

If the refresh token is still valid, the server returns a new access_token. If the refresh token has expired, the server responds with HTTP 402 (configurable) indicating the need to re‑authenticate.

Front‑End Pseudo‑Code

async function refreshToken() {
  const res = await axios.get('http://127.0.0.1:8888/oauth/refresh-token', {
    params: { refresh_token: localStorage.getItem('refresh_token') },
  });
  localStorage.setItem('access_token', res.data.access_token || '');
  localStorage.setItem('refresh_token', res.data.refresh_token || '');
  return res;
}

axios.interceptors.response.use(
  response => response,
  async err => {
    const { data, config } = err.response;
    if (data.statusCode === 401 && config.url.includes('/oauth/refresh-token')) {
      const res = await refreshToken();
      if (res.status === 200) {
        return axios(config);
      } else {
        alert('Login expired, please log in again');
        return Promise.reject(res.data);
      }
    }
    return err.response;
  }
);
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AuthenticationPHPJWTBackend Securityaccess_tokenRefresh token
Open Source Tech Hub
Written by

Open Source Tech Hub

Sharing cutting-edge internet technologies and practical AI resources.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.