๐ฎ Controllers โ Complete Guide๏
This guide covers all controllers included with Daycry Auth, including the new password reset, force reset, JWT, and user security controllers.
๐ Index๏
BaseAuthController๏
All auth controllers extend BaseAuthController, which provides a set of reusable helper methods.
abstract class BaseAuthController extends BaseController implements AuthController
{
use BaseControllerTrait; // Wires exception handler, attempt handler, request logger
use Viewable; // Provides $this->view()
// Every subclass must declare its validation rules
abstract protected function getValidationRules(): array;
}
Available Helper Methods๏
Method |
Purpose |
|---|---|
|
Returns CSRF token for forms |
|
Redirects if already authenticated |
|
Returns the session authenticator |
|
Checks if a 2FA/activation action is pending |
|
Redirects to the action controller |
|
Reads login fields from POST |
|
Checks the โremember meโ checkbox |
|
Runs CI4 validation |
|
Redirects back with errors |
|
Redirects with success message |
|
Redirects back with error |
|
Handles an auth result cleanly |
LoginController๏
Handles traditional email + password login and logout.
Routes (from Config/Auth.php):
GET /login โ loginView()
POST /login โ loginAction()
GET /logout โ logoutAction()
Extending LoginController๏
<?php
namespace App\Controllers;
use CodeIgniter\HTTP\ResponseInterface;
use Daycry\Auth\Controllers\LoginController as BaseLoginController;
class LoginController extends BaseLoginController
{
public function loginView(): ResponseInterface
{
if ($redirect = $this->redirectIfLoggedIn()) {
return $redirect;
}
// Add custom data to the view
$content = $this->view(setting('Auth.views')['login'], [
'title' => 'Sign In to ' . config('App')->appName,
'socialLogins' => ['google', 'github'],
]);
return $this->response->setBody($content);
}
}
RegisterController๏
Handles user registration.
Routes:
GET /register โ registerView()
POST /register โ registerAction()
Extending RegisterController๏
<?php
namespace App\Controllers;
use Daycry\Auth\Controllers\RegisterController as BaseRegisterController;
class RegisterController extends BaseRegisterController
{
protected function getValidationRules(): array
{
// Add extra fields on top of the defaults
return array_merge(parent::getValidationRules(), [
'firstname' => ['label' => 'First Name', 'rules' => 'required|max_length[50]'],
'lastname' => ['label' => 'Last Name', 'rules' => 'required|max_length[50]'],
]);
}
}
ActionController๏
Handles post-authentication actions such as email 2FA, account activation, and TOTP verification.
Routes:
GET /auth/a/show โ show()
POST /auth/a/handle โ handle()
POST /auth/a/verify โ verify()
This controller is called automatically by the library when $actions['login'] or $actions['register'] is set in Config/Auth.php. You do not typically extend it.
MagicLinkController๏
Handles passwordless login via one-time email links.
Routes:
GET /login/magic-link โ loginView()
POST /login/magic-link โ loginAction()
GET /login/verify-magic-link โ verify()
The base controller is fully functional. Extend only if you need to customise the view or post-login redirect:
use Daycry\Auth\Controllers\MagicLinkController as BaseMagicLinkController;
class MagicLinkController extends BaseMagicLinkController
{
// Override only what you need
}
PasswordResetController๏
Provides the complete password reset flow for users who have forgotten their password.
Routes (from Config/Auth.php):
GET /password-reset โ requestView() โ Show "Enter your email" form
POST /password-reset โ requestAction() โ Send reset email
GET /password-reset/message โ messageView() โ "Check your inbox" confirmation
GET /password-reset/{token} โ resetView() โ Show "Set new password" form
POST /password-reset/{token} โ resetAction() โ Apply the new password
How to Enable๏
Register the routes in app/Config/Routes.php:
$routes->group('', ['namespace' => 'Daycry\Auth\Controllers'], static function ($routes) {
$routes->get('password-reset', 'PasswordResetController::requestView', ['as' => 'password-reset']);
$routes->post('password-reset', 'PasswordResetController::requestAction');
$routes->get('password-reset/message', 'PasswordResetController::messageView', ['as' => 'password-reset-message']);
$routes->get('password-reset/(:any)', 'PasswordResetController::resetView', ['as' => 'password-reset-form']);
$routes->post('password-reset/(:any)', 'PasswordResetController::resetAction');
});
Configuration๏
// app/Config/Auth.php
public int $passwordResetLifetime = HOUR; // How long the token is valid
Security Design๏
The
requestAction()always shows a generic โcheck your emailโ message regardless of whether the email exists โ this prevents email enumeration.Tokens are cryptographically secure (20 random bytes).
Tokens are deleted immediately after use.
After a successful reset,
Events::trigger('passwordReset', $user)fires.
Adding a Link to Your Login Page๏
<a href="<?= route_to('password-reset') ?>">Forgot your password?</a>
Customising the Reset Email๏
Override the view in Config/Auth.php:
public array $views = [
// ...
'password-reset-email' => '\App\Views\Email\PasswordReset',
];
The view receives the variable $link โ the full reset URL with the token.
Listen for Reset Completion๏
// app/Config/Events.php
Events::on('passwordReset', static function (object $user): void {
// Revoke all active sessions on password change
$user->revokeAllAccessTokens();
});
ForcePasswordResetController๏
When an administrator flags an account for a mandatory password change (e.g., after a security incident), ForcePasswordResetFilter intercepts the user and sends them here.
Routes:
GET /force-reset โ showView() โ Show the form (requires current password)
POST /force-reset โ resetAction() โ Validate and update password
How to Enable the Filter๏
// app/Config/Filters.php
public array $aliases = [
'force-reset' => \Daycry\Auth\Filters\ForcePasswordResetFilter::class,
];
// app/Config/Routes.php โ apply the filter to protected routes
$routes->group('dashboard', ['filter' => 'session,force-reset'], static function ($routes) {
$routes->get('/', 'Dashboard::index');
});
// Register the reset routes (unfiltered, so user can access them)
$routes->get('force-reset', 'Daycry\Auth\Controllers\ForcePasswordResetController::showView', ['as' => 'force-reset']);
$routes->post('force-reset', 'Daycry\Auth\Controllers\ForcePasswordResetController::resetAction');
Flag a User Programmatically๏
use Daycry\Auth\Models\UserIdentityModel;
// Flag one user
model(UserIdentityModel::class)->forceMultiplePasswordReset([$userId]);
// Flag all users (security breach scenario)
model(UserIdentityModel::class)->forceGlobalPasswordReset();
What the Form Requires๏
The user must enter:
Their current password (verified before changing)
A new password and confirmation
This prevents someone who has briefly accessed an unlocked computer from changing another userโs password.
JwtController๏
Provides a complete stateless JWT authentication API with refresh token rotation.
Routes:
POST /auth/jwt/login โ login() โ Exchange credentials for access + refresh token
POST /auth/jwt/refresh โ refresh() โ Exchange refresh token for new token pair
POST /auth/jwt/logout โ logout() โ Revoke the refresh token
Register the Routes๏
$routes->post('auth/jwt/login', 'Daycry\Auth\Controllers\JwtController::login', ['as' => 'jwt-login']);
$routes->post('auth/jwt/refresh', 'Daycry\Auth\Controllers\JwtController::refresh', ['as' => 'jwt-refresh']);
$routes->post('auth/jwt/logout', 'Daycry\Auth\Controllers\JwtController::logout', ['as' => 'jwt-logout']);
Configuration๏
// app/Config/Auth.php
public int $jwtRefreshLifetime = 30 * DAY; // Refresh token validity
login()๏
Request (application/x-www-form-urlencoded or JSON):
email=user@example.com
password=secret
Response (200):
{
"access_token": "eyJ0eXAi...",
"refresh_token": "a3f8c2d1...",
"user_id": 42,
"token_type": "Bearer"
}
Error (401):
{ "message": "Invalid credentials." }
refresh()๏
Request:
user_id=42
refresh_token=a3f8c2d1...
Response (200):
{
"access_token": "eyJ0eXAi...",
"refresh_token": "newtoken...",
"user_id": 42,
"token_type": "Bearer"
}
The old refresh token is immediately revoked. Store the new one in your client.
logout()๏
Request:
user_id=42
refresh_token=a3f8c2d1...
Response (200):
{ "message": "Logged out successfully." }
Security Notes๏
Access tokens are short-lived JWTs (configured in
Config/JWT.php).Refresh tokens are stored hashed (SHA-256) in
auth_users_identitieswith typejwt_refresh.Each refresh token is one-time use โ a new pair is issued on every
/refreshcall.Revoke a refresh token by calling
/logoutor by revoking the identity record directly.
UserSecurityController๏
Provides self-service security management for logged-in users: change password, change email, manage device sessions, and unlink OAuth providers.
All routes require an active session (filter: session).
Register the Routes๏
// app/Config/Routes.php
$routes->group('security', ['filter' => 'session', 'namespace' => 'Daycry\Auth\Controllers'], static function ($routes) {
// Change password
$routes->get('password', 'UserSecurityController::changePasswordView', ['as' => 'security-password']);
$routes->post('password', 'UserSecurityController::changePassword');
// Change email
$routes->get('email', 'UserSecurityController::changeEmailView', ['as' => 'security-email']);
$routes->post('email', 'UserSecurityController::changeEmail');
$routes->get('email/confirm', 'UserSecurityController::confirmEmailChange', ['as' => 'security-email-confirm']);
// Device sessions
$routes->get('sessions', 'UserSecurityController::deviceSessionsView', ['as' => 'security-sessions']);
$routes->delete('sessions/(:segment)', 'UserSecurityController::terminateDeviceSession/$1');
$routes->delete('sessions/other/all', 'UserSecurityController::terminateOtherDeviceSessions');
// TOTP
$routes->get('totp/enable', 'UserSecurityController::totpEnableView', ['as' => 'totp-enable']);
$routes->post('totp/enable', 'UserSecurityController::totpEnableAction');
$routes->post('totp/disable', 'UserSecurityController::totpDisableAction', ['as' => 'totp-disable']);
// OAuth
$routes->post('oauth/unlink/(:segment)', 'UserSecurityController::unlinkOauth/$1', ['as' => 'oauth-unlink']);
// Login activity feed (recent attempts)
$routes->get('activity', 'UserSecurityController::loginActivity', ['as' => 'security-activity']);
});
changePassword()๏
Users can change their password by providing their current password + new password.
Form fields:
current_passwordโ verified before changingnew_passwordnew_password_confirmโ must matchnew_password
changeEmail()๏
Initiates an email address change. The system sends a confirmation link to the new email address. The address is only updated once the link is clicked.
Form fields:
new_emailโ the new address (must be a valid email not already in use)current_passwordโ required to authorise the change
confirmEmailChange()๏
Called when the user clicks the confirmation link in their email. Validates the token and updates the email address.
unlinkOauth()๏
Removes an OAuth provider link from the userโs account. Fails gracefully if the user would have no remaining login method.
loginActivity()๏
Returns a Bootstrap-styled view (Views/security/login_activity.php) listing the userโs recent login attempts (success + failure). Reads from auth_logins via LoginModel::recentForUser().
Query string parameters:
Param |
Default |
Max |
Description |
|---|---|---|---|
|
|
|
Number of entries to display. |
Override the view via setting('Auth.views')['security_login_activity']:
// app/Config/Auth.php
public array $views = [
// ...
'security_login_activity' => 'App\Views\security\my_activity_feed',
];
The default view exposes these variables:
Variable |
Type |
Description |
|---|---|---|
|
|
Login rows newest-first. |
|
|
The applied limit. |
<!-- In a security settings view -->
<?= form_open('security/oauth/unlink/google', ['method' => 'post']) ?>
<button type="submit" class="btn btn-sm btn-outline-danger">
Disconnect Google
</button>
<?= form_close() ?>
Creating Custom Controllers๏
Minimal Custom Controller๏
Any controller that needs auth features can extend BaseAuthController:
<?php
namespace App\Controllers;
use Daycry\Auth\Controllers\BaseAuthController;
use CodeIgniter\HTTP\ResponseInterface;
class AccountController extends BaseAuthController
{
// Required by BaseAuthController
protected function getValidationRules(): array
{
return [
'name' => ['label' => 'Name', 'rules' => 'required|max_length[100]'],
];
}
public function update(): ResponseInterface
{
$postData = $this->request->getPost();
if (! $this->validateRequest($postData, $this->getValidationRules())) {
return $this->handleValidationError(route_to('account-edit'));
}
$user = auth()->user();
$user->username = $postData['name'];
model(\Daycry\Auth\Models\UserModel::class)->save($user);
return $this->handleSuccess(route_to('account'), 'Profile updated successfully.');
}
}
API Controller with JWT + Access Token๏
<?php
namespace App\Controllers\API;
use CodeIgniter\RESTful\ResourceController;
class PostsController extends ResourceController
{
protected $format = 'json';
public function index()
{
$user = auth()->user(); // Works with any authenticator (jwt, access_token)
if (! $user->can('posts.view')) {
return $this->failForbidden('You do not have permission to view posts.');
}
$posts = model(\App\Models\PostModel::class)
->where('user_id', $user->id)
->findAll();
return $this->respond(['data' => $posts]);
}
public function create()
{
$user = auth()->user();
if (! $user->can('posts.create')) {
return $this->failForbidden('You cannot create posts.');
}
$data = $this->request->getJSON(true);
// ... create post
return $this->respondCreated(['message' => 'Post created.']);
}
}
Best Practices๏
1. Always Use BaseAuthController for Auth Logic๏
Avoid duplicating redirect, validation, and error-handling code. BaseAuthControllerโs helper methods handle all of this consistently.
2. Validate in getValidationRules(), Not in Action Methods๏
// Good
protected function getValidationRules(): array
{
return ['email' => ['rules' => 'required|valid_email']];
}
public function someAction()
{
if (! $this->validateRequest($this->request->getPost(), $this->getValidationRules())) {
return $this->handleValidationError('/form-url');
}
// ...
}
// Avoid
public function someAction()
{
if (! $this->request->getPost('email')) { // Ad-hoc validation
return redirect()->back()->with('error', 'Email required');
}
}
3. Check Permissions Early๏
public function adminAction()
{
if (! auth()->user()->inGroup('admin')) {
return redirect()->to('/')->with('error', 'Unauthorized');
}
// ... rest of action
}
4. Use Events for Side Effects๏
Donโt put email sending, logging, or audit trails directly in controllers. Use CI4 Events instead:
// In a controller
$user->setPassword($newPassword);
model(UserModel::class)->save($user);
Events::trigger('passwordChanged', $user); // โ clean
// In app/Config/Events.php โ the side effect lives here
Events::on('passwordChanged', fn($user) => /* send notification */);
5. Return Correct HTTP Status Codes in APIs๏
return $this->response->setStatusCode(422)->setJSON(['errors' => $errors]); // Validation
return $this->response->setStatusCode(401)->setJSON(['message' => 'Unauthenticated']);
return $this->response->setStatusCode(403)->setJSON(['message' => 'Forbidden']);
๐ See also:
Filters โ Protect routes before they reach controllers
Authorization โ Groups and permissions
Authentication โ JWT refresh tokens, password reset flow
TOTP 2FA โ Two-factor authentication
Device Sessions โ Session management