How to Build a Role-Based Login System in CodeIgniter 4

Role-Based Access Control (RBAC) is a widely used method for regulating access to application resources based on assigned user roles. In a CodeIgniter 4 application, you can define roles such as admin (full access) and user (limited access). This approach is far more structured than a simple login system where every authenticated user has the same privileges.

Here is how roles typically differ:

  • Admin Role: Full CRUD (Create, Read, Update, Delete) operations on all data, user management, and system settings.
  • User Role: Read-only access to content, with the ability to edit their own profile.

In CI4, RBAC is implemented using a users table with a role column (typically an ENUM). Role-based permissions are then enforced through controller filters (middleware) that check the session before allowing access to protected routes.


Why use RBAC in small projects? Even a blog with a few hundred users benefits from structured access control — it prevents unauthorized edits, keeps the admin panel secure, and adds a professional layer of security. However, for ultra-simple sites like a static portfolio, a basic login might suffice. The key is matching complexity to your project's actual needs.


If you are new to CodeIgniter 4 authentication, you may also find our CodeIgniter 4 Authentication Tutorial: Build Login & Registration System from Scratch helpful as a starting point.



Role-Based Login System in CodeIgniter 4: Admin & User Roles – Best for Small Projects?

Table Of Content

1 Prerequisites

  • PHP 8.1+ with PDO (MySQL driver).
  • Database: MySQL/MariaDB.
  • Composer autoloading enabled.
  • Run migrations or create tables manually.

2 How to Build Custom RBAC in CodeIgniter 4

Building a custom RBAC system in CodeIgniter 4 is straightforward for small projects, as it avoids the overhead of third-party authentication packages. Below is a high-level overview of the steps involved — each of which is implemented in detail in the sections that follow.

  1. Database Setup: Use CI4 migrations to create a users table with columns for id, username, email, password (hashed), and role (ENUM: 'admin', 'user').
  2. Model and Controller: Create a UserModel with a method to find users by email, and an AuthController that validates input, verifies passwords using password_verify(), and sets session data including the user's role.
  3. Role-Based Filters: Implement a custom RoleFilter (middleware) that checks the session role before granting access to admin or user routes. This is registered in app/Config/Filters.php and applied via route groups.
  4. Registration and Logout: A registration controller handles signup with role assignment (defaulting to 'user'), and logout destroys the session.

This custom approach typically takes about 200–300 lines of code for a basic setup. For small projects, it provides full control, zero extra dependencies, and is quick to debug. For a deeper comparison of CodeIgniter 4 versus other frameworks for small projects, see our article on CodeIgniter 4 vs Laravel 12: Which PHP Framework is Better for Small Projects?

3 Create / Install a Codeigniter 4 Project

3.1 Install Codeigniter 4 Project

First, make sure your computer has a composer.
Use the following command to install new Codeigniter Project.

composer create-project codeigniter4/appstarter ci4-rbac-app

Then, navigate to your project directory:

cd ci4-rbac-app

3.2 Configure Environment and MySql Database

Rename the env file to .env and set the development mode in the .env file, then configure the database connection:

# CI_ENVIRONMENT = production
CI_ENVIRONMENT = development

Configure your MySQL database connection. CodeIgniter 4 uses the database.default.* format in the .env file:


database.default.hostname = 127.0.0.1
database.default.database = ci4_rbac
database.default.username = root
database.default.password = 
database.default.DBDriver = MySQLi
database.default.DBPrefix =
database.default.port = 3306

Create the database ci4_rbac in your MySQL server before proceeding.

Note: If you are concerned about SQL injection in your CI4 application, check out our guide on How to Secure Your CodeIgniter 4 Application from SQL Injection.

4 Database Setup (Migration, Model & Seeder)

Create a migration for the users table:

php spark make:migration CreateUsersTable

    
<?php
// app/Database/Migrations/2026-01-01-000001_create_users_table.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,
            ],
            'username' => [
                'type'       => 'VARCHAR',
                'constraint' => '100',
            ],
            'email' => [
                'type'       => 'VARCHAR',
                'constraint' => '100',
                'unique'     => true,
            ],
            'password' => [
                'type'       => 'VARCHAR',
                'constraint' => '255',
            ],
            'role' => [
                'type'       => 'ENUM',
                'constraint' => ['admin', 'user'],
                'default'    => 'user',
            ],
            'created_at' => [
                'type' => 'DATETIME',
                'null' => true,
            ],
            'updated_at' => [
                'type' => 'DATETIME',
                'null' => true,
            ],
        ]);
        $this->forge->addKey('id', true);
        $this->forge->createTable('users');
    }

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

Run the migration:

php spark migrate

Create a Model for the users table:

php spark make:model UserModel


<?php
// app/Models/UserModel.php

namespace App\Models;

use CodeIgniter\Model;

class UserModel extends Model
{
    protected $table            = 'users';
    protected $primaryKey       = 'id';
    protected $useAutoIncrement = true;
    protected $returnType       = 'array';
    protected $useSoftDeletes   = false;
    protected $protectFields    = true;
    protected $allowedFields    = ['username', 'email', 'password', 'role'];

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

    // Find user by email
    public function getUserByEmail(string $email)
    {
        return $this->where('email', $email)->first();
    }
}

Seeder for test users:

php spark db:seed UserSeeder


<?php
// app/Database/Seeds/UserSeeder.php

namespace App\Database\Seeds;

use CodeIgniter\Database\Seeder;
use App\Models\UserModel;

class UserSeeder extends Seeder
{
    public function run()
    {
        $model = new UserModel();

        $data = [
            [
                'username' => 'admin',
                'email'    => 'admin@example.com',
                'password' => password_hash('admin123', PASSWORD_DEFAULT),
                'role'     => 'admin',
            ],
            [
                'username' => 'user',
                'email'    => 'user@example.com',
                'password' => password_hash('user123', PASSWORD_DEFAULT),
                'role'     => 'user',
            ],
        ];

        $model->insertBatch($data);
    }
}

Run seeder:

php spark db:seed UserSeeder

5 Create Controllers

Authentication Controller

php spark make:controller AuthController


<?php
// app/Controllers/AuthController.php

namespace App\Controllers;

use App\Models\UserModel;

class AuthController extends BaseController
{
    public function login()
    {
        if ($this->request->getMethod() === 'post') {
            $rules = [
                'email'    => 'required|valid_email',
                'password' => 'required|min_length[6]',
            ];

            if (! $this->validate($rules)) {
                return view('auth/login', ['validation' => $this->validator]);
            }

            $email    = $this->request->getPost('email');
            $password = $this->request->getPost('password');

            $model = new UserModel();
            $user  = $model->getUserByEmail($email);

            if ($user && password_verify($password, $user['password'])) {
                session()->set([
                    'user_id' => $user['id'],
                    'username' => $user['username'],
                    'role'     => $user['role'],
                    'isLoggedIn' => true,
                ]);

                if ($user['role'] === 'admin') {
                    return redirect()->to('/admin/dashboard');
                }
                return redirect()->to('/user/dashboard');
            }

            return view('auth/login', ['error' => 'Invalid credentials']);
        }

        return view('auth/login');
    }

    public function register()
    {
        if ($this->request->getMethod() === 'post') {
            $rules = [
                'username' => 'required|min_length[3]',
                'email'    => 'required|valid_email|is_unique[users.email]',
                'password' => 'required|min_length[6]',
            ];

            if (! $this->validate($rules)) {
                return view('auth/register', ['validation' => $this->validator]);
            }

            $model = new UserModel();
            $data = [
                'username' => $this->request->getPost('username'),
                'email'    => $this->request->getPost('email'),
                'password' => password_hash($this->request->getPost('password'), PASSWORD_DEFAULT),
                'role'     => 'user', // Default role
            ];

            $model->save($data);
            return redirect()->to('/login')->with('success', 'Registration successful!');
        }

        return view('auth/register');
    }

    public function logout()
    {
        session()->destroy();
        return redirect()->to('/login');
    }
}
?>

AdminController Controller

php spark make:controller AdminController


<?php
// app/Controllers/AdminController.php

namespace App\Controllers;

class AdminController extends BaseController
{
    public function dashboard()
    {
        return view('admin/dashboard');
    }
}
?>

UserController Controller

php spark make:controller UserController


<?php
// app/Controllers/UserController.php

namespace App\Controllers;

class UserController extends BaseController
{
    public function dashboard()
    {
        return view('user/dashboard');
    }
}
?>

6 Create Views

Place these files in the correct folders under app/Views/.
Basic Main Layout (recommended) app/Views/layouts/main.php

 <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= esc($title ?? 'Role Based App') ?></title>
    
    <!-- Bootstrap 5 CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    
    <style>
        body { background-color: #f8f9fa; }
        .card { box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
    </style>
</head>
<body>

    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="/">My App</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav ms-auto">
                    <?php if (session()->get('isLoggedIn')): ?>
                        <li class="nav-item">
                            <a class="nav-link" href="<?= base_url('/' . session()->get('role') . '/dashboard') ?>">
                                Dashboard
                            </a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="<?= base_url('/logout') ?>">Logout (<?= esc(session()->get('username')) ?>)</a>
                        </li>
                    <?php else: ?>
                        <li class="nav-item">
                            <a class="nav-link" href="<?= base_url('/login') ?>">Login</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="<?= base_url('/register') ?>">Register</a>
                        </li>
                    <?php endif; ?>
                </ul>
            </div>
        </div>
    </nav>

    <main class="py-4">
        <?= $this->renderSection('content') ?>
    </main>

    <!-- Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Login Page app/Views/auth/login.php

 <?= $this->extend('layouts/main') ?>

<?= $this->section('content') ?>

<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card mt-5">
                <div class="card-header text-center">
                    <h3>Login</h3>
                </div>
                <div class="card-body">

                    <?php if (session()->getFlashdata('success')): ?>
                        <div class="alert alert-success">
                            <?= session()->getFlashdata('success') ?>
                        </div>
                    <?php endif; ?>

                    <?php if (isset($error)): ?>
                        <div class="alert alert-danger">
                            <?= esc($error) ?>
                        </div>
                    <?php endif; ?>

                    <?php if ($validation = session()->getFlashdata('validation')): ?>
                        <div class="alert alert-danger">
                            <?= $validation->listErrors() ?>
                        </div>
                    <?php endif; ?>

                    <form action="<?= base_url('/login') ?>" method="post">
                        <?= csrf_field() ?>

                        <div class="mb-3">
                            <label for="email" class="form-label">Email</label>
                            <input type="email" name="email" id="email" class="form-control" 
                                   value="<?= old('email') ?>" required autofocus>
                        </div>

                        <div class="mb-3">
                            <label for="password" class="form-label">Password</label>
                            <input type="password" name="password" id="password" class="form-control" required>
                        </div>

                        <div class="d-grid">
                            <button type="submit" class="btn btn-primary">Login</button>
                        </div>
                    </form>

                    <div class="text-center mt-3">
                        Don't have an account? <a href="<?= base_url('/register') ?>">Register here</a>
                    </div>

                </div>
            </div>
        </div>
    </div>
</div>

<?= $this->endSection() ?>

Register Page app/Views/auth/register.php

 <?= $this->extend('layouts/main') ?>

<?= $this->section('content') ?>

<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card mt-5">
                <div class="card-header text-center">
                    <h3>Register</h3>
                </div>
                <div class="card-body">

                    <?php if ($validation = session()->getFlashdata('validation')): ?>
                        <div class="alert alert-danger">
                            <?= $validation->listErrors() ?>
                        </div>
                    <?php endif; ?>

                    <form action="<?= base_url('/register') ?>" method="post">
                        <?= csrf_field() ?>

                        <div class="mb-3">
                            <label for="username" class="form-label">Username</label>
                            <input type="text" name="username" id="username" class="form-control" 
                                   value="<?= old('username') ?>" required>
                        </div>

                        <div class="mb-3">
                            <label for="email" class="form-label">Email</label>
                            <input type="email" name="email" id="email" class="form-control" 
                                   value="<?= old('email') ?>" required>
                        </div>

                        <div class="mb-3">
                            <label for="password" class="form-label">Password</label>
                            <input type="password" name="password" id="password" class="form-control" required>
                        </div>

                        <div class="d-grid">
                            <button type="submit" class="btn btn-success">Register</button>
                        </div>
                    </form>

                    <div class="text-center mt-3">
                        Already have an account? <a href="<?= base_url('/login') ?>">Login here</a>
                    </div>

                </div>
            </div>
        </div>
    </div>
</div>

<?= $this->endSection() ?>

Admin Dashboard (example) app/Views/admin/dashboard.php

 <?= $this->extend('layouts/main') ?>

<?= $this->section('content') ?>

<div class="container">
    <div class="row">
        <div class="col-12">
            <div class="card mt-4">
                <div class="card-header bg-primary text-white">
                    <h4>Admin Dashboard</h4>
                </div>
                <div class="card-body">
                    <h5>Welcome, <?= esc(session()->get('username')) ?>!</h5>
                    <p class="lead">You have full administrative access.</p>

                    <div class="alert alert-info">
                        <strong>Admin Features:</strong> Manage users, content, settings, reports, etc.
                    </div>

                    <a href="<?= base_url('/logout') ?>" class="btn btn-outline-danger">Logout</a>
                </div>
            </div>
        </div>
    </div>
</div>

<?= $this->endSection() ?>

User Dashboard (example) app/Views/user/dashboard.php

 <?= $this->extend('layouts/main') ?>

<?= $this->section('content') ?>

<div class="container">
    <div class="row">
        <div class="col-12">
            <div class="card mt-4">
                <div class="card-header bg-success text-white">
                    <h4>User Dashboard</h4>
                </div>
                <div class="card-body">
                    <h5>Hello, <?= esc(session()->get('username')) ?>!</h5>
                    <p class="lead">This is your personal dashboard.</p>

                    <div class="alert alert-info">
                        <strong>User Features:</strong> View profile, change password, see your content...
                    </div>

                    <a href="<?= base_url('/logout') ?>" class="btn btn-outline-danger">Logout</a>
                </div>
            </div>
        </div>
    </div>
</div>

<?= $this->endSection() ?>

Access Denied Page app/Views/errors/access_denied.php

 <?= $this->extend('layouts/main') ?>

<?= $this->section('content') ?>

<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8 text-center mt-5">
            <h1 class="display-4 text-danger">Access Denied</h1>
            <p class="lead">Sorry, you don't have permission to view this page.</p>
            
            <?php if (session()->get('isLoggedIn')): ?>
                <p>You are logged in as <strong><?= esc(session()->get('role')) ?></strong>.</p>
            <?php endif; ?>

            <a href="<?= base_url('/') ?>" class="btn btn-primary btn-lg">Go to Home</a>
            <a href="<?= base_url('/logout') ?>" class="btn btn-outline-secondary btn-lg ms-2">Logout</a>
        </div>
    </div>
</div>

<?= $this->endSection() ?>

These views use Bootstrap 5 for clean, responsive design.

7 Role Filter (Middleware)

    

        // app/Filters/RoleFilter.php
        <?php

        namespace App\Filters;

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

        class RoleFilter implements FilterInterface
        {
            public function before(RequestInterface $request, $arguments = null)
            {
                if (! session()->get('isLoggedIn')) {
                    return redirect()->to('/login');
                }

                if ($arguments) {
                    $allowedRoles = is_array($arguments) ? $arguments : [$arguments];
                    $userRole = session()->get('role');

                    if (! in_array($userRole, $allowedRoles)) {
                        return redirect()->to('/access-denied');
                    }
                }
            }

            public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
            {
                // Nothing needed here
            }
        }

Register filter in app/Config/Filters.php:
    
    public $aliases = [
        // ...
        'role' => \App\Filters\RoleFilter::class,
    ];
    

8 Define a Route



// app/Config/Routes.php
$routes->get('/login', 'AuthController::login');
$routes->post('/login', 'AuthController::login');
$routes->get('/register', 'AuthController::register');
$routes->post('/register', 'AuthController::register');
$routes->get('/logout', 'AuthController::logout');

// Admin routes
$routes->group('admin', ['filter' => 'role:admin'], function($routes) {
    $routes->get('dashboard', 'AdminController::dashboard');
    // Add more admin routes...
});

// User routes
$routes->group('user', ['filter' => 'role:user'], function($routes) {
    $routes->get('dashboard', 'UserController::dashboard');
    // Add more user routes...
});

$routes->get('/access-denied', function() {
    return view('errors/access_denied');
});

9 Folder Structure

10 Run and Test

Start the built-in development server using the following command:

php spark serve

Visit http://localhost:8080/login in your browser.

Test login credentials:

  • Admin: admin@example.com / admin123 → Redirects to /admin/dashboard
  • User: user@example.com / user123 → Redirects to /user/dashboard

Try accessing /admin/dashboard while logged in as a regular user — you should be redirected to the Access Denied page. This confirms the RoleFilter middleware is working correctly.

Why This Approach Works Well for Small Projects:

  • Lightweight: No extra Composer packages required beyond the CI4 framework itself.
  • Secure: Uses PHP's built-in password_hash() and password_verify() for bcrypt hashing.
  • Extensible: Easy to add more roles, CSRF protection, remember-me tokens, or password reset flows.
  • Fast Setup: The entire authentication system is under 300 lines of core code.

For tips on optimizing your CI4 application's speed and performance, read our guide on Boost CodeIgniter 4 Performance and Page Speed.

11 Conclusion

A role-based login system in CodeIgniter 4 is an excellent choice for small projects. It provides structured access control, uses secure password hashing out of the box, and leverages CI4's built-in session and filter systems — all without adding third-party dependencies.

That said, for projects where security is mission-critical or where you need advanced features like two-factor authentication, email verification, or OAuth, a mature library like CodeIgniter Shield is the safer bet. Shield handles edge cases and security best practices that are easy to miss in a custom build.

When to choose each approach:

  • Custom RBAC: Best for small internal tools, simple blogs, prototypes, or learning projects where you want full control and minimal overhead.
  • Shield or similar library: Better for production apps with public-facing users, compliance requirements, or when you are not confident handling security edge cases yourself.

Whichever path you choose, CodeIgniter 4's lightweight architecture ensures your authentication layer won't become a performance bottleneck.


Continue Learning:

If you found this tutorial helpful, explore more CodeIgniter 4 guides on our site:

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

A role-based login system in CodeIgniter 4 is an authentication mechanism that not only verifies a user's identity (via email and password) but also assigns them a specific role — such as admin or user. Each role determines what features and pages the user can access. This is implemented using session data, database role columns, and CI4 controller filters (middleware) that check permissions before granting access to protected routes.

Yes, CodeIgniter 4 is an excellent choice for small projects. Its lightweight architecture (under 2MB framework size), minimal configuration requirements, and fast execution speed make it ideal for small-scale applications like blogs, internal dashboards, and simple e-commerce sites. Unlike heavier frameworks, CI4 does not require extensive setup or numerous dependencies to get started with features like authentication.

It depends on your project size and security requirements. Use CodeIgniter Shield if you need battle-tested security features like two-factor authentication, email verification, password resets, and brute-force protection out of the box. Choose a custom login system for very small projects (under 500 users) where you want minimal overhead, full control over the code, and a simpler architecture without extra Composer dependencies.

To implement RBAC in CodeIgniter 4: (1) Create a users table with a role column (ENUM of admin, user) using CI4 migrations. (2) Build a UserModel with a method to find users by email. (3) Create an AuthController that validates credentials, verifies passwords with password_verify(), and stores the role in the session. (4) Write a custom RoleFilter (middleware) that checks session roles before allowing access. (5) Register the filter in app/Config/Filters.php and apply it to route groups in app/Config/Routes.php.

Custom authentication carries several risks if not implemented carefully: weak password hashing (always use PASSWORD_DEFAULT with password_hash()), missing brute-force protection (no rate limiting on login attempts), lack of CSRF token validation on forms, improper session management (session fixation attacks), and no protection against timing attacks during password comparison. For production applications, consider using an established library like CodeIgniter Shield that addresses these concerns by default.

To add more roles, update the ENUM constraint in your migration to include the new roles (e.g., admin, editor, moderator, user). Then update your RoleFilter to accept multiple role arguments — the filter already supports this with the in_array() check. In your routes, apply the filter like: filter => role:admin,editor to allow both admins and editors access to specific route groups.

Yes, CodeIgniter Shield is a free, open-source authentication and authorization library officially maintained by the CodeIgniter Foundation. It is installed via Composer with the command: composer require codeigniter4/shield. It provides login, registration, role/permission management, two-factor authentication, and more.

Yes, but for API-based authentication you would typically replace session-based auth with token-based auth (such as JWT or API keys). The role-checking logic in the RoleFilter can remain the same — instead of reading from the session, you would decode the token and extract the user role. See our CodeIgniter 4 REST API CRUD tutorial for a related example.