From e1c211069ece0e58b44fee839791ccb8df8c2fce Mon Sep 17 00:00:00 2001 From: fs Date: Fri, 13 Mar 2026 18:28:13 +0100 Subject: [PATCH] fix(session): preserve intended URL across session timeout with remember-me MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Session timeout redirected to /de/login?return_to=... but the remember-me cookie was not cleared by logoutDueToSessionTimeout(). On the next request, autoLoginFromCookie() silently re-logged the user in, causing the MintyPHP router to resolve the login route to pages/index().php (dashboard) instead of the login action — the intended URL redirect logic never executed. Fix: intercept ?return_to= immediately after successful auto-login in index.php and redirect to the sanitized route path. Also harden the login flow with hidden return_to form fields and POST body fallback for cases without remember-me. Additional improvements in this commit: - Convert stored REQUEST_URI to router-compatible path (strip leading slash and locale prefix) via IntendedUrlService::toRoutePath() - Consolidate duplicate CSRF data attributes into shared data-csrf-key and data-csrf-token on element - Add tenant branding (logo) to auth pages via appAuthLogoUrl() helper Co-Authored-By: Claude Opus 4.6 --- lib/Http/IntendedUrlService.php | 31 ++++++++++++++++++++++- lib/Support/helpers/app.php | 18 +++++++++++++ pages/auth/login().php | 11 +++++++- pages/auth/login(login).phtml | 6 +++++ templates/default.phtml | 6 ++--- templates/partials/auth-logo.phtml | 2 +- tests/Http/IntendedUrlServiceTest.php | 32 ++++++++++++++++++++++-- web/index.php | 15 ++++++++++- web/js/components/app-session-warning.js | 4 +-- web/js/core/app-telemetry.js | 4 +-- 10 files changed, 115 insertions(+), 14 deletions(-) diff --git a/lib/Http/IntendedUrlService.php b/lib/Http/IntendedUrlService.php index 236900f..4631674 100644 --- a/lib/Http/IntendedUrlService.php +++ b/lib/Http/IntendedUrlService.php @@ -47,12 +47,22 @@ class IntendedUrlService /** * Retrieve the intended URL and remove it from the session. * Returns the fallback ('admin') if no valid URL is stored. + * + * The stored URL is a full REQUEST_URI (e.g. /de/admin/users?page=2) + * but Router::redirect() expects a route path without leading slash + * and without locale prefix (e.g. admin/users?page=2), because it + * prepends getBaseUrl() which already includes the base path. */ public function retrieveAndForget(SessionStoreInterface $session): string { $value = $this->retrieve($session); $session->remove(self::SESSION_KEY); - return $value ?? self::FALLBACK; + + if ($value === null) { + return self::FALLBACK; + } + + return $this->toRoutePath($value); } /** @@ -103,6 +113,25 @@ class IntendedUrlService return $url; } + /** + * Convert a full REQUEST_URI into a Router::redirect()-compatible route path. + * + * /de/admin/users?page=2 → admin/users?page=2 + * /admin/users → admin/users + */ + public function toRoutePath(string $url): string + { + // Remove leading slash + $path = ltrim($url, '/'); + + // Remove locale prefix (2-letter code followed by /) + if (preg_match('#^[a-z]{2}/#', $path)) { + $path = substr($path, 3); + } + + return $path !== '' ? $path : self::FALLBACK; + } + /** * Extract the path portion without query string and without locale prefix. */ diff --git a/lib/Support/helpers/app.php b/lib/Support/helpers/app.php index 8f352f0..7fb0d7d 100644 --- a/lib/Support/helpers/app.php +++ b/lib/Support/helpers/app.php @@ -450,6 +450,24 @@ function appLogoUrl(?int $size = null): string return asset('brand/logo.svg'); } +/** + * Resolve logo for auth pages: tenant avatar → global logo → default SVG. + * + * After logout the tenant context (`$_SESSION['current_tenant']`) is preserved, + * so the login page can show the tenant avatar instead of the generic app logo. + */ +function appAuthLogoUrl(?int $size = null): string +{ + $tenantUuid = $_SESSION['current_tenant']['uuid'] ?? ''; + if ($tenantUuid !== '' && class_exists('MintyPHP\\Service\\Tenant\\TenantAvatarService')) { + if (app(\MintyPHP\Service\Tenant\TenantAvatarService::class)->hasAvatar($tenantUuid)) { + $query = $size ? '&size=' . (int) $size : ''; + return lurl('auth/tenant-avatar-file?uuid=' . rawurlencode($tenantUuid) . $query); + } + } + return appLogoUrl($size); +} + /** * Resolve favicon path (tenant favicon with global fallback). */ diff --git a/pages/auth/login().php b/pages/auth/login().php index e36a5ee..68e2c7b 100644 --- a/pages/auth/login().php +++ b/pages/auth/login().php @@ -15,12 +15,21 @@ $requestRuntime = app(RequestRuntimeInterface::class); $sessionStore = app(SessionStoreInterface::class); $intendedUrlService = app(IntendedUrlService::class); -// Store return_to from query parameter (set by session timeout or auth guard redirect). +// Store return_to from query parameter (set by session timeout or auth guard redirect) +// or from hidden form field (preserved through multi-stage login POST flow). $returnTo = trim((string) (requestInput()->queryAll()['return_to'] ?? '')); +if ($returnTo === '') { + $returnTo = trim((string) (requestInput()->bodyAll()['return_to'] ?? '')); +} + if ($returnTo !== '') { $intendedUrlService->store($returnTo, $sessionStore); } +// Expose sanitized return_to for hidden form fields so it survives across +// POST stages even if the query string or session is lost. +$returnToSanitized = $intendedUrlService->sanitize($returnTo !== '' ? $returnTo : ($intendedUrlService->retrieve($sessionStore) ?? '')); + $session = $sessionStore->all(); if (!empty($session['user']['id'])) { diff --git a/pages/auth/login(login).phtml b/pages/auth/login(login).phtml index ca384be..5befa9c 100644 --- a/pages/auth/login(login).phtml +++ b/pages/auth/login(login).phtml @@ -58,6 +58,7 @@ $authLogoHref = lurl('login'); if ($tenantHint !== '') { $authLogoHref .= '?tenant=' . rawurlencode($tenantHint); } +$returnToSanitized = (string) ($returnToSanitized ?? ''); ?> @@ -85,6 +86,7 @@ if ($tenantHint !== '') {
+