Publicidad

Como crear una API con Codeigniter 4 y autenticación JWT


Por Alex el 21/03/2025, Comentar el artículo

Comparte este artículo:      




Con codeigniter y autenticación JWT podemos crear APIs Rests seguras, muy rápidas ya que codeigniter es uno de los frameworks PHP más rápidos y en muy poco tiempo.


En este ejemplo crearemos una APIs que para crear usuarios, hacer login y ver el perfil del usuario logueado, para ellos vamos a ver varias cosas:

Todo el código del proyecto está en el repositorio de github

1) Crear proyecto de codeigniter, requerimientos y configuraciones

En este punto vamos a:

  • Crear el proyecto
  • Instalar el componente JWT dentro del proyecto
  • Crear la base de datos de SQLite
  • Configurar el fichero .env
  • Crear la tabla de usuarios con una migración
Crear el proyecto con composer

composer create-project codeigniter4/appstarter ci-api-ejemplo

Y seguidamente dentro del directorio del proyecto instalamos la dependencia de JWT y así ya lo tenemos todo listo

cd ci-api-ejemplo
composer require firebase/php-jwt

Ahora creamos la base de datos con

touch writable/database.db

Damos permisos al directorio writable/

sudo chown 775 writable/ -R
sudo chown user:www-data writable/ -R

Donde user es tu usuario y www-data es el usuario de apache2, en windows creo que con dar permisos de escritura el directorio writablees suficiente.

Configuramos el fichero .env, que no existe, hay que renombrar el que viene env por .env y dentro poner la conexión a la base de datos

# Configuración de la base de datos para SQLite
database.default.DBDriver = SQLite3
database.default.database = /ruta/completa/a/writable/database.db
database.default.DBPrefix =
database.default.charset = utf8
database.default.DBDebug = true

Y ya que estamos el entorno

CI_ENVIRONMENT = development

Y el token JWT que utilizaremos

JWT_SECRET=t4DTq3wKW6EfnHb9x2kjumBN58JdFV7P

Así el fichero .env ya lo tenemos listo. Para probar si hemos creado bien la base de datos y la configuración a la base de datos se puede ejecutar el siguiente comando

php spark db:table

Este comando devuelve el listado de tablas o un mensaje que no hay tablas si la base de datos todavía está vacía- Si devuelve un error, efectivamente es que hay un error en la configuración. Ahora vamos a crear la tabla de usuarios Para esto crearemos una migración de la siguiente manera:

php spark make:migration CreateUsersTable

Y con el editor, por ejemplo, Visual Studio Code, abrimos el fichero que ha creado en APPPATH/Database/Migrations/2025-03-20-111032_CreateUsersTable.php

Y ponemos el siguiente código

namespace App\Database\Migrations;


use CodeIgniter\Database\Migration;


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


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


y ejecutamos la migración

php spark migrate

Y si miramos la base de datos tendríamos que tener la tabla users creada

Base de datos de SQLite - tabla users


1) Crear el Modelo de usuario y el controlador de autenticación y las rutas

Ya lo tenemos configurado el proyecto así que ahora crearemos

  • Un modelo para usuarios
  • Un controlador para autenticación
  • Y configuraremos el fichero de rutas
Para crear el modelo ejecutamos en el terminal

php spark make:model UserModel

Crea un fichero en APPPATH/Models/UserModel.php,

Aquí solo hay que añadir al array de allowedFields los campos de la tabla users y quedará de esta forma

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

Vamos con el controller de autenticación, lo creamos desde el terminal también

php spark make:controller AuthController

Crea el controlador en APPPATH/Controllers/AuthController.php, lo abrimos y eliminar lo que hay dentro y copiar el siguiente código

namespace App\Controllers;


use CodeIgniter\HTTP\ResponseInterface;


use CodeIgniter\RESTful\ResourceController;
use App\Models\UserModel;
use CodeIgniter\API\ResponseTrait;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use App\Libraries\JWTAuthService;


class AuthController extends ResourceController
{
   use ResponseTrait;
  
   public function register()
   {
       $rules = [
           'email' => 'required|valid_email|is_unique[users.email]',
           'username' => 'required|min_length[3]|is_unique[users.username]',
           'password' => 'required|min_length[8]',
       ];
      
       if (!$this->validate($rules)) {
           return $this->failValidationErrors($this->validator->getErrors());
       }
      
       $data = [
           'email' => $this->request->getVar('email'),
           'username' => $this->request->getVar('username'),
           'password' => password_hash($this->request->getVar('password'), PASSWORD_BCRYPT),
       ];
      
       $userModel = new UserModel();
       $userModel->save($data);
      
       return $this->respondCreated([
           'status' => 201,
           'message' => 'Usuario registrado correctamente',
           'user_id' => $userModel->getInsertID()
       ]);
   }




   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) {
           return $this->failNotFound('Email no encontrado');
       }
      
       $verify = password_verify($this->request->getVar('password'), $user['password']);
      
       if (!$verify) {
           return $this->fail('Contraseña incorrecta', 401);
       }
      
       $key = getenv('JWT_SECRET');
       $payload = [
           'iat' => time(),
           'exp' => time() + 3600, // Token válido por 1 hora
           'uid' => $user['id'],
       ];
      
       $token = JWT::encode($payload, $key, 'HS256');
      
       return $this->respond([
           'status' => 200,
           'message' => 'Login correcto',
           'token' => $token,
           'user' => [
               'id' => $user['id'],
               'email' => $user['email'],
               'username' => $user['username']
           ]
       ]);
   }
  


   public function profile()
   {
       # Obtener ID de usuario del servicio de autenticación
       $userId = JWTAuthService::getInstance()->getUserId();


       $userModel = new UserModel();
       $user = $userModel->find($userId);
      
       if (!$user)
       {
           return $this->failNotFound('Usuario no encontrado');
       }
      
       return $this->respond([
           'status' => 200,
           'user' => [
               'id' => $user['id'],
               'email' => $user['email'],
               'username' => $user['username']
           ]
       ]);
   }


}


Aquí tenemos los métodos de registro, login y profile.

Ahora creamos las rutas

Editar el fichero config/Routes.php y copiamos el siguiente código

$routes->group('api', ['namespace' => 'App\Controllers'], function ($routes)
{
   $routes->post('register', 'AuthController::register');
   $routes->post('login', 'AuthController::login');
  
   // Rutas protegidas que requieren autenticación
   $routes->group('', ['filter' => 'jwt'], function ($routes)
   {
       $routes->get('profile', 'AuthController::profile');
       // Aquí puedes agregar otras rutas protegidas
   });
});

Ahora queda crear el filtro JWT que aplicamos en estas rutas protegidas

$routes->group('', ['filter' => 'jwt'],

y el servicio para guardar los tokens


1) Añadir un filtro para validación del token JWT, un servicio singleton y un helper

Lo primero el filtro, los filtros en codeigniter son como los Middleware, se ejecutan antes y/o después del controller

Para crear un filtro ejecutamos desde el terminal

php spark make:filter JWTAuthFilter

Y crea un fichero en APPPATH/Filters/JWTAuthFilter.php

Lo abrimos y copiamos lo siguiente dentro

namespace App\Filters;


use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Firebase\JWT\JWT;
use Firebase\JWT\ExpiredException;
use Config\Services;
use Firebase\JWT\Key;
use App\Libraries\JWTAuthService;


class JWTAuthFilter implements FilterInterface
{
   public function before(RequestInterface $request, $arguments = null)
   {
       $header = $request->getServer('HTTP_AUTHORIZATION');
      
       if (!$header)
       {
           return Services::response()
               ->setJSON([
                   'status' => 401,
                   'error' => 'Token de autenticación no proporcionado'
               ])
               ->setStatusCode(ResponseInterface::HTTP_UNAUTHORIZED);
       }
      
       try
       {
           $token = explode(' ', $header)[1];
           $key = getenv('JWT_SECRET');
           $decoded = JWT::decode($token, new Key($key, 'HS256'));
                  
           # Almacenar la información del token en el servicio singleton
           JWTAuthService::getInstance()->setData($decoded, $token);
          
           return $request;


       }
       catch (ExpiredException $e)
       {
           return Services::response()
               ->setJSON([
                   'status' => 401,
                   'error' => 'Token expirado'
               ])
               ->setStatusCode(ResponseInterface::HTTP_UNAUTHORIZED);
       }
       catch (\Exception $e)
       {
           return Services::response()
               ->setJSON([
                   'status' => 401,
                   'error' => 'Token inválido: ' . $e->getMessage()
               ])
               ->setStatusCode(ResponseInterface::HTTP_UNAUTHORIZED);
       }
   }


   public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
   {
       // No se necesita hacer nada después
   }
}


Para que el filtro funcione correctamente hay que añadirlo al fichero de configuración app/Config/Filters.php

   public array $aliases = [
       // Otros filtros
       'jwt'           => \App\Filters\JWTAuthFilter::class
   ];



El que está al final del todo jwt

Y ahora creamos el servicio para guardar los tokens cuando se validan y en el controlador cuando se accede a una url protegida

Creamos el siguiente fichero app/Libraries/JWTAuthService.php y dentro ponemos el siguiente código

namespace App\Libraries;


class JWTAuthService
{
   private static $instance = null;
   private $userData = null;
   private $token = null;
  
   private function __construct() {}
  
   public static function getInstance()
   {
       if (self::$instance === null) {
           self::$instance = new self();
       }
       return self::$instance;
   }
  
   public function setData($userData, $token)
   {
       $this->userData = $userData;
       $this->token = $token;
       return $this;
   }
  
   public function getUserId()
   {
       return isset($this->userData->uid) ? $this->userData->uid : null;
   }
  
   public function getToken()
   {
       return $this->token;
   }
  
   public function getUserData()
   {
       return $this->userData;
   }
  
   public function clear()
   {
       $this->userData = null;
       $this->token = null;
       return $this;
   }
}




El servicio con singleton para almacenar los tokens esta bien para una implementación estandar de PHP pero en sistemas de alto rendimiento consideraria la opción de utilizar Redis, dicho esto ...

Cómo funciona este proceso de guardar el token

Cuando se accede a una url protegida con el filtro JWTAuthFilter después de decodificar el token y comprobar que es correcto con esta línea se almacena.

# Almacenar la información del token en el servicio singleton
JWTAuthService::getInstance()->setData($decoded, $token);

Después en el controlador, en el ejemplo en el método profile se obtiene el id del usuario para buscarlo en la bbdd

# Almacenar la información del token en el servicio singleton
$userId = JWTAuthService::getInstance()->getUserId();


En otros métodos utilizaremos otros datos del token


1) Probar la API

Para probar la API podemos utilizar cualquiera de los clientes de API Rest que existen postman, Thunder Client (extensión para visual studio code) o el terminal por curl

Registro de un usuario, enviaremos los siguiente:

Un POST a http://localhost/api/register con los siguientes datos

{
  "email": "usuario@ejemplo.com",
  "username": "usuario",
  "password": "Asdf1234"
}

Y tiene que devolver como salida

{
  "status": 201,
  "message": "Usuario registrado correctamente",
  "user_id": 1
}



Registro de nuevo usuario a través de API

Login de usuario, enviaremos

Un POST a http://localhost/api/login con los siguientes datos

{
  "email": "usuario@ejemplo.com",
  "password": "Asdf1234"
}

Y devuelve lo siguiente

{
  "status": 200,
  "message": "Login correcto",
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3NDI0ODQ4NTYsImV4cCI6MTc0MjQ4ODQ1NiwidWlkIjoxfQ.0Y6vPXLW-eijtQHS6LQe6vv2AoRuD-5iUFAMrk4zD80",
  "user": {
	"id": 1,
	"email": "usuario@ejemplo.com",
	"username": "usuario"
  }
}


Dentro de estos datos que devuelve el login necesitaremos el token para enviarlo con las peticiones que necesitas de validación

Login de un usuario a través de API

Ver el profile de usuario Un GET a http://localhost/api/profile y en el apartado de Auth->Bearer ponemos el token obtenido en la llamada al login

Ver los datos de un usuario a través de API

Y ya tenemos la API funcionando, ahora solo queda ir poniendo funcionalidades.


1) Conclusión

Como hemos visto crear una API con Codeigniter es muy sencillo y rápido.

Es una versión sencilla donde se pueden ir añadiendo mejoras como la del redis que comentaba y nuevas funcionalidad

Todo el código del proyecto está en el repositorio de github


Y esto es todo, feliz programming!!!
Saludos
Alex
:-)
/


Si te ha gustado el artículo compartelo en:      




Añadir un comentarios:

Nombre:
Email: (no se publica el email)




SIGUENOS EN


ARCHIVO

Publicidad

.