๐Ÿ›ก๏ธ 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 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:

    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 session filter; for APIs use tokens / 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' => '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' => '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' => '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' => 'session,permission:users.edit'
]);

// Multiple permissions (AND - must have all)
$routes->delete('admin/users/(:num)', 'Admin\Users::delete/$1', [
    'filter' => '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' => '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. 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' => 'tokens,token-scope:posts.read',
]);

// Multiple scopes โ€” AND-ed (token must grant BOTH)
$routes->post('api/posts', 'Posts::create', [
    'filter' => 'tokens,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 Forbidden JSON.

  • Web requests โ†’ redirect to Auth::permissionDeniedRedirect() with a flash error.

Tip: prefer token-scope over permission: for API tokens โ€” it scopes the token, not the user. A user with posts.write permission 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 (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:

  1. Runs after authentication. If the user is not logged in, the filter no-ops.

  2. If password_changed_at is null (older accounts before the migration), the filter leaves the user alone โ€” grandfathered.

  3. If the timestamp is older than passwordMaxAge, the filter sets force_reset = 1 on the userโ€™s email_password identity and redirects to Auth::forcePasswordResetRedirect() with Auth.passwordExpired.

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๏ƒ

  1. The filter no-ops for anonymous requests โ€” pair it with session/auth which handle the login redirect.

  2. Reads password_confirmed_at from the session.

  3. If the timestamp is missing or older than passwordConfirmationLifetime, stashes the current URL and redirects to password-confirm-show.

  4. After the user submits the form successfully, UserSecurityController::confirmPasswordAction stamps a fresh timestamp, writes an EVENT_PASSWORD_CONFIRMED audit entry, and redirects back to the originally intended URL.

Settings reference๏ƒ

Setting

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.

The view rendered by confirmPasswordView() is Daycry\Auth\Views\confirm_password.php. Override via setting('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