CodeIgniter 4 REST API JWT Authentication

Table of Contents

Introduction

In this example I will show you how to use JWT (JSON Web Token) in CodeIgniter 4 REST API for authenticating/authorizing a user when he/she is trying to access REST API. A user must register in the JWT server where he/she continues to generate JWT using the username and password in order to access the REST API.

The JWT is valid for a certain period of time and he/she can use the same JWT until it expires. The user must generate a new JWT upon its expiration.

You might have seen how to generate and validate JWT using PHP language without using any third party library. Here I will use the same concept to generate the JWT for individual user and allow him/her continue his/her work until JWT expires.

Prerequisites

Make sure your CodeIgniter framework 4 has been setup before you proceed further down. Here I am using CodeIgniter version 4.1.9, MySQL 8.0.26, PHP 7.4.27

Project Directory

It’s assumed that you have setup PHP and Codeigniter in Windows system.

Now I will create a project root directory called codeigniter-rest-jwt-authentication anywhere in the physical drive.

Now move all the directories and files from CodeIgniter framework into codeigniter-rest-jwt-authentication directory.

I may not mention the project root directory in subsequent sections and I will assume that I am talking with respect to the project root directory.

MySQL Table

I am creating two tables – user and teacher – under roytuts database. The table creation DDL is given below:

CREATE TABLE user (
    id INT unsigned COLLATE utf8mb4_unicode_ci AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(45) COLLATE utf8mb4_unicode_ci NOT NULL,
    password VARCHAR(255) COLLATE utf8mb4_unicode_ci NOT NULL
);

CREATE TABLE `teacher` (
  `id` int unsigned COLLATE utf8mb4_unicode_ci NOT NULL AUTO_INCREMENT,
  `name` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
  `expertise` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

I am also inserting some data into the teacher table so that I can test the application right away.

insert  into `teacher`(`id`,`name`,`expertise`)
values (1,'Bibhas Chandra Dhara','Statistics'),
(2,'UKR','System Programming'),(3,'New','Expert');

Database Configuration

You need to setup database connection in order to fetch or write data to the table. The following configuration is done in the file app/Config/Database.php under the default group of database setting. Make sure you change or update the configuration according to yours.

public $default = [
	'DSN'      => '',
	'hostname' => 'localhost',
	'username' => 'root',
	'password' => 'root',
	'database' => 'roytuts',
	'DBDriver' => 'MySQLi',
	'DBPrefix' => '',
	'pConnect' => false,
	'DBDebug'  => (ENVIRONMENT !== 'production'),
	'cacheOn'  => false,
	'cacheDir' => '',
	'charset'  => 'utf8mb4',
	'DBCollat' => 'utf8mb4_unicode_ci',
	'swapPre'  => '',
	'encrypt'  => false,
	'compress' => false,
	'strictOn' => false,
	'failover' => [],
	'port'     => 3306,
];

Route Configuration

You also need to configure route to point to your own controller file instead of the default controller that comes with the framework. Search for the line $routes->get('/', 'Home::index'); and replace it by your resource controller name, for this example, $routes->resource('product');. The following route configuration is done on the file app/Config/Routes.php.

//$routes->get('/', 'Home::index');
$routes->resource('teacher');

You can safely remove the line $routes->setDefaultController('Home');, $routes->setDefaultMethod('index'); and $routes->get('/', 'Home::index'); from the route configuration file because view files are not used but REST API.

Even if you do not put resource() config in route configuration file, it does not really matter, because you are going to access the REST endpoint as controller name followed by the function name.

Model

You need a model class to perform database activities, for example, for implementing CRUD operations. The below model class is written into app/Models/UserModel.php and app/Models/TeacherModel.php files.

In CodeIgniter you can choose two ways of modeling your data – using CodeIgniter’s model and using entity class. I am using CodeIgniter’s model to create my own model that extends CodeIgniter’s model class. They come out of the box with helper methods for much of the standard ways you would need to interact with a database table, including finding records, updating records, deleting records, and more.

The UserModel.php file has the following code:

<?php

namespace App\Models;
use CodeIgniter\Model;

class UserModel extends Model {

	protected $table      = 'user';
    protected $primaryKey = 'id';
	
	protected $returnType     = 'array';

    protected $allowedFields = ['username', 'password'];

    protected $validationRules    = [];
    protected $validationMessages = [];
    protected $skipValidation     = false;
	
}

The TeacherModel.php file has the following code:

<?php

namespace App\Models;
use CodeIgniter\Model;

class TeacherModel extends Model {

	protected $table      = 'teacher';
    protected $primaryKey = 'id';
	
	protected $returnType     = 'array';

    protected $allowedFields = ['name', 'expertise'];

    protected $validationRules    = [];
    protected $validationMessages = [];
    protected $skipValidation     = false;
	
}

The model class has a few configuration options that can be set to allow the class’ methods to work seamlessly for you.

$table – specifies the database table that this model primarily works with. This only applies to the built-in CRUD methods. You are not restricted to using only this table in your own queries.

$primaryKey – this is the name of the column that uniquely identifies the records in this table. This does not necessarily have to match the primary key that is specified in the database, but is used with methods like find() to know what column to match the specified value to.

$returnType – the Model’s CRUD methods will take a step of work away from you and automatically return the resulting data, instead of the Result object. This setting allows you to define the type of data that is returned. Valid values are ‘array’, ‘object’, or the fully qualified name of a class that can be used with the Result object’s getCustomResultObject() method.

$allowedFields – this array should be updated with the field names that can be set during save, insert, or update methods. Any field names other than these will be discarded. This helps to protect against just taking input from a form and throwing it all at the model, resulting in potential mass assignment vulnerabilities.

$validationRules – contains either an array of validation rules as described in How to save your rules or a string containing the name of a validation group, as described in the same section. Described in more detail below.

$validationMessages – contains an array of custom error messages that should be used during validation.

$skipValidation – whether validation should be skipped during all inserts and updates. The default value is false, meaning that data will always attempt to be validated. This is primarily used by the skipValidation() method, but may be changed to true so this model will never validate.

Controller

The following resource controllers (app/Controllers/User.php and app/Controllers/Teacher.php) define endpoints for REST APIs and interact with database table to perform the required activities.

You load your model class by overriding the $modelName property and response format from your REST APIs using $format property.

The User.php file contains the following code for registering new user and logging in the existing user.

<?php
namespace App\Controllers;

use CodeIgniter\RESTful\ResourceController;
use CodeIgniter\HTTP\RequestInterface;

use App\Libraries\JwtLibrary;

class User extends ResourceController {
	
    protected $modelName = 'App\Models\UserModel';
    protected $format    = 'json';
	
	private $jwtLib;
	
	public function __construct() {
		$this->jwtLib = new JwtLibrary();
	}

	// fetch all teachers
    public function register() {
		// get posted JSON
		//$json = json_decode(file_get_contents("php://input", true));
		$json = $this->request->getJSON();
		
		$username = $json->username;
		$password = $json->password;

		$data = array(
			'username' => $username,
			'password' => $password
		);
		
		$user = $this->model->where('username', $username)->first();
		
		if($user) {
			$response = array(
				'status'   => 409,
				'messages' => array(
					'success' => 'User already exists'
				)
			);
			
			return $this->failResourceExists($response);
		} else {		
			$this->model->insert($data);
			
			$response = array(
				'status'   => 201,
				'messages' => array(
					'success' => 'User registered successfully'
				)
			);
			
			return $this->respondCreated($response);
		}
    }
	
	public function login() {
		// get posted JSON
		//$json = json_decode(file_get_contents("php://input", true));
		$json = $this->request->getJSON();
		
		$username = $json->username;
		$password = $json->password;
		
        $data = $this->model->where(array('username' => $username, 'password' => $password))->first();
		
        if($data) {
			$headers = array('alg'=>'HS256','typ'=>'JWT');
			$payload = array('username'=>$username, 'exp'=>(time() + 60));

			$jwt = $this->jwtLib->generate_jwt($headers, $payload);
		
            return $this->respond(array('token' => $jwt));
        } else {
            return $this->failNotFound('No user found');
        }
    }

}

While registering a new user I have checked whether the user with same username already exists. If the user exists already then I am sending 409 http status code and 201 http status code when new record created in the database.

The endpoint for registration will be controller name followed by the function name, i.e., user/register.

The login function returns the JWT token if user exists otherwise returns 404 http status code (Not found).

The endpoint for login will be user/login. Both login() and register() take payload in the request body.

The Teacher.php file contains the following code. It fetches all teacher details from the database for the authorized user by validating the JWT token.

<?php
namespace App\Controllers;

use CodeIgniter\RESTful\ResourceController;
use CodeIgniter\HTTP\RequestInterface;

use App\Libraries\JwtLibrary;

class Teacher extends ResourceController {
	
    protected $modelName = 'App\Models\TeacherModel';
    protected $format    = 'json';
	
	private $jwtLib;
	
	public function __construct() {
		$this->jwtLib = new JwtLibrary();
	}

	// fetch all teachers
    public function index() {
		$bearer_token = $this->jwtLib->get_bearer_token();
		
		//echo $bearer_token;

		$is_jwt_valid = $this->jwtLib->is_jwt_valid($bearer_token);

		if($is_jwt_valid) {
			return $this->respond($this->model->findAll());
		} else {
			return $this->failUnauthorized('Unauthorized Access');
		}
    }

}

The endpoint for fetching teacher data is teacher. index() is the default function, so even if you do not mention index in the endpoint then also it will call the index() function of the controller. On unauthorized access I am returning 401 http status code.

Library

I have created a custom library for JWT token generation and validation. The library is written into the file app/Libraries/JwtLibrary.php file.

<?php

namespace App\Libraries;

/**
 * Description of JWT Library
 *
 * @author https://roytuts.com
 */
class JwtLibrary {
	
	private $request;
	
	public function __construct() {
		$this->request = \Config\Services::request();
	}
	
	function generate_jwt($headers, $payload, $secret = 'secret') {
		$headers_encoded = $this->base64url_encode(json_encode($headers));
		
		$payload_encoded = $this->base64url_encode(json_encode($payload));
		
		$signature = hash_hmac('SHA256', "$headers_encoded.$payload_encoded", $secret, true);
		$signature_encoded = $this->base64url_encode($signature);
		
		$jwt = "$headers_encoded.$payload_encoded.$signature_encoded";
		
		return $jwt;
	}

	function is_jwt_valid($jwt, $secret = 'secret') {
		if(empty($jwt)) {
			return false;
		}
			
		// split the jwt
		$tokenParts = explode('.', $jwt);
		$header = base64_decode($tokenParts[0]);
		$payload = base64_decode($tokenParts[1]);
		$signature_provided = $tokenParts[2];

		// check the expiration time - note this will cause an error if there is no 'exp' claim in the jwt
		$expiration = json_decode($payload)->exp;
		$is_token_expired = ($expiration - time()) < 0;

		// build a signature based on the header and payload using the secret
		$base64_url_header = $this->base64url_encode($header);
		$base64_url_payload = $this->base64url_encode($payload);
		$signature = hash_hmac('SHA256', $base64_url_header . "." . $base64_url_payload, $secret, true);
		$base64_url_signature = $this->base64url_encode($signature);

		// verify it matches the signature provided in the jwt
		$is_signature_valid = ($base64_url_signature === $signature_provided);
		
		if ($is_token_expired || !$is_signature_valid) {
			return false;
		} else {
			return true;
		}
	}

	function base64url_encode($data) {
		return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
	}

	function get_authorization_header(){
		$headers = null;
		
		if ($this->request->getServer('Authorization')) {
			$headers = $this->request->getServer('Authorization');
		} else if ($this->request->getServer('HTTP_AUTHORIZATION')) { //Nginx or fast CGI
			$headers = $this->request->getServer('HTTP_AUTHORIZATION');
		} else if (function_exists('apache_request_headers')) {
			$requestHeaders = apache_request_headers();
			// Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization)
			$requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
			//print_r($requestHeaders);
			if (isset($requestHeaders['Authorization'])) {
				$headers = trim($requestHeaders['Authorization']);
			}
		}
		
		return $headers;
	}

	function get_bearer_token() {
		$headers = $this->get_authorization_header();
		
		// HEADER: Get the access token from the header
		if (!empty($headers)) {
			if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
				return $matches[1];
			}
		}
		return null;
	}
	
}

I am not going to explain everything from the library class but it’s self explanatory. This library class has been used in User and Teacher controller classes.

Testing CodeIgniter 4 REST JWT

I am not going to use any external server but CLI command to run the application. Make sure you start the MySQL database server before you start your application. If you want to use external server to run your application you can use. Execute the following command on your project root directory to run your application.

php spark serve

Your application will be running on localhost and port 8080.

To test this application I am going to use Postman tool. You can use any REST client as per your convenience.

First a user need to register before accessing the REST API using username and password through REST API:

codeigniter4 rest jwt authentication

You will find the new user data stored into the MySQL database. Remember it is not good idea to store password in plain text. You can use encryption algorithm to make it a random string.

jwt codeigniter 4 rest api

The same username and password will be used as a login credential to obtain the JWT token through REST API.

codeigniter 4 rest api jwt authentication

If you try to access REST API without sending Authorization header with JWT as a Bearer token then you will get access denied error:

JWT authorization error 401

When you send Authorization header with JWT as a Bearer token, then you will be able to see the results in response:

codeigniter 4 rest jwt authentication

Hope you got an idea how to use JWT in CodeIgniter 4 REST API to authenticate/authorize user before the user can access the REST API.

Source Code

Download

Leave a Reply

Your email address will not be published. Required fields are marked *