Files
breadcrumb-the-shire/lib/Service/Auth/RememberMeService.php

185 lines
6.3 KiB
PHP
Raw Normal View History

2026-02-04 23:31:53 +01:00
<?php
2026-02-11 19:28:12 +01:00
namespace MintyPHP\Service\Auth;
2026-02-04 23:31:53 +01:00
2026-02-23 12:58:19 +01:00
use MintyPHP\Auth;
2026-03-06 00:44:52 +01:00
use MintyPHP\Http\CookieStoreInterface;
use MintyPHP\Http\RequestRuntimeInterface;
use MintyPHP\Http\SessionStoreInterface;
use MintyPHP\I18n;
2026-03-05 08:26:51 +01:00
use MintyPHP\Repository\Auth\RememberTokenRepositoryInterface;
use MintyPHP\Repository\User\UserReadRepositoryInterface;
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
2026-02-23 12:58:19 +01:00
use MintyPHP\Session;
2026-02-04 23:31:53 +01:00
class RememberMeService
{
private const COOKIE_NAME = 'remember';
private const LIFETIME_DAYS = 30;
2026-02-23 12:58:19 +01:00
public function __construct(
2026-03-05 08:26:51 +01:00
private readonly UserReadRepositoryInterface $userReadRepository,
private readonly UserWriteRepositoryInterface $userWriteRepository,
private readonly RememberTokenRepositoryInterface $rememberTokenRepository,
2026-02-23 12:58:19 +01:00
private readonly AuthPermissionGateway $permissionGateway,
2026-03-06 00:44:52 +01:00
private readonly AuthSessionTenantContextService $authSessionTenantContextService,
private readonly SessionStoreInterface $sessionStore,
private readonly CookieStoreInterface $cookieStore,
private readonly RequestRuntimeInterface $requestRuntime
2026-02-23 12:58:19 +01:00
) {
}
public function rememberUser(int $userId): void
2026-02-04 23:31:53 +01:00
{
$selector = bin2hex(random_bytes(12));
$token = bin2hex(random_bytes(32));
$tokenHash = hash('sha256', $token);
2026-02-23 12:58:19 +01:00
$expiresAt = gmdate('Y-m-d H:i:s', time() + $this->lifetimeSeconds());
$this->rememberTokenRepository->create($userId, $selector, $tokenHash, $expiresAt);
$this->setCookie($selector, $token);
2026-02-04 23:31:53 +01:00
}
2026-02-23 12:58:19 +01:00
public function autoLoginFromCookie(): bool
2026-02-04 23:31:53 +01:00
{
2026-03-06 00:44:52 +01:00
$sessionUser = $this->sessionStore->get('user', []);
if (is_array($sessionUser) && !empty($sessionUser['id'])) {
2026-02-04 23:31:53 +01:00
return false;
}
2026-03-06 00:44:52 +01:00
$value = $this->cookieStore->get($this->cookieName());
2026-02-04 23:31:53 +01:00
if ($value === '' || strpos($value, ':') === false) {
return false;
}
[$selector, $token] = explode(':', $value, 2);
$selector = trim($selector);
$token = trim($token);
if ($selector === '' || $token === '') {
2026-02-23 12:58:19 +01:00
$this->clearCookie();
2026-02-04 23:31:53 +01:00
return false;
}
2026-02-23 12:58:19 +01:00
$record = $this->rememberTokenRepository->findBySelector($selector);
2026-02-04 23:31:53 +01:00
if (!$record) {
2026-02-23 12:58:19 +01:00
$this->clearCookie();
2026-02-04 23:31:53 +01:00
return false;
}
$expiresAt = (string) ($record['expires_at'] ?? '');
if ($expiresAt !== '' && strtotime($expiresAt . ' UTC') <= time()) {
2026-02-23 12:58:19 +01:00
$this->rememberTokenRepository->deleteById((int) $record['id']);
$this->clearCookie();
2026-02-04 23:31:53 +01:00
return false;
}
$hash = (string) ($record['token_hash'] ?? '');
if ($hash === '' || !hash_equals($hash, hash('sha256', $token))) {
2026-02-23 12:58:19 +01:00
$this->rememberTokenRepository->deleteById((int) $record['id']);
$this->clearCookie();
2026-02-04 23:31:53 +01:00
return false;
}
$userId = (int) ($record['user_id'] ?? 0);
if ($userId <= 0) {
2026-02-23 12:58:19 +01:00
$this->clearCookie();
2026-02-04 23:31:53 +01:00
return false;
}
2026-02-23 12:58:19 +01:00
$user = $this->userReadRepository->find($userId);
2026-02-04 23:31:53 +01:00
if (!$user || empty($user['id']) || !($user['active'] ?? 1)) {
2026-02-23 12:58:19 +01:00
$this->rememberTokenRepository->deleteById((int) $record['id']);
$this->clearCookie();
2026-02-04 23:31:53 +01:00
return false;
}
Session::regenerate();
2026-03-06 00:44:52 +01:00
$this->sessionStore->set('user', $user);
2026-02-04 23:31:53 +01:00
if (!empty($user['locale'])) {
I18n::$locale = (string) $user['locale'];
}
$userId = (int) $user['id'];
2026-02-04 23:31:53 +01:00
if ($userId > 0) {
2026-02-23 12:58:19 +01:00
$this->permissionGateway->warmUserPermissions($userId, true);
2026-03-06 00:44:52 +01:00
$this->authSessionTenantContextService->hydrateForUser($userId);
if ((bool) $this->sessionStore->get('no_active_tenant', false)) {
// Enforce the same tenant policy as interactive login.
2026-02-23 12:58:19 +01:00
$this->forgetCurrentUser();
Auth::logout();
return false;
2026-02-11 19:28:12 +01:00
}
2026-02-23 12:58:19 +01:00
$this->userWriteRepository->updateLastLogin($userId, 'local');
2026-02-04 23:31:53 +01:00
}
2026-03-06 00:44:52 +01:00
// Rotate token on each auto-login — limits the window if a cookie is stolen.
2026-02-04 23:31:53 +01:00
$newToken = bin2hex(random_bytes(32));
$newHash = hash('sha256', $newToken);
2026-02-23 12:58:19 +01:00
$newExpires = gmdate('Y-m-d H:i:s', time() + $this->lifetimeSeconds());
$this->rememberTokenRepository->updateToken((int) $record['id'], $newHash, $newExpires);
$this->setCookie($selector, $newToken);
2026-02-04 23:31:53 +01:00
return true;
}
2026-02-23 12:58:19 +01:00
public function forgetCurrentUser(): void
2026-02-04 23:31:53 +01:00
{
2026-03-06 00:44:52 +01:00
$value = $this->cookieStore->get($this->cookieName());
2026-02-04 23:31:53 +01:00
if ($value !== '' && strpos($value, ':') !== false) {
[$selector] = explode(':', $value, 2);
$selector = trim($selector);
if ($selector !== '') {
2026-02-23 12:58:19 +01:00
$record = $this->rememberTokenRepository->findBySelector($selector);
2026-02-04 23:31:53 +01:00
if ($record && isset($record['id'])) {
2026-02-23 12:58:19 +01:00
$this->rememberTokenRepository->deleteById((int) $record['id']);
2026-02-04 23:31:53 +01:00
}
}
}
2026-02-23 12:58:19 +01:00
$this->clearCookie();
2026-02-04 23:31:53 +01:00
}
2026-02-23 12:58:19 +01:00
public function forgetAllForUser(int $userId): void
2026-02-04 23:31:53 +01:00
{
2026-02-23 12:58:19 +01:00
$this->rememberTokenRepository->deleteByUserId($userId);
2026-02-04 23:31:53 +01:00
}
2026-02-23 12:58:19 +01:00
public function expireAllTokensByAdmin(): int
2026-02-11 19:28:12 +01:00
{
2026-02-23 12:58:19 +01:00
return $this->rememberTokenRepository->expireAllByAdmin();
2026-02-11 19:28:12 +01:00
}
2026-02-23 12:58:19 +01:00
private function setCookie(string $selector, string $token): void
2026-02-04 23:31:53 +01:00
{
$value = $selector . ':' . $token;
2026-02-23 12:58:19 +01:00
$expires = time() + $this->lifetimeSeconds();
2026-03-06 00:44:52 +01:00
$secure = $this->requestRuntime->isSecure();
2026-02-04 23:31:53 +01:00
2026-03-06 00:44:52 +01:00
$this->cookieStore->set($this->cookieName(), $value, [
2026-02-04 23:31:53 +01:00
'expires' => $expires,
'path' => '/',
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax',
]);
}
2026-02-23 12:58:19 +01:00
private function clearCookie(): void
2026-02-04 23:31:53 +01:00
{
2026-03-06 00:44:52 +01:00
$secure = $this->requestRuntime->isSecure();
$this->cookieStore->remove($this->cookieName(), [
2026-02-04 23:31:53 +01:00
'path' => '/',
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax',
]);
}
2026-02-23 12:58:19 +01:00
private function lifetimeSeconds(): int
2026-02-04 23:31:53 +01:00
{
return self::LIFETIME_DAYS * 24 * 60 * 60;
}
2026-02-23 12:58:19 +01:00
private function cookieName(): string
2026-02-04 23:31:53 +01:00
{
$name = getenv('REMEMBER_COOKIE_NAME') ?: self::COOKIE_NAME;
return trim($name) !== '' ? $name : self::COOKIE_NAME;
}
2026-02-23 12:58:19 +01:00
2026-02-04 23:31:53 +01:00
}