-
Notifications
You must be signed in to change notification settings - Fork 2
User
Pair\Models\User is Pair's central authentication model. It extends ActiveRecord and coordinates:
- local login with password verification
- session-free login for native mobile/API bearer sessions
- login by external factor (
doLoginById()), useful for Passkey or SSO flows - ACL checks against modules and actions
- session creation and logout
- remember-me cookies and token rotation
- locale, timezone, landing-page, and impersonation helpers
Use User whenever backend code needs authenticated identity, access checks, session bootstrap, password-reset completion, or remember-me behavior.
This is the main entry point for local username or email login.
Current behavior:
- loads the user by
usernameoremaildepending onPAIR_AUTH_BY_EMAIL - rate limits password attempts by normalized identifier and client IP
- rejects disabled users and users with more than 9 failed attempts
- verifies the password with
checkPassword() - creates a
Session, updateslastLogin, resetsfaults, clearspwReset, and writes audit logs - returns an object with
error,message,userId, andsessionId
The rate limiter is controlled by PAIR_AUTH_RATE_LIMIT_ENABLED, PAIR_AUTH_RATE_LIMIT_MAX_ATTEMPTS, and PAIR_AUTH_RATE_LIMIT_DECAY_SECONDS. Blocked attempts still return the generic authentication failure message, so the response does not reveal whether the identifier exists.
Typical controller usage:
use App\Models\User;
$timezone = (string)($_POST['timezone'] ?? 'Europe/Rome');
$result = User::doLogin(
trim((string)($_POST['username'] ?? '')),
(string)($_POST['password'] ?? ''),
$timezone
);
if ($result->error) {
$this->toastError('Login failed', (string)$result->message);
return;
}
$user = new User((int)$result->userId);
if (!empty($_POST['remember'])) {
$user->createRememberMe($timezone);
}
$this->redirect('dashboard');Use this for mobile/API login flows that must verify local credentials without creating a PHP session or mutating browser remember-me state.
Current behavior:
- loads the user by
usernameoremaildepending onPAIR_AUTH_BY_EMAIL - uses the same AuthAttemptLimiter as
doLogin() - rejects disabled users and users with more than 9 failed attempts
- verifies the password with
checkPassword() - updates
lastLogin, resetsfaults, clearspwReset, and writes audit logs - does not create a
Session, set cookies, revoke browser remember-me records, or close other web sessions
The built-in ApiController::authAction() uses this method before issuing an ApiToken. Both token login and normal web login are limited by normalized identifier and client IP.
use App\Models\User;
use Pair\Models\ApiToken;
$result = User::doTokenLogin($email, $password);
if ($result->error) {
return null;
}
$user = new User((int)$result->userId);
$issued = ApiToken::issueForUser($user);Use this when the identity has already been verified by another factor, for example Passkey/WebAuthn, OAuth callback handling, or a trusted SSO flow.
It follows the same safety rules as doLogin(): locked or disabled users are still rejected, and a normal Pair session is created.
Example from an API or Passkey flow:
use App\Models\User;
use Pair\Api\ApiResponse;
$result = User::doLoginById($verifiedUserId, 'Europe/Rome');
if ($result->error) {
return ApiResponse::errorResponse('UNAUTHORIZED');
}
return ApiResponse::jsonResponse([
'userId' => $result->userId,
'sid' => $result->sessionId,
]);This closes the session, removes persistent state cookies, unsets remember-me data, resets Application::currentUser, and writes the logout audit entry.
use App\Models\User;
$ok = User::doLogout(session_id());
if ($ok) {
$this->redirect('user/login');
}This is the main ACL check used by Pair.
Current behavior:
- super users always pass
- the
usermodule is always allowed -
publicis always allowed - the method accepts either
module+actionor a singlemodule/actionstring - custom routes are resolved before ACL matching
- rules are loaded once and cached on the user object
Examples:
if (!$user->canAccess('orders', 'edit')) {
throw new \RuntimeException('Access denied');
}if ($user->canAccess('reports/export')) {
// module/action combined in one string
}6) Remember-me lifecycle: createRememberMe(), loginByRememberMe(), renewRememberMe(), unsetRememberMe()
These methods implement the persistent login flow.
createRememberMe():
- generates a random token
- stores only the hashed token in
user_remembers - writes a versioned cookie payload
- keeps other active remember-me tokens for the same user so remembered browsers/devices do not compete with each other
loginByRememberMe():
- is used automatically by
Applicationduring unauthenticated web requests - validates the cookie, loads the related user, creates a fresh session, rotates the remember-me token, and sets the current application user
renewRememberMe() rotates only the cookie and DB token pair represented by the current browser cookie.
unsetRememberMe() removes the current browser cookie and only the matching server-side token. Without a current remember-me cookie, Pair leaves other remembered devices untouched.
Example after a successful login:
$result = \App\Models\User::doLogin($username, $password, $timezone);
if (!$result->error && !empty($_POST['remember'])) {
$user = new \App\Models\User((int)$result->userId);
$user->createRememberMe($timezone);
}Example manual auto-login check in a custom bootstrap path:
if (\App\Models\User::loginByRememberMe()) {
\Pair\Core\Application::getInstance()->redirectToUserDefault();
}landing() returns the default module/action for the user's ACL group. redirectToDefault() turns that into a browser redirect.
$landing = $user->landing();
if ($landing) {
echo $landing->module . '/' . $landing->action;
}$user->redirectToDefault();The main reset path is:
getByPwReset(string $token): ?UsersetNewPassword(string $newPassword, string $timezone): bool
setNewPassword() clears the reset token, stores the new hash, creates a new session, resets faults, and writes the audit event.
use App\Models\User;
$user = User::getByPwReset((string)($_GET['token'] ?? ''));
if (!$user) {
throw new \RuntimeException('Invalid reset token');
}
$user->setNewPassword((string)$_POST['password'], 'Europe/Rome');The low-level helpers are also useful:
-
checkPassword($plain, $hash)verifies a local password -
getHashedPasswordWithSalt($plain)builds the stored hash
New password hashes use Argon2id when the PHP runtime supports it, with bcrypt as the compatibility fallback. Successful web and token logins rehash older verified passwords in place when the stored algorithm is outdated.
impersonate() swaps the active session user while remembering the former user ID. impersonateStop() restores the original user. isSuper() also checks the former user during impersonation so elevated access is preserved correctly.
$admin->impersonate($targetUser);$currentUser->impersonateStop();-
current(): ?staticreturns theApplicationcurrent user ornull. -
avatar(string $classPrefix = 'user'): stringrenders initials with a deterministic template-based color. -
fullName(): stringreturns"name surname". - Virtual properties
fullNameandgroupNameare available through__get(). -
getGroup(): Grouploads the related group. -
getLocale(): Localereturns the stored locale or the default locale. -
getLanguageCode(): ?stringreturns the cached language code derived from the locale relation. -
getDateTimeZone(): DateTimeZonereads the current session timezone or falls back toBASE_TIMEZONE. -
getValidTimeZone(string $timezone): DateTimeZonevalidates an IANA timezone name. -
isSuper(): boolchecks both the current and former impersonating user. -
isDeletable(): boolrefuses self-deletion and then applies the normal ActiveRecord FK checks. -
isLocaleSet(): booltells whetherlocaleIdis currently set.
Authentication hooks:
-
beforeLogin(),afterLogin() afterLoginFailed()-
beforeLogout(),afterLogout()
Remember-me hooks:
-
beforeRememberMeCreate(),afterRememberMeCreate() -
beforeRememberMeLogin(),afterRememberMeLogin() -
beforeRememberMeRenew(),afterRememberMeRenew() -
beforeRememberMeUnset(),afterRememberMeUnset()
These are useful when you need application-specific audit, telemetry, or side effects without rewriting the core flow.
-
PAIR_AUTH_BY_EMAIL=trueswitches local login lookup fromusernametoemail. -
PAIR_SINGLE_SESSION=truedeletes the user's other sessions after a successful login. - The current implementation treats users with more than 9 faults as locked for login purposes.
- Remember-me cookies store a versioned payload, while the DB keeps only a deterministic hash of the token.
- Remember-me records are device-scoped: login, renewal, and logout for one browser do not revoke other remembered browsers unless the application adds an explicit account-level revocation flow.
See also: Session, AuthAttemptLimiter, Rule, Locale, UserRemember, OAuth2Token, SocialAuth, PasskeyAuth.