🛡️ Security Filters¶
Filters are the cornerstone of security in Daycry Auth. This complete guide will teach you how to use each available filter.
📋 Filter Index¶
- 🔐 Authentication Filters
- 👥 Authorization Filters
- 🔗 Chain Filters
- 📊 Control Filters
- 🛠️ Advanced Configuration
- 🎯 Practical Examples
🔧 Initial Setup¶
Filter aliases are auto-registered¶
You do not register the auth filter aliases yourself. They are contributed
automatically by Daycry\Auth\Config\Registrar::Filters() when the package is
installed, so they are available in app/Config/Filters.php out of the box:
| Alias | Class | Purpose |
|---|---|---|
auth |
AuthFilter |
Authenticate (default authenticator, or one passed as an argument). |
basic-auth |
BasicAuthFilter |
HTTP Basic authentication. |
chain |
ChainAuthFilter |
Try each authenticator in $authenticationChain order. |
group |
GroupFilter |
Require group membership (group:admin,editor). |
permission |
PermissionFilter |
Require a permission (permission:users.edit). |
gate |
GateFilter |
Authorize via a Gate ability / Policy. |
token-scope |
TokenScopeFilter |
Require an access-token scope. |
rates |
RatesFilter |
Per-IP/user rate limiting (rates:50,MINUTE). |
force-reset |
ForcePasswordResetFilter |
Force a password change. |
password-age |
PasswordAgeFilter |
Require a password younger than the configured max age. |
password-confirm |
PasswordConfirmFilter |
Require recent password re-confirmation. |
Selecting an authenticator. There is no per-authenticator alias such as
session/tokens/jwt. Pass the authenticator name as an argument to theauthfilter instead:auth:session,auth:access_token, orauth:jwt. With no argument,authusesConfig\Auth::$defaultAuthenticator.
To enable a filter globally, reference the alias (and optional arguments) in
app/Config/Filters.php:
public array $globals = [
'before' => [
// 'rates', // Global rate limiting using the configured defaults
],
'after' => [],
];
🔐 Authentication Filters¶
1. Session Filter (session)¶
Verifies that the user is authenticated via session.
Basic Usage¶
// In routes
$routes->group('dashboard', ['filter' => 'auth:session'], function($routes) {
$routes->get('/', 'Dashboard::index');
$routes->get('profile', 'Dashboard::profile');
});
// In controller
class Dashboard extends BaseController
{
protected $filters = ['auth:session'];
public function index()
{
// User guaranteed as authenticated
$user = auth()->user();
return view('dashboard', ['user' => $user]);
}
}
Advanced Configuration¶
// Apply only to specific methods
protected $filters = [
'session' => ['except' => ['public_method']]
];
// With additional parameters
$routes->get('admin', 'Admin::index', ['filter' => 'session:admin']);
2. Access Token Filter (tokens)¶
Verifies authentication via access tokens.
Usage in APIs¶
// API Routes
$routes->group('api/v1', ['filter' => 'auth:access_token'], function($routes) {
$routes->get('users', 'API\Users::index');
$routes->post('users', 'API\Users::create');
$routes->resource('posts', ['controller' => 'API\Posts']);
});
// In API controller
class UsersAPI extends ResourceController
{
protected $filters = ['auth:access_token'];
public function index()
{
// Token automatically validated
$user = auth('access_token')->user();
return $this->respond([
'user' => $user,
'data' => $this->model->findAll()
]);
}
}
Required Headers¶
// In AJAX/API requests
fetch('/api/v1/users', {
headers: {
'X-API-KEY': 'your-access-token-here',
'Content-Type': 'application/json'
}
});
3. JWT Filter (jwt)¶
Verifies JWT tokens in the Authorization header.
Configuration¶
// API with JWT
$routes->group('api/jwt', ['filter' => 'auth:jwt'], function($routes) {
$routes->get('profile', 'API\Profile::show');
$routes->put('profile', 'API\Profile::update');
});
JWT Headers¶
// Request with JWT
fetch('/api/jwt/profile', {
headers: {
'Authorization': 'Bearer your-jwt-token-here',
'Content-Type': 'application/json'
}
});
4. Basic Auth Filter (basic-auth)¶
HTTP Basic authentication (RFC 7617). Reads Authorization: Basic base64(user:pass), verifies the credentials against the user provider, and on success logs the user in via the session authenticator. Designed for machine-to-machine endpoints (cron jobs, health checks, webhooks, internal tooling) where managing tokens or sessions is overkill.
Configuration¶
// app/Config/Auth.php — realm shown by browsers / cached by clients.
public string $basicAuthRealm = 'My App API';
Routes¶
// Persist the auth into the session for the rest of the request lifecycle.
$routes->group('cron', ['filter' => 'basic-auth'], function ($routes) {
$routes->get('purge-old-tokens', 'Maintenance::purgeOldTokens');
});
// Stateless: re-verify credentials on every request, do not write to session.
$routes->group('api/internal', ['filter' => 'basic-auth:once'], function ($routes) {
$routes->get('health', 'Health::index');
});
Behaviour¶
| Scenario | Response |
|---|---|
Missing Authorization header |
401 Unauthorized + WWW-Authenticate: Basic realm="..." |
Wrong scheme (e.g. Bearer) |
401 + challenge header |
| Malformed base64 / no colon | 401 + challenge header |
| Unknown user / wrong password | 401 + challenge header (no info leak) |
| Valid credentials | Logs the user in, request proceeds |
The identifier is matched as email when it parses as a valid email address (filter_var(..., FILTER_VALIDATE_EMAIL)), otherwise as username.
Use cases¶
- Cron / scheduled jobs that hit an internal endpoint:
- Health checks from monitoring systems (Prometheus blackbox, Pingdom).
- Webhooks from third-party services that only support Basic auth.
- Local CLI tooling against a deployed API.
Don't use Basic auth on user-facing endpoints. Browsers prompt for credentials with native (ugly) modals and there is no logout flow. For interactive auth use the
sessionfilter; for APIs usetokens/jwt.
5. Chain Filter (chain)¶
Tries multiple authentication methods in order.
Configuration¶
// In Auth.php
public array $authenticationChain = [
'session', // First session
'access_token', // Then access token
'jwt', // Finally JWT
];
// Usage in routes
$routes->group('hybrid', ['filter' => 'chain'], function($routes) {
$routes->get('data', 'Hybrid::getData'); // Accepts any auth method
});
Practical Example¶
class HybridController extends BaseController
{
protected $filters = ['chain'];
public function getData()
{
// Works with session, token or JWT
$user = auth()->user();
// Detect authentication method used
$authMethod = 'unknown';
if (auth('session')->loggedIn()) $authMethod = 'session';
elseif (auth('access_token')->loggedIn()) $authMethod = 'token';
elseif (auth('jwt')->loggedIn()) $authMethod = 'jwt';
return $this->respond([
'user' => $user,
'auth_method' => $authMethod,
'data' => 'sensitive data here'
]);
}
}
👥 Authorization Filters¶
1. Group Filter (group)¶
Verifies that the user belongs to one or more groups.
Basic Usage¶
// Single group
$routes->group('admin', ['filter' => 'auth:session,group:admin'], function($routes) {
$routes->get('/', 'Admin::dashboard');
$routes->get('users', 'Admin::users');
});
// Multiple groups (OR - any of them)
$routes->get('moderator-panel', 'Moderator::panel', [
'filter' => 'auth:session,group:admin,moderator'
]);
// In controller
class AdminController extends BaseController
{
protected $filters = [
'session',
'group:admin,super-admin' // Any of these groups
];
}
Hierarchical Groups¶
// Configure hierarchy in Database Seeder
$groups = [
'super-admin' => ['permissions' => ['*']],
'admin' => ['permissions' => ['admin.*']],
'moderator' => ['permissions' => ['content.*']],
'user' => ['permissions' => ['user.profile']]
];
// Usage with hierarchy
$routes->group('management', ['filter' => 'auth:session,group:admin'], function($routes) {
$routes->get('/', 'Management::index');
// Only super-admin
$routes->group('system', ['filter' => 'group:super-admin'], function($routes) {
$routes->get('settings', 'Management::systemSettings');
});
});
2. Permission Filter (permission)¶
Verifies specific granular permissions.
Basic Usage¶
// Specific permission
$routes->get('admin/users/edit/(:num)', 'Admin\Users::edit/$1', [
'filter' => 'auth:session,permission:users.edit'
]);
// Multiple permissions (AND - must have all)
$routes->delete('admin/users/(:num)', 'Admin\Users::delete/$1', [
'filter' => 'auth:session,permission:users.delete,users.manage'
]);
// In controller
class UserManagement extends BaseController
{
protected $filters = [
'session',
'permission:users.view' => ['except' => ['index']],
'permission:users.edit' => ['only' => ['edit', 'update']],
'permission:users.delete' => ['only' => ['delete']],
];
}
Granular Permission System¶
// Example permission structure
$permissions = [
// User management
'users.view',
'users.create',
'users.edit',
'users.delete',
'users.manage',
// Content management
'content.view',
'content.create',
'content.edit',
'content.publish',
'content.delete',
// System settings
'system.settings',
'system.backups',
'system.logs',
];
// Usage in specific routes
$routes->group('admin/content', ['filter' => 'auth:session'], function($routes) {
$routes->get('/', 'Content::index', ['filter' => 'permission:content.view']);
$routes->get('create', 'Content::create', ['filter' => 'permission:content.create']);
$routes->post('store', 'Content::store', ['filter' => 'permission:content.create']);
$routes->get('(:num)/edit', 'Content::edit/$1', ['filter' => 'permission:content.edit']);
$routes->put('(:num)', 'Content::update/$1', ['filter' => 'permission:content.edit']);
$routes->delete('(:num)', 'Content::destroy/$1', ['filter' => 'permission:content.delete']);
});
3. Gate Filter (gate)¶
Authorizes a request against a Gate ability — a closure rule registered with
Gate::define() or a class-based policy registered with Gate::policy(). Apply it on
routes that map cleanly to a single ability without a resource argument
(gate:dashboard.view, gate:billing.access). For abilities that need a resource
instance, call the Gate API inside the controller (Gate::authorize('post.update', $post)).
// Single ability
$routes->get('admin', 'Admin::index', ['filter' => 'auth:session,gate:admin.access']);
// Multiple abilities — AND-ed (every ability must allow)
$routes->get('billing', 'Billing::index', ['filter' => 'auth:session,gate:billing.view,billing.manage']);
Gate → RBAC fallback¶
The gate filter honors the Gate → RBAC fallback. When an ability looks like an RBAC
permission — it contains a scope separator, e.g. users.edit — and there is no
registered closure or policy for it, the Gate defers to the authenticated user's RBAC
permissions via User::can(). This lets gate:users.edit and permission:users.edit
share the same semantics.
| Setting | Default | Meaning |
|---|---|---|
AuthSecurity::$gateFallbackToRbac |
true |
A scoped ability (users.edit) with no registered closure/policy falls back to User::can(). Set false to keep the Gate and RBAC systems fully independent (such abilities then simply deny). |
The fallback only applies to abilities that contain a . scope separator and only when a
User is authenticated. Explicit closures and policies always take precedence over the
RBAC fallback.
Failure response¶
gate extends AbstractAuthFilter, so a denied request reuses
buildDeniedResponse() and redirects to Auth::permissionDeniedRedirect() (or returns a
403 JSON body for API requests).
4. Token Scope Filter (token-scope)¶
Validates that the access token used to authenticate the request grants every scope listed in the filter argument. Only meaningful after a token-based authenticator has run (tokens, jwt, or chain).
// Single scope — token must grant `posts.read`
$routes->get('api/posts', 'Posts::index', [
'filter' => 'auth:access_token,token-scope:posts.read',
]);
// Multiple scopes — AND-ed (token must grant BOTH)
$routes->post('api/posts', 'Posts::create', [
'filter' => 'auth:access_token,token-scope:posts.read,posts.write',
]);
How scopes are matched¶
Scopes live on the AccessToken entity (the extra column, mapped via the scopes datamap). The filter calls AccessToken::can($scope) for each requested scope:
| Stored scopes | Filter argument | Result |
|---|---|---|
['posts.read'] |
posts.read |
✅ allow |
['posts.read'] |
posts.write |
❌ deny |
['posts.read', 'posts.write'] |
posts.read,posts.write |
✅ allow |
['*'] (wildcard) |
anything | ✅ allow |
[] |
any | ❌ deny |
Generating scoped tokens¶
$token = $user->generateAccessToken('mobile-app', ['posts.read', 'posts.write']);
echo $token->raw_token; // give to the client once
Failure response¶
token-scope reuses AbstractAuthFilter::buildDeniedResponse():
- API requests (
Accept: application/json) →403 ForbiddenJSON. - Web requests → redirect to
Auth::permissionDeniedRedirect()with a flash error.
Tip: prefer
token-scopeoverpermission:for API tokens — it scopes the token, not the user. A user withposts.writepermission can still hold a read-only token.
🔗 Chain Filters¶
Advanced Chain Configuration¶
// In Auth.php - order matters
public array $authenticationChain = [
'session', // Fastest, for web users
'access_token', // For external APIs
'jwt', // For SPAs and mobile
];
// Custom chain per route
$routes->group('api/mobile', [
'filter' => 'chain:jwt,access_token' // Only JWT and tokens
], function($routes) {
$routes->resource('posts');
});
Hybrid API Example¶
class HybridAPI extends ResourceController
{
protected $filters = ['chain'];
public function before(RequestInterface $request, $arguments = null)
{
// Specific logic based on auth method
$response = parent::before($request, $arguments);
if (auth('jwt')->loggedIn()) {
// JWT-specific configuration
$this->format = 'json';
} elseif (auth('session')->loggedIn()) {
// Web session configuration
$this->format = 'html';
}
return $response;
}
}
📊 Control Filters¶
1. Auth Rates Filter (rates)¶
Request rate control per user/IP. The registered alias is rates (there is no
auth-rates alias).
Global Configuration¶
// In app/Config/Filters.php
public array $globals = [
'before' => [
'rates', // Apply the global defaults to all routes
],
];
// In app/Config/AuthSecurity.php
public string $limitMethod = 'METHOD_NAME'; // IP_ADDRESS | USER | METHOD_NAME | ROUTED_URL
public int $requestLimit = 10; // requests allowed per window
public int $timeLimit = MINUTE; // window length (seconds)
| Setting | Default | Meaning |
|---|---|---|
AuthSecurity::$limitMethod |
'METHOD_NAME' |
Bucket key: IP_ADDRESS, USER, METHOD_NAME, or ROUTED_URL. |
AuthSecurity::$requestLimit |
10 |
Requests allowed per window (used when no per-route/endpoint override). |
AuthSecurity::$timeLimit |
MINUTE |
Window length in seconds. |
Per-route arguments: rates:<limit>,<period>¶
The filter now honors per-route arguments that override the global limit/time for that route only:
// API-specific rate limiting: 50 requests per minute on this group.
$routes->group('api', ['filter' => 'rates:50,MINUTE'], function($routes) {
$routes->resource('users');
});
// In a controller with custom rate limiting.
class APIController extends ResourceController
{
protected $filters = [
'tokens',
'rates:200,HOUR', // 200 requests per hour
];
}
- The first argument is the request limit (a number).
- The second argument is the period. It may be a number of seconds, or one of the named units below (case-insensitive; plural and short forms are accepted):
| Period argument | Resolves to |
|---|---|
SECOND / SECONDS / SEC |
1 s |
MINUTE / MINUTES / MIN |
60 s |
HOUR / HOURS |
3 600 s |
DAY / DAYS |
86 400 s |
WEEK / WEEKS |
604 800 s |
90 (numeric) |
90 s |
| unrecognised | leaves the resolved window unchanged |
// 30 requests per 5 minutes (period given in seconds):
$routes->get('reports', 'Reports::index', ['filter' => 'rates:30,300']);
// 30 requests per minute (named unit resolves to 60 seconds):
$routes->get('reports', 'Reports::index', ['filter' => 'rates:30,MINUTE']);
Only the limit may be supplied (rates:25), in which case the period falls back to the
global timeLimit.
Override precedence¶
A configured endpoint database row still wins over both the global defaults and the
per-route argument. The resolution order applied by RatesFilter::before() is:
- Global
AuthSecurity::$requestLimit/$timeLimit. - Per-route argument (
rates:<limit>,<period>) — overrides the globals for that route. - A matching
Endpointrow (runtime/admin override) — overrides everything above.
A user whose ignore_rates flag is set bypasses throttling entirely. When the limit is
exceeded the filter returns a 429 response with the Auth.throttled message.
2. Force Password Reset Filter (force-reset)¶
Forces password change when necessary.
// Apply after login
$routes->group('secure', [
'filter' => 'auth:session,force-reset'
], function($routes) {
$routes->get('dashboard', 'Dashboard::index');
});
// In database, mark user for reset
auth()->user()->forcePasswordReset();
3. Password Age Filter (password-age)¶
Forces a password reset once the user's password_changed_at is older than AuthSecurity::$passwordMaxAge seconds. Apply after authentication.
// app/Config/AuthSecurity.php
public int $passwordMaxAge = 90 * DAY; // 90-day rotation
// app/Config/Routes.php
$routes->group('app', ['filter' => 'auth:session,password-age'], function ($routes) {
$routes->get('dashboard', 'Dashboard::index');
});
Behaviour:
- Runs after authentication. If the user is not logged in, the filter no-ops.
- If
password_changed_atisnull(older accounts before the migration), the filter leaves the user alone — grandfathered. - If the timestamp is older than
passwordMaxAge, the filter setsforce_reset = 1on the user's email_password identity and redirects toAuth::forcePasswordResetRedirect()withAuth.passwordExpired.
See Audit & Compliance — Password Rotation for the full lifecycle.
4. Password Confirm Filter (password-confirm)¶
Forces the user to re-enter their password before performing sensitive actions ("sudo mode"). Inspired by Laravel Fortify's password.confirm middleware. Use it on routes that change critical state — disabling 2FA, generating API tokens, unlinking OAuth providers, deleting the account.
// app/Config/AuthSecurity.php
public int $passwordConfirmationLifetime = 3 * HOUR; // 0 = always re-confirm
Routes¶
// 1. Wire the confirmation form once (must be reachable WITHOUT
// password-confirm to break the chicken-and-egg loop):
$routes->group('auth', ['filter' => 'auth:session', 'namespace' => 'Daycry\Auth\Controllers'], static function ($routes) {
$routes->get('confirm-password', 'UserSecurityController::confirmPasswordView', ['as' => 'password-confirm-show']);
$routes->post('confirm-password', 'UserSecurityController::confirmPasswordAction', ['as' => 'password-confirm']);
});
// 2. Apply the filter on routes that need fresh confirmation:
$routes->group('account/security', ['filter' => 'auth:session,password-confirm'], static function ($routes) {
$routes->post('totp/disable', 'Account::disableTotp');
$routes->post('email/change', 'Account::changeEmail');
$routes->post('tokens/generate', 'Account::generateApiToken');
$routes->delete('account', 'Account::deleteAccount');
});
Per-route TTL: password-confirm:<seconds>¶
The filter honors a per-route lifetime argument. password-confirm:<seconds> requires a
password confirmation no older than <seconds> for that route, regardless of the
global AuthSecurity::$passwordConfirmationLifetime. Use it to demand a fresher
confirmation on your most sensitive routes ("sudo mode"):
// The global window may be 3 hours, but these two routes demand a confirmation
// that is at most 60 seconds old.
$routes->post('account/delete', 'Account::deleteAccount', ['filter' => 'auth:session,password-confirm:60']);
$routes->post('account/disable-2fa', 'Account::disableTotp', ['filter' => 'auth:session,password-confirm:60']);
The argument must be numeric; non-numeric arguments are ignored and the global
passwordConfirmationLifetime applies. A value of 0 (global or per-route) means every
protected request requires a fresh confirmation.
Behaviour¶
- The filter no-ops for anonymous requests — pair it with
session/authwhich handle the login redirect. - Reads
password_confirmed_atfrom the session. - Resolves the lifetime: a numeric per-route argument overrides the global
passwordConfirmationLifetime. - If the timestamp is missing or older than the resolved lifetime, stashes the current URL (
passwordConfirmIntendedUrltempdata) and redirects topassword-confirm-showwith theAuth.passwordConfirmRequirederror. - After the user submits the form successfully,
UserSecurityController::confirmPasswordActionstamps a fresh timestamp, writes anEVENT_PASSWORD_CONFIRMEDaudit entry, and redirects back to the originally intended URL.
Settings reference¶
| Setting / argument | Default | Effect |
|---|---|---|
passwordConfirmationLifetime = 0 |
— | Every protected request requires a fresh confirmation. |
passwordConfirmationLifetime = HOUR |
— | One confirmation valid for 1 h. |
passwordConfirmationLifetime = 3 * HOUR |
default | Matches Laravel Fortify. |
password-confirm:<seconds> (route argument) |
unset | Per-route override of the lifetime, in seconds, for that route only. |
The view rendered by
confirmPasswordView()isDaycry\Auth\Views\confirm_password.php. Override viasetting('Auth.views')['confirm_password'].
Request logging (not a filter)¶
There is no auth-request filter alias. Logging and monitoring of
authenticated requests happens automatically for controllers that extend
Daycry\Auth\Controllers\BaseAuthController: end-of-request bookkeeping runs in
BaseControllerTrait::finalizeRequest() (idempotent and exception-safe — it can
also be invoked from an after filter for deterministic timing). What gets
logged is controlled by the logging settings — see the
Logging guide.
🛠️ Advanced Configuration¶
Conditional Filters¶
class ConditionalController extends BaseController
{
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger): void
{
parent::initController($request, $response, $logger);
// Apply filters conditionally
if ($this->request->isAJAX()) {
$this->filters['tokens'] = ['only' => ['api_method']];
} else {
$this->filters['session'] = ['only' => ['web_method']];
}
}
}
Custom Filters¶
<?php
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class CustomAuthFilter implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null)
{
// Custom authentication logic
if (!auth()->loggedIn()) {
if ($request->isAJAX()) {
return service('response')->setStatusCode(401)
->setJSON(['error' => 'Unauthorized']);
}
return redirect()->to('/login');
}
// Additional validations
$user = auth()->user();
if (!$user->active) {
return redirect()->to('/account-suspended');
}
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
// Logic after response
if (auth()->loggedIn()) {
// Update last activity
auth()->user()->updateLastActive();
}
}
}
Filter Combination¶
// Multiple filters in specific order
$routes->group('secure-api', [
'filter' => 'rates,chain,permission:api.access'
], function($routes) {
$routes->resource('sensitive-data');
});
// In controller with multiple filters
class SecureController extends BaseController
{
protected $filters = [
'session', // Must be authenticated
'group:admin,moderator', // Must be admin or moderator
'permission:admin.access', // Must have specific permission
'force-reset', // Check if password reset needed
'rates:50,HOUR', // Maximum 50 requests per hour
];
}
🎯 Practical Examples¶
1. Complete Admin Panel¶
// Main panel - requires authentication
$routes->group('admin', [
'namespace' => 'App\Controllers\Admin',
'filter' => 'auth:session'
], function($routes) {
// Dashboard - basic admin access
$routes->get('/', 'Dashboard::index', ['filter' => 'group:admin']);
// User management - specific permissions
$routes->group('users', ['filter' => 'group:admin'], function($routes) {
$routes->get('/', 'Users::index', ['filter' => 'permission:users.view']);
$routes->get('create', 'Users::create', ['filter' => 'permission:users.create']);
$routes->post('/', 'Users::store', ['filter' => 'permission:users.create']);
$routes->get('(:num)/edit', 'Users::edit/$1', ['filter' => 'permission:users.edit']);
$routes->put('(:num)', 'Users::update/$1', ['filter' => 'permission:users.edit']);
$routes->delete('(:num)', 'Users::destroy/$1', ['filter' => 'permission:users.delete']);
});
// System settings - super-admin only
$routes->group('system', ['filter' => 'group:super-admin'], function($routes) {
$routes->get('settings', 'System::settings');
$routes->post('settings', 'System::updateSettings');
$routes->get('backups', 'System::backups', ['filter' => 'permission:system.backups']);
});
});
2. RESTful API with Multiple Auth Methods¶
// API that accepts tokens, JWT or session
$routes->group('api/v1', [
'namespace' => 'App\Controllers\API',
'filter' => 'rates:1000,HOUR' // Global rate limiting
], function($routes) {
// Public endpoints (no auth)
$routes->get('status', 'Status::index');
$routes->post('auth/login', 'Auth::login');
// Authenticated endpoints (any method)
$routes->group('', ['filter' => 'chain'], function($routes) {
$routes->get('profile', 'Users::profile');
$routes->put('profile', 'Users::updateProfile');
// Posts - granular permissions
$routes->get('posts', 'Posts::index', ['filter' => 'permission:posts.view']);
$routes->post('posts', 'Posts::create', ['filter' => 'permission:posts.create']);
$routes->put('posts/(:num)', 'Posts::update/$1', ['filter' => 'permission:posts.edit']);
$routes->delete('posts/(:num)', 'Posts::delete/$1', ['filter' => 'permission:posts.delete']);
});
// Admin endpoints - admins only with strict rate limiting
$routes->group('admin', [
'filter' => 'chain,group:admin,rates:100,HOUR'
], function($routes) {
$routes->get('stats', 'Admin::stats');
$routes->get('users', 'Admin::users', ['filter' => 'permission:admin.users']);
});
});
3. Application with Different Access Levels¶
class MultiLevelController extends BaseController
{
protected $filters = [
'session' => ['except' => ['public']],
'group:subscriber' => ['only' => ['basic_content']],
'group:premium' => ['only' => ['premium_content']],
'group:admin' => ['only' => ['admin_content']],
'permission:content.moderate' => ['only' => ['moderate']],
];
public function public()
{
// Public content
return view('public_content');
}
public function basic_content()
{
// Only for users with 'subscriber' group or higher
return view('basic_content');
}
public function premium_content()
{
// Only for premium users
return view('premium_content');
}
public function admin_content()
{
// Only for administrators
return view('admin_content');
}
public function moderate()
{
// Only users with specific moderation permission
return view('moderation_panel');
}
}
🚨 Error Handling¶
Custom Responses for Filters¶
// In app/Config/Filters.php
public array $globals = [
'before' => [
'rates' => ['except' => ['api/public/*']],
],
];
// Customize responses in events
// In app/Config/Events.php
Events::on('auth.fail', function($result) {
if (service('request')->isAJAX()) {
return service('response')->setStatusCode(401)
->setJSON([
'error' => 'Authentication failed',
'message' => $result->reason(),
'redirect' => site_url('login')
]);
}
});
📈 Monitoring and Debugging¶
Filter Debugging¶
// In development, enable filter logging
// In .env
CI_ENVIRONMENT = development
// Filters will automatically log their execution
// Check in writable/logs/
Filter Testing¶
// In tests
class FilterTest extends FeatureTestCase
{
public function testAdminFilterRequiresAdminGroup()
{
$user = fake(UserModel::class);
$user->addGroup('user'); // Normal group
$result = $this->actingAs($user)
->get('/admin');
$result->assertRedirect(); // Should redirect
$result->assertSessionHas('error');
}
public function testAPITokenFilter()
{
$token = 'valid-token';
$result = $this->withHeaders(['X-API-KEY' => $token])
->get('/api/users');
$result->assertOK();
}
}
🔗 Next: Controllers - Learn to create robust controllers with the new architecture