📊 Logging, Events & Monitoring¶
Daycry Auth provides two independent logging systems that complement each other:
- CodeIgniter Events — lightweight hooks fired at authentication milestones; listened to in your app code
- Database Logs — persistent records of login attempts, access logs, and rate limiting stored in dedicated tables
📋 Table of Contents¶
- CodeIgniter Events
- Available Events
- Listening to Events
- Pre-Authentication Events
- Suspicious Login Event
- Database Logging
- Login Attempt Logging
- Failed Attempt Blocking
- Per-User Account Lockout
- Rate Limiting
- Audit Log (auth_audit_logs)
- Monitoring & Querying Logs
CodeIgniter Events¶
Daycry Auth fires CI4 events at key authentication moments. Your application code listens to these events in app/Config/Events.php — no modifications to the library are needed.
Register Event Listeners¶
<?php
// app/Config/Events.php
use CodeIgniter\Events\Events;
// Runs after a successful login
Events::on('login', static function (object $user): void {
log_message('info', "User {$user->email} logged in.");
});
// Runs after a failed login attempt
Events::on('failedLogin', static function (array $credentials): void {
log_message('warning', "Failed login attempt for: " . ($credentials['email'] ?? 'unknown'));
});
// Runs after logout
Events::on('logout', static function (object $user): void {
log_message('info', "User {$user->email} logged out.");
});
// Runs after successful registration
Events::on('registered', static function (object $user): void {
// Send a welcome email, assign a default group, etc.
service('email')
->setTo($user->email)
->setSubject('Welcome!')
->setMessage("Thanks for signing up!")
->send();
});
Available Events¶
| Event | When Fired | Arguments |
|---|---|---|
pre-login |
Before credentials are checked | array $credentials |
login |
After successful login | User $user |
failedLogin |
After a failed login attempt | array $credentials |
logout |
After logout | User $user |
pre-register |
Before registration is processed | array $postData |
registered |
After successful registration | User $user |
passwordReset |
After password successfully reset | User $user |
magicLogin |
After magic link login | User $user |
oauth-login |
After successful OAuth login | User $user, string $providerName |
oauth-profile-fetched |
After profile fields resolved from OAuth provider | User $user, string $providerName, array $profileData |
suspicious-login |
After a successful login flagged as anomalous (when AuthSecurity::$suspiciousLoginAlerts = true) |
User $user, list<string> $flags, string $ipAddress, string $userAgent |
Listening to Events¶
Security Alert on Multiple Failed Logins¶
Events::on('failedLogin', static function (array $credentials): void {
$email = $credentials['email'] ?? null;
if ($email === null) {
return;
}
// Count recent failures from the auth_logins table
$recentFailures = model(\Daycry\Auth\Models\LoginModel::class)
->where('identifier', $email)
->where('success', 0)
->where('created_at >', date('Y-m-d H:i:s', strtotime('-15 minutes')))
->countAllResults();
if ($recentFailures >= 5) {
// Alert the security team
log_message('critical', "Brute-force suspected for account: {$email}");
}
});
Welcome Email on Registration¶
Events::on('registered', static function (object $user): void {
$emailService = service('email');
$emailService->setTo($user->email)
->setSubject('Welcome to ' . config('App')->appName)
->setMessage(view('emails/welcome', ['user' => $user]))
->send();
});
Audit Trail for Password Resets¶
Events::on('passwordReset', static function (object $user): void {
log_message('notice', "Password reset completed for user ID {$user->id} ({$user->email}).");
// Write to an audit log table
db_connect()->table('audit_log')->insert([
'user_id' => $user->id,
'action' => 'password_reset',
'created_at' => date('Y-m-d H:i:s'),
]);
});
OAuth Login Tracking¶
// Log all OAuth logins with provider name
Events::on('oauth-login', static function (object $user, string $provider): void {
log_message('info', "OAuth login via {$provider} for user {$user->email}");
});
// Sync profile data when fetched from OAuth provider
Events::on('oauth-profile-fetched', static function (object $user, string $provider, array $profileData): void {
log_message('info', "Profile fetched from {$provider}: " . json_encode(array_keys($profileData)));
});
See OAuth 2.0 & Social Login for more details on OAuth events.
Pre-Authentication Events¶
pre-login and pre-register fire before any database check. Listeners can inspect or enrich the data. They cannot cancel the operation directly, but can redirect via redirect().
Log All Login Attempts¶
Events::on('pre-login', static function (array $credentials): void {
$ip = service('request')->getIPAddress();
log_message('debug', "Login attempt for '{$credentials['email']}' from IP {$ip}");
});
Block Specific Domains from Registering¶
Events::on('pre-register', static function (array $data): void {
$email = $data['email'] ?? '';
$domain = substr(strrchr($email, '@'), 1);
$blockedDomains = ['tempmail.com', 'throwaway.email'];
if (in_array($domain, $blockedDomains, true)) {
// Redirect before processing — effectively cancels registration
redirect()->to('/register')->with('error', 'Disposable email addresses are not allowed.')->send();
exit;
}
});
Suspicious Login Event¶
When AuthSecurity::$suspiciousLoginAlerts = true, every successful login runs SuspiciousLoginDetector and fires the suspicious-login event whenever the IP / User-Agent does not match the user's recent history.
Listener — email the user¶
use CodeIgniter\Events\Events;
use CodeIgniter\I18n\Time;
use Daycry\Auth\Entities\User;
Events::on('suspicious-login', static function (User $user, array $flags, string $ip, string $ua): void {
helper('email');
$email = emailer()
->setFrom(setting('Email.fromEmail'), (string) setting('Email.fromName'))
->setTo($user->email)
->setSubject(lang('Auth.suspiciousLoginSubject'))
->setMessage(view('Daycry\\Auth\\Views\\Email\\suspicious_login_alert', [
'user' => $user,
'flags' => $flags,
'ipAddress' => $ip,
'userAgent' => $ua,
'date' => Time::now()->toDateTimeString(),
]));
$email->send(false);
});
Possible flag values¶
| Flag | Meaning |
|---|---|
new_ip |
The IP has not appeared in this user's successful logins for the last 30 days. |
new_device |
The User-Agent has not been seen on any device session for this user. |
The flags list is forward-compatible — additional signals (geo-IP mismatch, ASN reputation, time-of-day anomaly) can be added without breaking existing listeners.
Every flagged login also writes an
EVENT_SUSPICIOUS_LOGINrow to the audit log — see Audit Log below.
See Audit & Compliance — Suspicious Login Detection for the full reference.
Database Logging¶
Enable Activity Logs¶
When enabled, authentication events are written to the auth_logs table. The log includes the user ID, action type, IP address, and timestamp.
Query Logs¶
// Get recent login events for a user
$logs = model(\Daycry\Auth\Models\LogModel::class)
->where('user_id', $userId)
->orderBy('created_at', 'DESC')
->limit(20)
->findAll();
foreach ($logs as $entry) {
echo "[{$entry->created_at}] {$entry->action} from {$entry->ip_address}" . PHP_EOL;
}
Login Attempt Logging¶
Track all login attempts (success and failure) in the auth_logins table.
Configuration¶
// app/Config/Auth.php
// Options:
// Auth::RECORD_LOGIN_ATTEMPT_NONE (0) - Don't record anything
// Auth::RECORD_LOGIN_ATTEMPT_FAILURE (1) - Only record failures
// Auth::RECORD_LOGIN_ATTEMPT_ALL (2) - Record everything (default)
public int $recordLoginAttempt = \Daycry\Auth\Config\Auth::RECORD_LOGIN_ATTEMPT_ALL;
What Gets Stored¶
| Column | Description |
|---|---|
ip_address |
IP address of the request |
identifier |
The email (or username) used |
credential_type |
Type of credential (e.g., email_password) |
success |
1 for success, 0 for failure |
user_agent |
Browser/client string |
created_at |
Timestamp |
Query Login Attempts¶
use Daycry\Auth\Models\LoginModel;
$loginModel = model(LoginModel::class);
// All failures in the last hour from a specific IP
$suspiciousAttempts = $loginModel
->where('ip_address', '203.0.113.42')
->where('success', 0)
->where('created_at >', date('Y-m-d H:i:s', strtotime('-1 hour')))
->countAllResults();
Failed Attempt Blocking¶
Daycry Auth can automatically block an IP address that fails login too many times in a row.
Configuration¶
// app/Config/Auth.php
// Enable IP-based failed attempt blocking
public bool $enableInvalidAttempts = true;
// Maximum failed attempts before blocking
public int $maxAttempts = 10;
// How long to block the IP (seconds)
public int $timeBlocked = 3600; // 1 hour
When an IP address exceeds $maxAttempts, all subsequent requests from that IP receive a "too many attempts" error until $timeBlocked seconds have passed.
Note: This is IP-level blocking. For per-user account lockout, see the section below.
Per-User Account Lockout¶
Independent of IP blocking, you can lock individual user accounts after too many failed password attempts. This is stored on the users table (failed_login_count, locked_until).
Configuration¶
// app/Config/Auth.php
// Maximum failed password attempts before locking the account
// Set to 0 to disable per-user lockout
public int $userMaxAttempts = 5;
// How long to lock the account (seconds)
public int $userLockoutTime = 3600; // 1 hour
How It Works¶
- A user fails to log in →
failed_login_countincrements - When
failed_login_count >= userMaxAttempts→locked_untilis set - Any login attempt before
locked_untilreturns an error with minutes remaining - After lockout expires → counter resets automatically on next attempt
- After a successful login → counter resets to 0
Unlocking a User Manually¶
// In an admin controller or command
$user = model(\Daycry\Auth\Models\UserModel::class)->find($userId);
model(\Daycry\Auth\Models\UserModel::class)->update($user->id, [
'failed_login_count' => 0,
'locked_until' => null,
]);
Rate Limiting¶
Rate limiting controls how many requests a client can make in a time window, independent of login failures.
Configuration¶
// app/Config/Auth.php
// How to identify the rate limit subject
// Options: 'IP_ADDRESS', 'USER', 'METHOD_NAME', 'ROUTED_URL'
public string $limitMethod = 'IP_ADDRESS';
// Maximum requests per window
public int $requestLimit = 60;
// Time window (seconds)
public int $timeLimit = MINUTE;
Apply Rate Limiting to Routes¶
// app/Config/Routes.php
// Limit all auth routes
$routes->group('auth', ['filter' => 'rates'], static function ($routes) {
$routes->post('login', 'LoginController::loginAction');
$routes->post('register', 'RegisterController::registerAction');
});
The
ratesalias is auto-registered byDaycry\Auth\Config\Registrar::Filters()— you don't declare it inapp/Config/Filters.php. Use a per-route limit with arguments, e.g.['filter' => 'rates:50,MINUTE']. See the Filters guide.
Audit Log (auth_audit_logs)¶
A second log table — auth_audit_logs — captures account-level events that need long-term traceability, distinct from request-level activity (auth_logs) and login attempts (auth_logins):
| Table | Granularity | Use |
|---|---|---|
auth_logs |
Per request | Request-level activity log (controller, URI, response code) |
auth_logins |
Per login attempt | Successful + failed login attempts |
auth_audit_logs |
Per account event | Sensitive account changes (2FA, password, role, lockout) |
Built-in events¶
The \Daycry\Auth\Services\AuditLogger service records 22 canonical event types — see the Audit & Compliance reference for the full list. Highlights:
- TOTP enable/disable, admin reset
- Password change, password reset
- User lockout / unlock
- Group / permission grant / revoke
- Token / refresh-token revoke
- Trusted device added
- Suspicious login
- User anonymization (GDPR)
Recording your own events¶
use Daycry\Auth\Services\AuditLogger;
(new AuditLogger())->record(
AuditLogger::EVENT_PASSWORD_CHANGED,
userId: $user->id,
metadata: ['source' => 'profile_form'],
);
Querying¶
# CLI
php spark auth:audit --user=alice@example.com --since=30d
php spark auth:audit --type=login.suspicious --limit=200
// Code
use Daycry\Auth\Models\AuditLogModel;
$audit = model(AuditLogModel::class);
$entries = $audit->recentForUser($userId, 50);
foreach ($entries as $entry) {
echo $entry->event_type . ' at ' . $entry->created_at . "\n";
var_dump($entry->getMetadata());
}
Failures inside
AuditLogger::record()are caught and logged atwarning— audit failure must never break the user-facing flow.
See Audit & Compliance for the full feature documentation.
Monitoring & Querying Logs¶
Dashboard Statistics¶
use Daycry\Auth\Models\LoginModel;
$loginModel = model(LoginModel::class);
$stats = [
'total_logins_today' => $loginModel
->where('success', 1)
->where('created_at >', date('Y-m-d 00:00:00'))
->countAllResults(),
'failed_today' => $loginModel
->where('success', 0)
->where('created_at >', date('Y-m-d 00:00:00'))
->countAllResults(),
'unique_ips_today' => $loginModel
->select('ip_address')
->where('created_at >', date('Y-m-d 00:00:00'))
->distinct()
->countAllResults(),
];
Find Locked Accounts¶
$lockedUsers = model(\Daycry\Auth\Models\UserModel::class)
->where('locked_until >', date('Y-m-d H:i:s'))
->findAll();
foreach ($lockedUsers as $user) {
echo "{$user->email} — locked until {$user->locked_until}" . PHP_EOL;
}
CI4 Log Files¶
Beyond the database, standard CI4 log files in writable/logs/ capture log messages written with log_message():
Events::on('failedLogin', static function (array $credentials): void {
log_message('warning', 'Failed login: ' . json_encode($credentials));
});
Set the log threshold in app/Config/Logger.php:
🔗 See also:
- Configuration — All logging configuration options
- Per-User Lockout & Password Reset — Security features
- Filters — rates filter