Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
CodeIgniterDevelopmentPHPTutorial

Protect a CodeIgniter 4 API with Shield access tokens

Install CodeIgniter Shield, issue hashed access tokens at login, protect API routes with the tokens filter, restrict actions with scopes and permissions, and revoke tokens on logout, with code checked against current docs.

Cloudsoft Zone

Updated 8 min read

Closed laptop wrapped in an orange chain and padlock on a blue textured background
Photograph: Richard Nowell / CC BY-SA 4.0, via Wikimedia Commons
In this article

An API without authentication is a public database. For CodeIgniter 4 projects, the framework team maintains an official answer: CodeIgniter Shield, an authentication and authorisation library that the CodeIgniter security guidelines point to under several OWASP categories. Shield can authenticate browser sessions, personal access tokens, HMAC-signed requests and JSON Web Tokens, and it ships with groups, permissions and ready-made route filters.

This guide focuses on the option that suits most first-party mobile apps and partner integrations: access tokens. You will install Shield, issue a token when a user logs in, protect API routes with the tokens filter, restrict individual endpoints with token scopes and group permissions, and revoke tokens on logout. Everything here was checked against the Shield documentation and the CodeIgniter 4.7.4 user guide in October 2026.

The examples build on a books API like the one in our CodeIgniter 4.7 REST API tutorial, but they apply to any controller.

Choosing a Shield authenticator

Shield provides four built-in authenticators. Picking the right one first saves rework later:

AuthenticatorHow the client proves identityGood fit
SessionUsername or email and password, then a session cookieServer-rendered sites and same-domain single-page apps
Access tokensA long random token sent as Authorization: Bearer ...Mobile apps, internal tools, third-party developers using your API
HMAC SHA256A request signature made with a shared secret keyServer-to-server integrations where the secret should never travel with the request
JWTA signed JSON Web Token (needs extra setup)Systems that already use JWTs or need stateless tokens with built-in expiry

Shield's access tokens are stored in your database as SHA-256 hashes, so a leaked database dump does not reveal usable tokens. Because they are database records, revoking one takes effect immediately, which is harder to achieve with self-contained JWTs. Shield also offers a chain filter that accepts either a session or a token, useful when the same endpoint serves a browser front end and a mobile app.

Illustration of a blue padlock over a printed circuit board pattern
Pick the authenticator first: session, access token, HMAC or JWT.

Install and set up Shield

Shield's own requirements are modest (PHP 8.1+, CodeIgniter 4.3.5 or later, Composer, and InnoDB rather than MyISAM if you use MySQL), but if you are on CodeIgniter 4.7 you already need PHP 8.2. From your project root:

Shell
composer require codeigniter4/shield
php spark shield:setup

The shield:setup command automates the manual setup steps in the Shield documentation: it publishes Auth.php, AuthGroups.php and AuthToken.php into app/Config/, adds the auth and setting helpers to autoloading, registers the authentication routes with service('auth')->routes($routes);, and runs the database migrations. If you want custom table names, change them before migrating. Confirm the tables exist with php spark migrate:status.

Two follow-up settings matter. If you also use the session authenticator, the Shield docs recommend setting Config\Security::$csrfProtection to 'session'. And configure app/Config/Email.php so Shield can send activation and magic-link emails.

Create a user to test with

Shield stores credentials in a separate identities table, so create users through its user provider rather than by inserting rows yourself. For a quick test, a small Spark command or a seeder can run this:

PHP
<?php

use CodeIgniter\Shield\Entities\User;

$users = auth()->getProvider();

$user = new User([
    'username' => 'api-tester',
    'email'    => 'tester@example.com',
    'password' => 'use-a-long-unique-password-here',
]);
$users->save($user);

// Reload to get the ID, then add the user to the default group
$user = $users->findById($users->getInsertID());
$users->addToDefaultGroup($user);

Issue a token at login

A mobile app sends the user's email, password and a device name once, receives a token, and stores it securely on the device. Add a route outside the protected group:

PHP
// app/Config/Routes.php
$routes->post('auth/token', '\App\Controllers\Auth\TokenController::issue', ['filter' => 'auth-rates']);

The auth-rates filter is Shield's built-in rate limiter for authentication routes, which the docs recommend for every login and registration endpoint. Now the controller, based on the pattern in Shield's mobile apps guide:

PHP
<?php

namespace App\Controllers\Auth;

use App\Controllers\BaseController;
use CodeIgniter\API\ResponseTrait;

class TokenController extends BaseController
{
    use ResponseTrait;

    public function issue()
    {
        $rules = [
            'email'       => config('Auth')->emailValidationRules,
            'password'    => ['label' => 'Auth.password', 'rules' => 'required'],
            'device_name' => ['label' => 'Device name', 'rules' => 'required|string|max_length[100]'],
        ];

        if (! $this->validateData($this->request->getPost(), $rules, [], config('Auth')->DBGroup)) {
            return $this->failValidationErrors($this->validator->getErrors());
        }

        $result = auth()->attempt([
            'email'    => $this->request->getPost('email'),
            'password' => $this->request->getPost('password'),
        ]);

        if (! $result->isOK()) {
            return $this->failUnauthorized('Invalid credentials.');
        }

        $token = auth()->user()->generateAccessToken(
            (string) $this->request->getPost('device_name'),
            ['books.read', 'books.write'],
        );

        return $this->respondCreated(['token' => $token->raw_token]);
    }
}

The plain-text token is available only in the raw_token property of the object returned by generateAccessToken(). After this response it cannot be retrieved again, because only the hash is stored. If a user loses it, they generate a new one. Returning a generic Invalid credentials. message, rather than the specific reason, avoids telling an attacker whether an email address is registered.

Protect API routes with the tokens filter

Shield registers its filter aliases automatically (session, tokens, hmac, chain, jwt, auth-rates, group, permission and force-reset), so you do not add them to app/Config/Filters.php. Wrap the API group:

PHP
$routes->group('api', ['namespace' => 'App\Controllers\Api', 'filter' => 'tokens'], static function ($routes) {
    $routes->resource('books', ['placeholder' => '(:num)', 'except' => 'new,edit']);
});

When a request arrives, the filter reads the Authorization header, hashes the bearer value, looks it up and, if it matches an unexpired token, makes the user available through auth()->user(). A missing or invalid token is rejected before your controller runs. You can test it:

Shell
TOKEN=$(curl -s -X POST http://localhost:8080/auth/token \
  -d email=tester@example.com -d password='use-a-long-unique-password-here' \
  -d device_name='curl test' | php -r 'echo json_decode(stream_get_contents(STDIN))->token;')

curl -s http://localhost:8080/api/books -H "Authorization: Bearer $TOKEN"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/api/books   # expect 401

If your project enables the csrf filter globally, exclude the token routes (for example 'except' => ['api/*', 'auth/token']). CSRF protects cookie-based sessions; bearer-token requests do not rely on cookies.

Blue and yellow network cables plugged into the ports of a rack-mounted network switch
Bearer tokens travel with every request, so serve token-protected APIs over HTTPS only.

Limit what each token can do

Authentication answers "who is this?"; you still need to decide what they may do. Shield gives you two layers.

Token scopes

Scopes are permission strings attached to a token when it is created. They cannot be changed afterwards. If you pass no scopes, the token receives the wildcard * and can do everything, which is rarely what you want for a third-party key. Check a scope in code with tokenCan():

PHP
public function create()
{
    if (auth()->user()->tokenCant('books.write')) {
        return $this->failForbidden('This token cannot create books.');
    }

    // ... validation and insert as before
}

You can also put a scope on a route filter, such as ['filter' => 'tokens:books.write']. The Shield docs note two limits: only the first scope in a filter is checked, and scope names should not contain a colon.

Groups and permissions

Scopes describe the token; permissions describe the user. Define permissions and map them to groups in app/Config/AuthGroups.php. Add these entries to the existing $permissions and $matrix arrays rather than replacing Shield's defaults:

PHP
public array $permissions = [
    'books.read'   => 'Can read books',
    'books.write'  => 'Can create and edit books',
    'books.delete' => 'Can delete books',
];

public array $matrix = [
    'superadmin' => ['books.*'],
    'editor'     => ['books.read', 'books.write'],
    'user'       => ['books.read'],
];

Then combine both checks in sensitive actions, so a token is only as powerful as the user behind it:

PHP
$user = auth()->user();

if (! $user->can('books.delete') || $user->tokenCant('books.delete')) {
    return $this->failForbidden();
}

On CodeIgniter 4.7 you can attach the same checks with PHP attributes instead of route options, for example #[Filter(by: 'permission', having: ['books.delete'])] on the delete() method. Pick one style per project; the user guide warns that applying a filter both by attribute and in the filter configuration can lead to unexpected results.

Expiry, lifetime and revocation

Shield distinguishes two ideas. The lifetime ($unusedTokenLifetime in app/Config/AuthToken.php, one year by default) expires a token that has not been used for that long. An expiration date is optional and fixed: pass a Time object as the third argument to generateAccessToken(), or change it later with updateAccessTokenExpiration().

PHP
use CodeIgniter\I18n\Time;

$token = $user->generateAccessToken('Reporting script', ['books.read'], Time::now()->addMonths(3));

For logout, revoke the token the client is using. Revocation deletes the database record, so the next request with that token fails:

PHP
public function logout()
{
    $header   = $this->request->getHeaderLine('Authorization');
    $rawToken = trim(substr($header, strlen('Bearer ')));

    auth()->user()->revokeAccessToken($rawToken);

    return $this->respondNoContent();
}

revokeAllAccessTokens() signs a user out of every device, which is the right response to a password change or a suspected compromise. By default Shield records only failed token logins in the auth_token_logins table; change $recordLoginAttempt if you need a full audit trail.

Security checklist for token-protected APIs

  • Serve the API over HTTPS only; set Config\App::$forceGlobalSecureRequests = true so the forcehttps filter redirects and sends HSTS.
  • Apply auth-rates to every authentication route.
  • Issue tokens with the narrowest scopes that work; avoid the default wildcard for third-party keys.
  • Check record ownership in controllers. A valid token for user A must not read user B's data (OWASP API1:2023).
  • Set an expiration date for tokens given to scripts and partners.
  • Revoke all tokens on password reset and when an account is banned.
  • Keep Shield and the framework on current releases; Shield 1.4.1 was the latest release on GitHub when this was written.

Summary

With composer require codeigniter4/shield, one setup command and a few lines of routing, a CodeIgniter 4 API gains hashed, revocable access tokens, scope checks and group-based permissions. Start with the tokens filter on the whole API group, add scope and permission checks to write and delete actions, and rate-limit the login route. If you later need browser sessions on the same endpoints, switch the group filter to chain rather than building a second authentication path.

Frequently asked questions

  • Is CodeIgniter Shield official?

    Yes. Shield is maintained in the codeigniter4 GitHub organisation and the CodeIgniter security guidelines refer to it as the official authentication and authorisation framework.

  • Should I use access tokens or JWT with Shield?

    Access tokens are simpler and can be revoked immediately because they live in your database. Shield's JWT authenticator needs extra setup and suits systems that already rely on JWTs or want self-contained tokens.

  • Can I see a token again after creating it?

    No. Shield stores only a SHA-256 hash. The plain token is available once, in raw_token, immediately after generateAccessToken().

  • How long do Shield access tokens last?

    By default a token expires after one year without use ($unusedTokenLifetime). You can also give a token a fixed expiration date when you create it.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. Shield: Installation — CodeIgniter Shield
  2. Shield: Protecting an API with Access Tokens — CodeIgniter Shield
  3. Shield: Access Token Authenticator — CodeIgniter Shield
  4. Shield: Mobile Authentication with Access Tokens — CodeIgniter Shield
  5. Shield: Controller Filters — CodeIgniter Shield
  6. Shield: Authorization — CodeIgniter Shield
  7. Shield releases — GitHub
  8. Controller Attributes — CodeIgniter Foundation

Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.

Comments

No comments yet. Be the first to join the conversation.

Leave a comment

Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.

  • Plush elePHPant toy from Heroku, a version of the PHP community mascot, photographed close up

    Laravel

    Upgrading from Laravel 12 to Laravel 13: a practical guide

    Laravel 12 lost bug-fix support in August 2026. This guide covers the PHP 8.3 requirement, Composer changes, the CSRF middleware rename, cache and session hardening, low-impact changes that often bite, and a safe rollout plan.

    By Cloudsoft Zone