๐ก๏ธ 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๏
๐ง Initial Setup๏
1. Register Filters in app/Config/Filters.php๏
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Filters extends BaseConfig
{
public array $aliases = [
// === AUTHENTICATION FILTERS ===
'session' => \Daycry\Auth\Filters\AuthSessionFilter::class,
'tokens' => \Daycry\Auth\Filters\AuthAccessTokenFilter::class,
'jwt' => \Daycry\Auth\Filters\AuthJWTFilter::class,
'basic-auth' => \Daycry\Auth\Filters\BasicAuthFilter::class,
'chain' => \Daycry\Auth\Filters\ChainFilter::class,
// === AUTHORIZATION FILTERS ===
'group' => \Daycry\Auth\Filters\GroupFilter::class,
'permission' => \Daycry\Auth\Filters\PermissionFilter::class,
'token-scope' => \Daycry\Auth\Filters\TokenScopeFilter::class,
// === CONTROL FILTERS ===
'auth-rates' => \Daycry\Auth\Filters\AuthRatesFilter::class,
'force-reset' => \Daycry\Auth\Filters\ForcePasswordResetFilter::class,
'password-age' => \Daycry\Auth\Filters\PasswordAgeFilter::class,
'auth-request' => \Daycry\Auth\Filters\AuthRequestFilter::class,
];
// Global filters (optional)
public array $globals = [
'before' => [
// 'auth-rates', // Global rate limiting
],
'after' => [],
];
}
๐ Authentication Filters๏
1. Session Filter (session)๏
Verifies that the user is authenticated via session.
Basic Usage๏
// In routes
$routes->group('dashboard', ['filter' => 'session'], function($routes) {
$routes->get('/', 'Dashboard::index');
$routes->get('profile', 'Dashboard::profile');
});
// In controller
class Dashboard extends BaseController
{
protected $filters = ['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' => 'tokens'], 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 = ['tokens'];
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' => '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 |
|
Wrong scheme (e.g. |
|
Malformed base64 / no colon |
|
Unknown user / wrong password |
|
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:
curl -u maintenance@example.com:secret https://app.example/cron/purge-old-tokens
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'
]);
}
}
๐ 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 (auth-rates)๏
Request rate control per user/IP.
Global Configuration๏
// In Filters.php
public array $globals = [
'before' => [
'auth-rates', // Apply to all routes
],
];
// In Auth.php
public string $limitMethod = 'USER'; // Per authenticated user
public int $requestLimit = 100; // 100 requests
public int $timeLimit = HOUR; // Per hour
Specific Configuration๏
// API-specific rate limiting
$routes->group('api', ['filter' => 'auth-rates:50,MINUTE'], function($routes) {
$routes->resource('users');
});
// In controller with custom rate limiting
class APIController extends ResourceController
{
protected $filters = [
'tokens',
'auth-rates:200,HOUR' // 200 requests per hour
];
}
2. Force Password Reset Filter (force-reset)๏
Forces password change when necessary.
// Apply after login
$routes->group('secure', [
'filter' => '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' => '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' => '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' => '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');
});
Behaviour๏
The filter no-ops for anonymous requests โ pair it with
session/authwhich handle the login redirect.Reads
password_confirmed_atfrom the session.If the timestamp is missing or older than
passwordConfirmationLifetime, stashes the current URL and redirects topassword-confirm-show.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 |
Effect |
|---|---|
|
Every protected request requires a fresh confirmation. |
|
One confirmation valid for 1 h. |
|
Matches Laravel Fortify. |
The view rendered by
confirmPasswordView()isDaycry\Auth\Views\confirm_password.php. Override viasetting('Auth.views')['confirm_password'].
5. Auth Request Filter (auth-request)๏
Logging and monitoring of authenticated requests.
// Enable request logging
$routes->group('admin', [
'filter' => 'session,auth-request'
], function($routes) {
$routes->get('/', 'Admin::index');
});
๐ ๏ธ 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' => 'auth-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
'auth-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' => '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' => 'auth-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,auth-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' => [
'auth-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