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.
Updated 8 min read

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:
| Authenticator | How the client proves identity | Good fit |
|---|---|---|
| Session | Username or email and password, then a session cookie | Server-rendered sites and same-domain single-page apps |
| Access tokens | A long random token sent as Authorization: Bearer ... | Mobile apps, internal tools, third-party developers using your API |
| HMAC SHA256 | A request signature made with a shared secret key | Server-to-server integrations where the secret should never travel with the request |
| JWT | A 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.

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:
composer require codeigniter4/shield
php spark shield:setupThe 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
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:
// 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
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:
$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:
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 401If 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.

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():
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:
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:
$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().
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:
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 = trueso theforcehttpsfilter redirects and sends HSTS. - Apply
auth-ratesto 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
codeigniter4GitHub 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 aftergenerateAccessToken().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.
Your next customer could start right here.
Introduce your tool, product or business to readers exploring technology.
Explore placements and request a private quote
Sources
- Shield: Installation — CodeIgniter Shield
- Shield: Protecting an API with Access Tokens — CodeIgniter Shield
- Shield: Access Token Authenticator — CodeIgniter Shield
- Shield: Mobile Authentication with Access Tokens — CodeIgniter Shield
- Shield: Controller Filters — CodeIgniter Shield
- Shield: Authorization — CodeIgniter Shield
- Shield releases — GitHub
- 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.
More from Cloudsoft Zone

Troubleshooting guideCodeIgniter
CodeIgniter 4 validation not working? 9 causes and fixes
Fix CodeIgniter 4 validation problems fast: wrong input source, lost errors after redirect, permit_empty, strict JSON rules, is_unique placeholders and more.
By Cloudsoft Zone

Industry updateCodeIgniter
CodeIgniter 4.7.5 security release: what to patch and how
CodeIgniter 4.7.5 fixes six advisories, including a critical upload bypass and Parser code execution. See who is exposed and how to upgrade safely.
By Cloudsoft Zone

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
