Account Security Hooks
The nine hooks over verification, password resets, sign-outs and access.
Overview
Every step that guards the identity of an account lives here: email and phone verification, the security question, password resets, login codes and sign-outs.
Two gates stand out. The password gate hands you the raw password; only check it. The access gate works the other way round from the rest: a filled return skips the built-in checks.
Reference
Following an email change
Runs after the email address of an account changes. The new address is verified.
Hook::add('action:user.email_changed', 10, function ($uid, $oldEmail, $newEmail) {
// Tell the old address too: if the account was taken, that is the only warning path.
Acme::alertOldAddress($oldEmail, $newEmail);
});Following an email verification
Runs after a customer verifies their email address.
Hook::add('action:user.email_verified', 10, function ($user_id) {
Acme::unlockOnboarding($user_id);
});Following a phone verification
Runs after a customer verifies their phone number.
Hook::add('action:user.phone_verified', 10, function ($uid, $phone) {
// The number is in international form.
Acme::enableSmsAlerts($uid, $phone);
});Following a security question change
Runs when the security question of an account is set or removed.
Hook::add('action:user.security_question_changed', 10, function ($user_id, $has_question) {
// Removal comes through this hook too.
if (!$has_question) Acme::warnWeakerRecovery($user_id);
});Following a password reset request
Runs when a password reset is requested. Both customer and administrator accounts pass here.
member or admin. Administrator resets matter far more; keep the two apart.Hook::add('action:user.password_reset_requested', 10, function ($userId, $type, $email) {
// An administrator reset matters far more.
if ($type === 'admin') Acme::alertSecurityTeam($userId, $email);
});Following a sign-out
Runs when a session is closed.
admin or member.Hook::add('action:user.logged_out', 10, function ($data, $type) {
// With no session the array arrives EMPTY.
if (!($data['id'] ?? 0)) return;
Acme::closeSession((int) $data['id'], $type);
});Following a login code being sent
Runs when a one-time login code goes out.
Hook::add('action:user.login_code_issued', 10, function ($userId, $channel, $expiry) {
// The code itself is deliberately withheld.
Acme::noteLoginAttempt($userId, $channel);
});Following a device being trusted
Runs when an account ticks "trust this device" on the two-factor screen and the browser is remembered.
member or admin.Hook::add('action:user.device_trusted', 10, function ($userId, $type, $window, $ua) {
Acme::securityLog('device_trusted', $userId, ['type' => $type, 'days' => $window]);
});Following device trust being revoked
Runs when an account's trusted devices are removed: a password reset, two-factor being switched on or off, a recovery-code sign-in, "sign out everywhere", or a removal from the account's own list.
member or admin. On a bulk clear with no type given it arrives empty — every type went.1.Hook::add('action:user.device_trust_revoked', 10, function ($userId, $type, $count) {
Acme::securityLog('trust_revoked', $userId, ['count' => $count]);
});Stopping a password reset
Runs before an administrator password is reset. Enforce your own password policy here.
Hook::add('gate:user.password_reset', 10, function ($user_id, $password, $by) {
// The raw password: check it only, write it NOWHERE.
if (strlen($password) < 16) return 'An administrator password needs at least 16 characters.';
return null;
});Sending a customer to a screen of your own
Runs when a customer enters the panel, ahead of the built-in access checks: missing fields, data consent and billing details all come after it.
Hook::add('gate:user.full_access', 10, function ($id) {
// A filled return SKIPS THE BUILT-IN CHECKS: keep the condition narrow.
if (Acme::mustAcceptTerms($id)) return Utility::AppAdress() . '/acme/terms';
return null;
});Pitfalls
In other gates a filled return blocks; in the access gate a filled return redirects and skips every built-in check. Returning an address unconditionally switches off the missing-field and consent checks for good.
The reset gate hands you the new password in the clear, which a policy check needs. Logging it, storing it or sending it anywhere leaks exactly what you set out to protect.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.