Why Use JWT Authentication in CodeIgniter 4? Benefits for Modern APIs

             

In today's API-driven world, securing your backend is non-negotiable—especially for mobile apps, single-page applications (SPAs), and microservices that rely on RESTful endpoints. Traditional session-based authentication falls short here because it’s stateful, server-heavy, and tricky to scale across distributed systems. That's where JWT (JSON Web Token) authentication shines in CodeIgniter 4.

JWT is a compact, self-contained standard (RFC 7519) that securely transmits information between parties as a JSON object. It’s digitally signed (using HS256 or RS256) to guarantee integrity and authenticity—no database lookups needed on every request. This makes it stateless, lightweight, and perfect for high-performance APIs. With CodeIgniter 4 JWT authentication, you eliminate server-side session storage, reduce latency, and enable seamless cross-origin requests over HTTPS.

Key advantages include:

  • Scalability: Tokens are verified independently—no shared session store required.
  • Security: Built-in expiration (exp), issued-at (iat), and custom claims prevent replay attacks and unauthorized access.
  • Flexibility: Ideal for CodeIgniter 4 login and register flows in APIs, supporting role-based access, refresh tokens, and integration with modern frontends.
  • Simplicity: Libraries like firebase/php-jwt or daycry/jwt make implementation straightforward in CodeIgniter 4’s lightweight architecture.

Whether you're building a mobile backend or a headless CMS, mastering JWT authentication in CodeIgniter 4 ensures your API remains secure, efficient, and future-proof. In this updated 2026 tutorial, we'll walk through every step—from setup to protected routes—so you can implement a robust, production-ready system today.



CodeIgniter 4 JWT Authentication: Complete Tutorial for Secure API Login & Register

Table Of Content

1 Prerequisites

  • PHP 8.2 or higher
  • Composer
  • MySQL or compatible database
  • Basic knowledge of CodeIgniter 4

2 Introduction

In this comprehensive tutorial, you'll learn how to implement CodeIgniter 4 JWT authentication to create a secure, stateless REST API. We'll guide you step-by-step through building CodeIgniter 4 login and register functionality using JWT (JSON Web Tokens), covering token generation, validation with filters, and protecting API endpoints.

Whether you're developing mobile backends, SPAs, or microservices, mastering CodeIgniter 4 JSON Web Token authentication is essential for modern API security. JWT provides a compact, signed, and self-contained token that eliminates server-side sessions, improves scalability, and supports secure cross-origin requests over HTTPS. In 2026, the recommended production approach leverages CodeIgniter Shield's built-in JWT authenticator (an official extension) for robust, framework-native handling—though we'll also demonstrate a lightweight manual implementation using firebase/php-jwt or daycry/jwt for flexibility.

By the end, you'll have a fully functional, production-ready authentication system that enhances API integrity, prevents unauthorized access, and follows current best practices for token expiration, claims validation, and refresh mechanisms.

3 Create / Install a Codeigniter 4 Project

3.1 Install Codeigniter 4 Project

First, ensure you have Composer installed. Use the following command to create a new CodeIgniter project:

composer create-project codeigniter4/appstarter ci-4-jwt-app

Navigate to your project directory:

cd ci-4-jwt-app

3.2 Configure Environment (.env)

Upon installing CodeIgniter 4, you will find an env file at the root of your project. Rename it to .env to activate environment variables. This can also be done via the command line:

sudo cp env .env

Next, set up development mode by editing the .env file:

# CI_ENVIRONMENT = production
CI_ENVIRONMENT = development


Configure the database settings in .env:

app.baseURL = 'http://localhost:8080/'
database.default.hostname = localhost
database.default.database = your_db
database.default.username = root
database.default.password = 
database.default.DBDriver = MySQLi

JWT_SECRET = your_very_long_random_secret_key_here_32_chars_min

4 Create A Model and Migration

Create a migration for the users table and a model to store data:
Create a migration file for the users table:

php spark make:migration AddUser

Edit the migration file to define the table structure:

<?php
namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class CreateUsersTable extends Migration
{
    public function up()
    {
        $this->forge->addField([
            'id'              => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
            'name'            => ['type' => 'VARCHAR', 'constraint' => 255],
            'email'           => ['type' => 'VARCHAR', 'constraint' => 255, 'unique' => true],
            'password'        => ['type' => 'VARCHAR', 'constraint' => 255],
            'created_at'      => ['type' => 'DATETIME', 'null' => true],
            'updated_at'      => ['type' => 'DATETIME', 'null' => true],
        ]);

        $this->forge->addKey('id', true);
        $this->forge->addUniqueKey('email');
        $this->forge->createTable('users');
    }

    public function down()
    {
        $this->forge->dropTable('users');
    }
}

Run the migration:

php spark migrate


php spark make:model UserModel

Edit app/Models/UserModel.php to configure the model for user management:

<?php
namespace App\Models;

use CodeIgniter\Model;

class UserModel extends Model
{
    protected $table            = 'users';
    protected $primaryKey       = 'id';
    protected $allowedFields    = ['name', 'email', 'password'];

    protected $useTimestamps    = true;
    protected $createdField     = 'created_at';
    protected $updatedField     = 'updated_at';

    protected $validationRules = [
        'name'     => 'required|min_length[3]',
        'email'    => 'required|valid_email|is_unique[users.email]',
        'password' => 'required|min_length[6]'
    ];
}

5 Install JWT Library

We will then install the JWT package using Composer:

composer require firebase/php-jwt

After installing the JWT package, add the JWT_SECRET in the .env file:

#--------------------------------------------------------------------
# JWT
#--------------------------------------------------------------------
JWT_SECRET = 'JWT SECRET KEY SAMPLE HERE'

6 Create a Helper for JWT

Create a helper file to manage token creation, validation, and refreshing.
Location: app/Helpers/jwt_Helper.php

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

function generateJWT(array $userData): string
{
    $key = getenv('JWT_SECRET');
    $expiration = time() + (int)getenv('JWT_EXPIRATION');

    $payload = [
        'iat'  => time(),
        'exp'  => $expiration,
        'data' => $userData
    ];

    return JWT::encode($payload, $key, 'HS256');
}

function validateJWT(string $token)
{
    try {
        $key = getenv('JWT_SECRET');
        return JWT::decode($token, new Key($key, 'HS256'));
    } catch (\Exception $e) {
        return null;
    }
}

Autoload Helper → Edit app/Config/Autoload.php:

  
    public $helpers = ['url', 'jwt'];
 
 

7 Create Controller ( Auth Controller - Register + Login )

Use the following Spark command to create a controller:

php spark make:controller Auth
php spark make:controller User

Edit app/Controllers/Auth.php to handle CodeIgniter 4 JWT Login and Register with JWT (JSON Web Token) functionality.

<?php

namespace App\Controllers;

use App\Models\UserModel;
use CodeIgniter\RESTful\ResourceController;

class AuthController extends ResourceController
{
    public function register()
    {
        $rules = [
            'name'     => 'required|min_length[3]',
            'email'    => 'required|valid_email|is_unique[users.email]',
            'password' => 'required|min_length[6]'
        ];

        if (!$this->validate($rules)) {
            return $this->failValidationErrors($this->validator->getErrors());
        }

        $userModel = new UserModel();
        $data = [
            'name'     => $this->request->getVar('name'),
            'email'    => $this->request->getVar('email'),
            'password' => password_hash($this->request->getVar('password'), PASSWORD_DEFAULT),
        ];

        $userModel->insert($data);
        return $this->respondCreated(['message' => 'User registered successfully']);
    }

    public function login()
    {
        $rules = [
            'email'    => 'required|valid_email',
            'password' => 'required'
        ];

        if (!$this->validate($rules)) {
            return $this->failValidationErrors($this->validator->getErrors());
        }

        $userModel = new UserModel();
        $user = $userModel->where('email', $this->request->getVar('email'))->first();

        if (!$user || !password_verify($this->request->getVar('password'), $user['password'])) {
            return $this->failUnauthorized('Invalid email or password');
        }

        $payload = [
            'id'    => $user['id'],
            'email' => $user['email'],
            'name'  => $user['name']
        ];

        $token = generateJWT($payload);

        return $this->respond([
            'message' => 'Login successful',
            'token'   => $token
        ]);
    }
}

Location: app/Controllers/User.php

<?php
namespace App\Controllers;

use CodeIgniter\RESTful\ResourceController;

class UserController extends ResourceController
{
    public function profile()
    {
        return $this->respond([
            'user' => $this->request->user
        ]);
    }
}

8 Create Auth Filter

Controller filters perform actions before or after controllers execute.
Use the following spark command to Create Controller Filter.

php spark make:filter AuthFilter 

Location: app/Filters/AuthFilter.php

<?php
namespace App\Filters;

use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Filters\FilterInterface;

class AuthFilter implements FilterInterface
{
    public function before(RequestInterface $request, $arguments = null)
    {
        $authHeader = $request->getHeaderLine('Authorization');

        if (!$authHeader) {
            return Services::response()
                ->setJSON(['error' => 'Authorization token required'])
                ->setStatusCode(401);
        }

        $token = str_replace('Bearer ', '', $authHeader);
        $decoded = validateJWT($token);

        if (!$decoded) {
            return Services::response()
                ->setJSON(['error' => 'Invalid or expired token'])
                ->setStatusCode(401);
        }

        $request->user = $decoded->data;
        return $request;
    }

    public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
    {
        // No post-processing needed
    }
}

After creating the filter, we must add it to filters config located at app/Config/Filters.php. We will creating an alias for our filter. Location: app/Config/Filters.php

<?php
namespace Config;

use CodeIgniter\Config\Filters as BaseFilters;
use CodeIgniter\Filters\Cors;
use CodeIgniter\Filters\CSRF;
use CodeIgniter\Filters\DebugToolbar;
use CodeIgniter\Filters\ForceHTTPS;
use CodeIgniter\Filters\Honeypot;
use CodeIgniter\Filters\InvalidChars;
use CodeIgniter\Filters\PageCache;
use CodeIgniter\Filters\PerformanceMetrics;
use CodeIgniter\Filters\SecureHeaders;



class Filters extends BaseFilters
{
    /**
     * Configures aliases for Filter classes to
     * make reading things nicer and simpler.
     *
     * @var array>
     *
     * [filter_name => classname]
     * or [filter_name => [classname1, classname2, ...]]
     */
    public array $aliases = [
        'csrf'          => CSRF::class,
        'toolbar'       => DebugToolbar::class,
        'honeypot'      => Honeypot::class,
        'invalidchars'  => InvalidChars::class,
        'secureheaders' => SecureHeaders::class,
        'cors'          => Cors::class,
        'forcehttps'    => ForceHTTPS::class,
        'pagecache'     => PageCache::class,
        'performance'   => PerformanceMetrics::class,
        'authFilter' => \App\Filters\AuthFilter::class,
    ];

    /**
     * List of special required filters.
     *
     * The filters listed here are special. They are applied before and after
     * other kinds of filters, and always applied even if a route does not exist.
     *
     * Filters set by default provide framework functionality. If removed,
     * those functions will no longer work.
     *
     * @see https://codeigniter.com/user_guide/incoming/filters.html#provided-filters
     *
     * @var array{before: list, after: list}
     */
    public array $required = [
        'before' => [
            'forcehttps', // Force Global Secure Requests
            'pagecache',  // Web Page Caching
        ],
        'after' => [
            'pagecache',   // Web Page Caching
            'performance', // Performance Metrics
            'toolbar',     // Debug Toolbar
        ],
    ];

    /**
     * List of filter aliases that are always
     * applied before and after every request.
     *
     * @var array>>|array>
     */
    public array $globals = [
        'before' => [
            // 'honeypot',
            // 'csrf',
            // 'invalidchars',
        ],
        'after' => [
            // 'honeypot',
            // 'secureheaders',
        ],
    ];

    /**
     * List of filter aliases that works on a
     * particular HTTP method (GET, POST, etc.).
     *
     * Example:
     * 'POST' => ['foo', 'bar']
     *
     * If you use this, you should disable auto-routing because auto-routing
     * permits any HTTP method to access a controller. Accessing the controller
     * with a method you don't expect could bypass the filter.
     *
     * @var array>
     */
    public array $methods = [];

    /**
     * List of filter aliases that should run on any
     * before or after URI patterns.
     *
     * Example:
     * 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
     *
     * @var array>>
     */
    public array $filters = [];
}


9 Define a Route

In app/Config/Routes.php, define routes for the controller:

<?php
use CodeIgniter\Router\RouteCollection;
/**
 * @var RouteCollection $routes
 */
$routes->get('/', 'Home::index');
$routes->group('api', function ($routes) {
    $routes->post('register', 'AuthController::register');
    $routes->post('login', 'AuthController::login');

    // Protected routes
    $routes->group('', ['filter' => 'auth'], function ($routes) {
        $routes->get('profile', 'UserController::profile');
    });
});

10 Folder Structure

11 Run Web Server to Test the App

To test the Codeigniter 4 API Authentication Using JWT, set the request header to Accept: application/json and include the JWT token as a Bearer token for authenticated routes. Start the server using:


Use the following spark command to Test the App.

php spark serve

Test with Postman

1. Register:

  • POST → http://localhost:8080/api/register
  • Body (JSON):
  
    {
      "name": "John Doe",
      "email": "john@example.com",
      "password": "password123"
    }

2. Login:

  • POST → http://localhost:8080/api/login
  • Get token from response

3. Get Profile:

  • GET → http://localhost:8080/api/profile
  • Headers: Authorization: Bearer YOUR_TOKEN_HERE

11 Conclusion

You've now built a secure JWT authentication system in CodeIgniter 4! Extend with role-based access, refresh tokens, etc.
Revathi M - PHP and CodeIgniter Developer

Written by Revathi M

PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel

Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.

Frequently Asked Questions

The tutorial uses firebase/php-jwt, installed via composer require firebase/php-jwt.

Run composer require firebase/php-jwt, then add JWT_SECRET='your-secret-key' to your .env file. Use this secret for encoding and decoding tokens.

In AuthController, register() validates and saves user with password_hash(). login() verifies credentials and generates a JWT access token (valid for 10 minutes) using the helper function.

Create an AuthFilter that checks the Authorization: Bearer header, validates the token, and returns 401 if invalid. Apply the filter to routes in Routes.php.

Create a 'users' migration with id, name, email, created_at, and updated_at fields. Use a UserModel with allowedFields for safe insertion.

Access tokens expire after 10 minutes (iat + 600 seconds). Use the refresh-token endpoint to generate a new token from a valid existing one.

Include it in the header: Authorization: Bearer . The AuthFilter extracts and validates it for protected routes.

Common issues: Missing or invalid Bearer token, expired token, incorrect JWT_SECRET, or filter not applied to the route. Check headers and token validity.

Yes, a /api/refresh-token endpoint validates the current access token and issues a new one (no separate long-lived refresh token).

It uses HS256 signing, password hashing, and short-lived tokens. For production, use HTTPS, longer secrets, and consider adding blacklisting for revoked tokens.