Introduction to CodeIgniter 4 and JWT Authentication

             JSON Web Token (JWT) authentication serves as a gatekeeper, ensuring that data transmitted between two entities can be trusted. Unlike encryption, JWT is an open standard that allows secure transmission of information as a JSON object. The data is digitally signed using either a public/private key pair or a secret, ensuring the integrity of the exchange.

CodeIgniter 4 JWT -  JWT Authentication and API Integration Tutorial

Table Of Content

1 Prerequisites

1.) PHP version of >= 8.2
2.) Composer
3.) Mysql

2 Introduction

In this tutorial, we will delve into the intricacies of integrating CodeIgniter 4 JWT Authentication into your applications, guiding you through CodeIgniter 4 Login and Register with JWT (JSON Web Token) for API-driven functionalities. This hands-on demonstration will not only teach you about CodeIgniter 4 JSON Web Token (JWT) Authentication, but also showcase how JWT can enhance your API security.

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:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=ci4_jwt
DB_USERNAME=root
DB_PASSWORD=

4 Create A Model and Migration

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

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', 'created_at'];
}

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 AddUser extends Migration
{
    public function up()
    {
        $this->forge->addField([
            'id' => [
                'type' => 'BIGINT',
                'constraint' => 255,
                'unsigned' => true,
                'auto_increment' => true
            ],
            'name' => [
                'type' => 'VARCHAR',
                'constraint' => '255',
            ],
            'email' => [
                'type' => 'longtext'
            ],
            'created_at' => [
                'type' => 'TIMESTAMP',
                'null' => true
            ],
            'updated_at' => [
                'type' => 'TIMESTAMP',
                'null' => true
            ],
        ]);
        $this->forge->addPrimaryKey('id');
        $this->forge->createTable('users');
    }

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


Run the migration:

php spark migrate

5 Install JWT Package

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 generateAccessToken($userData) {
    $key = getenv('JWT_SECRET');
    $issuedAt = time();
    $expirationTime = $issuedAt + 600; // 10 minutes validity for access token
   
    $payload = [
        'iat' => $issuedAt,
        'exp' => $expirationTime,
        'data' => $userData
    ];
    return JWT::encode($payload, $key, 'HS256');
}

function validateToken($token) {
    $key = getenv('JWT_SECRET');
 
    try {
        return JWT::decode($token, new Key($key, 'HS256'));
    } catch (Exception $e) {
        return null; // Invalid or expired token
    }
}

7 Create Controller

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\Controllers\BaseController;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\API\ResponseTrait;
use App\Models\UserModel;

class Auth extends BaseController
{
    use ResponseTrait;
    public function register()
    {
        $rules = [
            'email' => ['rules' => 'required|min_length[4]|valid_email|is_unique[users.email]'],
            'password' => ['rules' => 'required|min_length[6]'],
            'confirm_password'  => [ 'label' => 'confirm password', 'rules' => 'matches[password]']
        ];
            
  
        if($this->validate($rules)){
            $model = new UserModel();
            $data = [
                'email'    => $this->request->getVar('email'),
                'password' => password_hash($this->request->getVar('password'), PASSWORD_DEFAULT)
            ];
            $model->save($data);
             
            return $this->respond(['message' => 'Registered Successfully'], 200);
        }else{
            $response = [
                'errors' => $this->validator->getErrors(),
                'message' => 'Invalid Inputs'
            ];
            return $this->fail($response , 409);
             
        }
    }
    public function login()
    {
        $userModel = new UserModel();
   
        $email = $this->request->getVar('email');
        $password = $this->request->getVar('password');
           
        $user = $userModel->where('email', $email)->first();
   
        if(is_null($user)) {
            return $this->respond(['error' => 'Invalid Email Id.'], 401);
        }
   
        $pwd_verify = password_verify($password, $user['password']);
   
        if(!$pwd_verify) {
            return $this->respond(['error' => 'Wrong password.'], 401);
        }
        helper('Jwt');
        $token = generateAccessToken($user);
        
        $response = [
            'message' => 'Login Succesful',
            'token' => $token
        ];
          
        return $this->respond($response, 200);
    }
}


Location: app/Controllers/User.php

<?php
namespace App\Controllers;

use App\Controllers\BaseController;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\API\ResponseTrait;
use App\Models\UserModel;


class User extends BaseController
{
    use ResponseTrait;
    public function index()
    {
        $users = new UserModel;
        return $this->respond(['users' => $users->findAll()], 200);
    }
    public function getuser()
    {
        $header = $this->request->getHeader("Authorization");
        // extract the token from the header
        if(!empty($header)) {
            if (preg_match('/Bearer\s(\S+)/', $header, $matches)) {
                $token = $matches[1];
            }
        }
        if(is_null($token) || empty($token)) {
            return $this->respond(['error' => 'Invalid Token'], 401);
        }

        helper('Jwt');
        $decoded = validateToken($token);
        $response = [
            'id' => $decoded->data->id,
            'email' => $decoded->data->email
        ];
        return $this->respond($response, 200);
    }
    public function get_refresh_token()
    {
        $header = $this->request->getHeader("Authorization");

        // extract the token from the header
        if(!empty($header)) {
            if (preg_match('/Bearer\s(\S+)/', $header, $matches)) {
                $token = $matches[1];
            }
        }

        if(is_null($token) || empty($token)) {
            return $this->respond(['error' => 'Invalid Token'], 401);
        }

        helper('Jwt');
        $decoded = validateToken($token);
        $token = generateAccessToken($decoded->data);

        $response = [
            'message' => 'Refresh Token Generated Succesful',
            'token' => $token
        ];
          
        return $this->respond($response, 200);

    }
}


8 Create Controller 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\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class AuthFilter implements FilterInterface
{
    /**
     * Do whatever processing this filter needs to do.
     * By default it should not return anything during
     * normal execution. However, when an abnormal state
     * is found, it should return an instance of
     * CodeIgniter\HTTP\Response. If it does, script
     * execution will end and that Response will be
     * sent back to the client, allowing for error pages,
     * redirects, etc.
     *
     * @param RequestInterface $request
     * @param array|null       $arguments
     *
     * @return RequestInterface|ResponseInterface|string|void
     */
    public function before(RequestInterface $request, $arguments = null)
    {
        $key = getenv('JWT_SECRET');
        $header = $request->getHeader("Authorization");
        $token = null;
   
        // extract the token from the header
        if(!empty($header)) {
            if (preg_match('/Bearer\s(\S+)/', $header, $matches)) {
                $token = $matches[1];
            }
        }
   
        // check if token is null or empty
        if(is_null($token) || empty($token)) {
            $response = service('response');
            $response->setBody('Access denied');
            $response->setStatusCode(401);
            return $response;
        }
   
        try {
            // $decoded = JWT::decode($token, $key, array("HS256"));
            $decoded = JWT::decode($token, new Key($key, 'HS256'));
        } catch (\Exception $ex) {
            $response = service('response');
            $response->setBody('Access denied');
            $response->setStatusCode(401);
            return $response;
        }
    }

    /**
     * Allows After filters to inspect and modify the response
     * object as needed. This method does not allow any way
     * to stop execution of other after filters, short of
     * throwing an Exception or Error.
     *
     * @param RequestInterface  $request
     * @param ResponseInterface $response
     * @param array|null        $arguments
     *
     * @return ResponseInterface|void
     */
    public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
    {
        //
    }
}

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", "Auth::register");
    $routes->post("login", "Auth::login");
    $routes->get("users", "User::index", ['filter' => 'authFilter']);
    $routes->get("get-logger-user", "User::getuser", ['filter' => 'authFilter']);
    $routes->get("refresh-token", "User::get_refresh_token", ['filter' => 'authFilter']);
});

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

11 Conclusion

Your CodeIgniter 4 JSON Web Token (JWT) API Authentication is now fully set up.

Tags