🔐 Build a Secure Token-Based Authentication API in PHP (JWT + Refresh Tokens)

build-secure-jwt-authentication-api-in-php

Introduction

In today’s web apps, stateless authentication is the gold standard. And for backend developers, mastering JWT-based authentication with refresh tokens is crucial — especially if you're building RESTful APIs that need to scale securely.

In this in-depth tutorial, we’ll build a secure PHP-based token authentication system using:

  • JSON Web Tokens (JWT)
  • Refresh token rotation
  • Access control via middleware
  • Secure storage in MySQL

We’ll go step by step — snippet by snippet — to explain how JWT auth works and how to build it from scratch using vanilla PHP (no frameworks). This is production-style learning, not just theory.


📁 Project Structure Overview

Before diving into code, here’s how we’ll structure this project:

/auth-api/
│
├── config/
│   └── database.php
│
├── includes/
│   ├── jwt_utils.php
│   ├── auth_middleware.php
│   └── helpers.php
│
├── public/
│   ├── register.php
│   ├── login.php
│   ├── token.php
│   └── protected.php
│
└── .env

We’ll walk through each section gradually. Let’s begin by setting up our database.


🧱 Step 1: Create the Users Table in MySQL

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    refresh_token VARCHAR(500),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

🔍 Explanation:

  • username: User's login identifier.
  • password_hash: We’ll use PHP's password_hash() for secure storage.
  • refresh_token: We store a hashed version of the refresh token.
  • created_at: Timestamp for tracking user creation.

⚙️ Step 2: Setup Database Connection (config/database.php)

<?php
$host = 'localhost';
$db_name = 'auth_api';
$username = 'root';
$password = '';

try {
    $pdo = new PDO("mysql:host=$host;dbname=$db_name;charset=utf8mb4", $username, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die("Connection failed: " . $e->getMessage());
}
?>

🧠 Explanation:

This uses PDO (PHP Data Objects) for secure and flexible database access. Always enable exceptions to catch issues.


🔐 Step 3: Register New Users (public/register.php)

<?php
require '../config/database.php';

$data = json_decode(file_get_contents("php://input"), true);
$username = $data['username'] ?? '';
$password = $data['password'] ?? '';

if (!$username || !$password) {
    http_response_code(400);
    echo json_encode(["message" => "Username and password required"]);
    exit;
}

$hash = password_hash($password, PASSWORD_BCRYPT);
$stmt = $pdo->prepare("INSERT INTO users (username, password_hash) VALUES (?, ?)");

try {
    $stmt->execute([$username, $hash]);
    echo json_encode(["message" => "User registered successfully"]);
} catch (PDOException $e) {
    http_response_code(409);
    echo json_encode(["message" => "Username already exists"]);
}

🔒 Key Details:

  • Uses password_hash() for secure storage.
  • Returns 409 if username already exists.
  • Handles empty input with 400.

🔑 Step 4: JWT Utility Functions (includes/jwt_utils.php)

<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

require '../vendor/autoload.php';

$secret_key = "your_secret_key";

function create_jwt($user_id, $ttl = 300) {
    global $secret_key;
    $issuedAt = time();
    $payload = [
        'iat' => $issuedAt,
        'exp' => $issuedAt + $ttl,
        'sub' => $user_id
    ];
    return JWT::encode($payload, $secret_key, 'HS256');
}

function validate_jwt($token) {
    global $secret_key;
    try {
        return JWT::decode($token, new Key($secret_key, 'HS256'));
    } catch (Exception $e) {
        return null;
    }
}

📌 What This Does:

  • create_jwt(): Generates a short-lived access token (e.g., 5 mins).
  • validate_jwt(): Decodes and validates the token signature and expiration.

ℹ️ Uses firebase/php-jwt library — install with composer require firebase/php-jwt.


🧾 Step 5: Login and Issue Tokens (public/login.php)

<?php
require '../config/database.php';
require '../includes/jwt_utils.php';

$data = json_decode(file_get_contents("php://input"), true);
$username = $data['username'];
$password = $data['password'];

$stmt = $pdo->prepare("SELECT id, password_hash FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();

if (!$user || !password_verify($password, $user['password_hash'])) {
    http_response_code(401);
    echo json_encode(["message" => "Invalid credentials"]);
    exit;
}

$access_token = create_jwt($user['id']);
$refresh_token = bin2hex(random_bytes(64));
$refresh_token_hash = password_hash($refresh_token, PASSWORD_DEFAULT);

$update = $pdo->prepare("UPDATE users SET refresh_token = ? WHERE id = ?");
$update->execute([$refresh_token_hash, $user['id']]);

echo json_encode([
    "access_token" => $access_token,
    "refresh_token" => $refresh_token
]);

🧪 Flow Breakdown:

  1. Verifies user credentials.
  2. Issues a short-lived access token.
  3. Issues a long-lived refresh token (securely hashed in DB).
  4. Returns both to client.

✅ This is the first half of our in-depth JWT authentication system.

Coming up next in Part 2:

  • Refresh token endpoint
  • Middleware to protect routes
  • Token invalidation & refresh rotation
  • Security considerations

➡️ Continue to Part 2 →


🧠 Best Practices & Tips

  • Rotate refresh tokens every time they’re used.
  • Never store access tokens in localStorage (use memory or HttpOnly cookies).
  • Always hash sensitive data — even refresh tokens.
  • Use HTTPS to avoid man-in-the-middle attacks.

🔍 SEO & Web Performance Angle

JWT-based auth helps improve scalability and session performance by avoiding server-side session storage. This makes it ideal for API-driven apps, SPAs, and mobile backends — which translates into faster response times and better user experience.


✅ Conclusion

Building secure token-based authentication from scratch might feel complex, but it's a foundational skill for modern backend developers.

Take your time to study each snippet and test the API flow using Postman or cURL. Once you're confident, try extending it with token blacklisting, role-based access, or OAuth integration.

💬 Drop your questions in the comments or join our Tech Talker 360 Facebook group to discuss more!


🤝 Stay Connected with Tech Talker 360

Post a Comment

0 Comments