lifetimeSeconds()); $this->rememberTokenRepository->create($userId, $selector, $tokenHash, $expiresAt); $this->setCookie($selector, $token); } public function autoLoginFromCookie(): bool { $sessionUser = $this->sessionStore->get('user', []); if (is_array($sessionUser) && !empty($sessionUser['id'])) { return false; } $value = $this->cookieStore->get($this->cookieName()); if ($value === '' || strpos($value, ':') === false) { return false; } $ip = $this->requestRuntime->ip(); if ($this->rateLimiter !== null && $this->rateLimiter->isBlocked($ip)) { $this->clearCookie(); return false; } [$selector, $token] = explode(':', $value, 2); $selector = trim($selector); $token = trim($token); if ($selector === '' || $token === '') { $this->clearCookie(); return false; } $record = $this->rememberTokenRepository->findBySelector($selector); if (!$record) { $this->rateLimiter?->recordFailure($ip); $this->clearCookie(); return false; } $expiresAt = (string) ($record['expires_at'] ?? ''); if ($expiresAt !== '' && strtotime($expiresAt . ' UTC') <= time()) { $this->rememberTokenRepository->deleteById((int) $record['id']); $this->clearCookie(); return false; } $hash = (string) ($record['token_hash'] ?? ''); if ($hash === '' || !hash_equals($hash, hash('sha256', $token))) { $this->rateLimiter?->recordFailure($ip); $this->rememberTokenRepository->deleteById((int) $record['id']); $this->clearCookie(); return false; } $userId = (int) ($record['user_id'] ?? 0); if ($userId <= 0) { $this->clearCookie(); return false; } $user = $this->userReadRepository->find($userId); if (!$user || empty($user['id']) || !($user['active'] ?? 1)) { $this->rememberTokenRepository->deleteById((int) $record['id']); $this->clearCookie(); return false; } Session::regenerate(); $this->sessionStore->set('user', $user); $now = time(); $this->sessionStore->set('session_started_at', $now); $this->sessionStore->set('session_last_activity', $now); if (!empty($user['locale'])) { I18n::$locale = (string) $user['locale']; } $userId = (int) $user['id']; if ($userId > 0) { $this->permissionService->getUserPermissions($userId, true); $this->authSessionTenantContextService->hydrateForUser($userId); if ((bool) $this->sessionStore->get('no_active_tenant', false)) { // Enforce the same tenant policy as interactive login. $this->forgetCurrentUser(); Auth::logout(); return false; } $this->userWriteRepository->updateLastLogin($userId, 'local'); } // Rotate token on each auto-login — limits the window if a cookie is stolen. $newToken = bin2hex(random_bytes(32)); $newHash = hash('sha256', $newToken); $newExpires = gmdate('Y-m-d H:i:s', time() + $this->lifetimeSeconds()); $this->rememberTokenRepository->updateToken((int) $record['id'], $newHash, $newExpires); $this->setCookie($selector, $newToken); $this->rateLimiter?->reset($ip); return true; } public function forgetCurrentUser(): void { $value = $this->cookieStore->get($this->cookieName()); if ($value !== '' && strpos($value, ':') !== false) { [$selector] = explode(':', $value, 2); $selector = trim($selector); if ($selector !== '') { $record = $this->rememberTokenRepository->findBySelector($selector); if ($record && isset($record['id'])) { $this->rememberTokenRepository->deleteById((int) $record['id']); } } } $this->clearCookie(); } public function forgetAllForUser(int $userId): void { $this->rememberTokenRepository->deleteByUserId($userId); } public function expireAllTokensByAdmin(): int { return $this->rememberTokenRepository->expireAllByAdmin(); } private function setCookie(string $selector, string $token): void { $value = $selector . ':' . $token; $expires = time() + $this->lifetimeSeconds(); $secure = $this->requestRuntime->isSecure(); $this->cookieStore->set($this->cookieName(), $value, [ 'expires' => $expires, 'path' => '/', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax', ]); } private function clearCookie(): void { $secure = $this->requestRuntime->isSecure(); $this->cookieStore->remove($this->cookieName(), [ 'path' => '/', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax', ]); } private function lifetimeSeconds(): int { if ($this->authSettingsGateway !== null) { return $this->authSettingsGateway->getRememberTokenLifetimeDays() * 86400; } return self::LIFETIME_DAYS * 86400; } private function cookieName(): string { $name = getenv('REMEMBER_COOKIE_NAME') ?: self::COOKIE_NAME; return trim($name) !== '' ? $name : self::COOKIE_NAME; } }