From a95c6f58ddf820f8ffd348286e814f7c348fef15 Mon Sep 17 00:00:00 2001 From: mscherer Date: Fri, 17 Apr 2026 00:08:02 +0200 Subject: [PATCH 1/5] Add redirectAfterLogin() helper --- .../Component/AuthenticationComponent.php | 22 ++++++ .../Component/AuthenticationComponentTest.php | 78 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/src/Controller/Component/AuthenticationComponent.php b/src/Controller/Component/AuthenticationComponent.php index a631927b..1836cccd 100644 --- a/src/Controller/Component/AuthenticationComponent.php +++ b/src/Controller/Component/AuthenticationComponent.php @@ -29,6 +29,7 @@ use Cake\Controller\Component; use Cake\Event\EventDispatcherInterface; use Cake\Event\EventDispatcherTrait; +use Cake\Http\Response; use Cake\Routing\Router; use Cake\Utility\Hash; use Exception; @@ -370,6 +371,27 @@ public function getLoginRedirect(array|string|null $default = null): ?string return $this->getAuthenticationService()->getLoginRedirect($this->getController()->getRequest()) ?? $default; } + /** + * Redirect after a successful login using the validated login redirect + * target from the current request when available. + * + * This is a convenience wrapper around `getLoginRedirect()` plus the + * controller's redirect method so applications can use the plugin's + * existing safe redirect parsing without manually reading query params. + * + * @param array|string|null $default Default URL to use when no valid login redirect is available. + * @return \Cake\Http\Response|null + */ + public function redirectAfterLogin(array|string|null $default = '/'): ?Response + { + $target = $this->getLoginRedirect($default) ?? $default; + if ($target === null) { + return null; + } + + return $this->getController()->redirect($target); + } + /** * Get the Controller callbacks this Component is interested in. * diff --git a/tests/TestCase/Controller/Component/AuthenticationComponentTest.php b/tests/TestCase/Controller/Component/AuthenticationComponentTest.php index 3c953bea..94550e5f 100644 --- a/tests/TestCase/Controller/Component/AuthenticationComponentTest.php +++ b/tests/TestCase/Controller/Component/AuthenticationComponentTest.php @@ -397,6 +397,84 @@ public function testGetLoginRedirect(): void Configure::delete('App.base'); } + /** + * testRedirectAfterLogin + * + * @return void + */ + public function testRedirectAfterLogin(): void + { + Configure::write('App.base', '/cakephp'); + $url = ['controller' => 'Users', 'action' => 'dashboard']; + Router::createRouteBuilder('/') + ->connect('/dashboard', $url); + + $this->service->setConfig('queryParam', 'redirect'); + $request = $this->request + ->withAttribute('identity', $this->identity) + ->withAttribute('authentication', $this->service) + ->withQueryParams(['redirect' => 'ok/path?value=key']); + + $controller = new Controller($request); + $registry = new ComponentRegistry($controller); + $component = new AuthenticationComponent($registry); + + $response = $component->redirectAfterLogin($url); + $this->assertSame('/ok/path?value=key', $response?->getHeaderLine('Location')); + + Configure::delete('App.base'); + } + + /** + * testRedirectAfterLoginFallsBackToDefaultForAbsoluteUrls + * + * @return void + */ + public function testRedirectAfterLoginFallsBackToDefaultForAbsoluteUrls(): void + { + $url = ['controller' => 'Users', 'action' => 'dashboard']; + Router::createRouteBuilder('/') + ->connect('/dashboard', $url); + + $this->service->setConfig('queryParam', 'redirect'); + $request = $this->request + ->withAttribute('identity', $this->identity) + ->withAttribute('authentication', $this->service) + ->withQueryParams(['redirect' => 'https://evil.example/phish']); + + $controller = new Controller($request); + $registry = new ComponentRegistry($controller); + $component = new AuthenticationComponent($registry); + + $response = $component->redirectAfterLogin($url); + $this->assertSame('/dashboard', $response?->getHeaderLine('Location')); + } + + /** + * testRedirectAfterLoginFallsBackToDefaultForProtocolRelativeUrls + * + * @return void + */ + public function testRedirectAfterLoginFallsBackToDefaultForProtocolRelativeUrls(): void + { + $url = ['controller' => 'Users', 'action' => 'dashboard']; + Router::createRouteBuilder('/') + ->connect('/dashboard', $url); + + $this->service->setConfig('queryParam', 'redirect'); + $request = $this->request + ->withAttribute('identity', $this->identity) + ->withAttribute('authentication', $this->service) + ->withQueryParams(['redirect' => '//evil.example/phish']); + + $controller = new Controller($request); + $registry = new ComponentRegistry($controller); + $component = new AuthenticationComponent($registry); + + $response = $component->redirectAfterLogin($url); + $this->assertSame('/dashboard', $response?->getHeaderLine('Location')); + } + /** * testAfterIdentifyEvent * From a3d18ea1c339bb154ad608fd37c85106905107d2 Mon Sep 17 00:00:00 2001 From: mscherer Date: Fri, 17 Apr 2026 00:24:58 +0200 Subject: [PATCH 2/5] Fix redirectAfterLogin test expectation --- .../Controller/Component/AuthenticationComponentTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase/Controller/Component/AuthenticationComponentTest.php b/tests/TestCase/Controller/Component/AuthenticationComponentTest.php index 94550e5f..7482bd1d 100644 --- a/tests/TestCase/Controller/Component/AuthenticationComponentTest.php +++ b/tests/TestCase/Controller/Component/AuthenticationComponentTest.php @@ -420,7 +420,7 @@ public function testRedirectAfterLogin(): void $component = new AuthenticationComponent($registry); $response = $component->redirectAfterLogin($url); - $this->assertSame('/ok/path?value=key', $response?->getHeaderLine('Location')); + $this->assertSame('/cakephp/ok/path?value=key', $response?->getHeaderLine('Location')); Configure::delete('App.base'); } From 51a5ef28d5f41f2c9bab7b68704ec5dc679bb135 Mon Sep 17 00:00:00 2001 From: mscherer Date: Fri, 17 Apr 2026 00:28:30 +0200 Subject: [PATCH 3/5] Document redirectAfterLogin() usage --- docs/en/authentication-component.md | 31 +++++++++++++++++++++ docs/en/authenticators.md | 20 +++++++------ docs/en/index.md | 4 +-- docs/en/migration-from-the-authcomponent.md | 19 +++++++------ 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/docs/en/authentication-component.md b/docs/en/authentication-component.md index a98f8731..f1bb24dd 100644 --- a/docs/en/authentication-component.md +++ b/docs/en/authentication-component.md @@ -121,3 +121,34 @@ option or by calling `disableIdentityCheck` from the controller's `beforeFilter( ``` php $this->Authentication->disableIdentityCheck(); ``` + +## Redirecting after login + +For the common post-login redirect flow, use `redirectAfterLogin()`: + +``` php +public function login(): ?\Cake\Http\Response +{ + $result = $this->Authentication->getResult(); + + if ($result && $result->isValid()) { + return $this->Authentication->redirectAfterLogin('/home'); + } + + return null; +} +``` + +This uses the plugin's validated login redirect target from the current +request when available and falls back to the default you provide. + +If you need to inspect the validated target before redirecting, use +`getLoginRedirect()` instead: + +``` php +$target = $this->Authentication->getLoginRedirect('/home'); +return $this->redirect($target); +``` + +Avoid reading raw `redirect` query string parameters and passing them directly +to the controller's `redirect()` method. diff --git a/docs/en/authenticators.md b/docs/en/authenticators.md index 0f2e9beb..6bc855a0 100644 --- a/docs/en/authenticators.md +++ b/docs/en/authenticators.md @@ -581,8 +581,8 @@ $service->setConfig([ ]); ``` -Then in your controller's login method you can use `getLoginRedirect()` to get -the redirect target safely from the query string parameter: +Then in your controller's login method you can use +`redirectAfterLogin()` for the common safe post-login redirect flow: ``` php public function login(): ?\Cake\Http\Response @@ -591,19 +591,21 @@ public function login(): ?\Cake\Http\Response // Regardless of POST or GET, redirect if user is logged in if ($result->isValid()) { - // Use the redirect parameter if present. - $target = $this->Authentication->getLoginRedirect(); - if (!$target) { - $target = ['controller' => 'Pages', 'action' => 'display', 'home']; - } - - return $this->redirect($target); + return $this->Authentication->redirectAfterLogin([ + 'controller' => 'Pages', + 'action' => 'display', + 'home', + ]); } return null; } ``` +If you need to inspect the validated target before redirecting, you can still +use `getLoginRedirect()` directly and handle the response yourself. Avoid +passing raw query string parameters to the controller's `redirect()` method. + ## Having Multiple Authentication Flows In an application that provides both an API and a web interface diff --git a/docs/en/index.md b/docs/en/index.md index 2956e3ee..8429edbb 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -173,9 +173,7 @@ public function login(): ?\Cake\Http\Response $result = $this->Authentication->getResult(); // If the user is logged in send them away. if ($result && $result->isValid()) { - $target = $this->Authentication->getLoginRedirect() ?? '/home'; - - return $this->redirect($target); + return $this->Authentication->redirectAfterLogin('/home'); } if ($this->request->is('post')) { $this->Flash->error('Invalid username or password'); diff --git a/docs/en/migration-from-the-authcomponent.md b/docs/en/migration-from-the-authcomponent.md index fcc63090..3e736436 100644 --- a/docs/en/migration-from-the-authcomponent.md +++ b/docs/en/migration-from-the-authcomponent.md @@ -288,8 +288,8 @@ $service->setConfig([ ]); ``` -Then in your controller's login method you can use `getLoginRedirect()` to get -the redirect target safely from the query string parameter: +Then in your controller's login method you can use +`redirectAfterLogin()` for the common safe post-login redirect flow: ``` php public function login(): ?\Cake\Http\Response @@ -298,19 +298,20 @@ public function login(): ?\Cake\Http\Response // Regardless of POST or GET, redirect if user is logged in if ($result->isValid()) { - // Use the redirect parameter if present. - $target = $this->Authentication->getLoginRedirect(); - if (!$target) { - $target = ['controller' => 'Pages', 'action' => 'display', 'home']; - } - - return $this->redirect($target); + return $this->Authentication->redirectAfterLogin([ + 'controller' => 'Pages', + 'action' => 'display', + 'home', + ]); } return null; } ``` +If you need to inspect the validated target before redirecting, you can still +use `getLoginRedirect()` directly and then call `redirect()` yourself. + ## Migrating Hashing Upgrade Logic If your application uses `AuthComponent`’s hash upgrade From 44d05bfaa9bb9d6558882868c5e431ad7c52a427 Mon Sep 17 00:00:00 2001 From: mscherer Date: Fri, 17 Apr 2026 00:31:20 +0200 Subject: [PATCH 4/5] Fix CI for redirectAfterLogin changes --- docs/en/authentication-component.md | 4 ++-- .../Controller/Component/AuthenticationComponentTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/authentication-component.md b/docs/en/authentication-component.md index f1bb24dd..a6c2321c 100644 --- a/docs/en/authentication-component.md +++ b/docs/en/authentication-component.md @@ -126,7 +126,7 @@ $this->Authentication->disableIdentityCheck(); For the common post-login redirect flow, use `redirectAfterLogin()`: -``` php +```php public function login(): ?\Cake\Http\Response { $result = $this->Authentication->getResult(); @@ -145,7 +145,7 @@ request when available and falls back to the default you provide. If you need to inspect the validated target before redirecting, use `getLoginRedirect()` instead: -``` php +```php $target = $this->Authentication->getLoginRedirect('/home'); return $this->redirect($target); ``` diff --git a/tests/TestCase/Controller/Component/AuthenticationComponentTest.php b/tests/TestCase/Controller/Component/AuthenticationComponentTest.php index 7482bd1d..16b71a0a 100644 --- a/tests/TestCase/Controller/Component/AuthenticationComponentTest.php +++ b/tests/TestCase/Controller/Component/AuthenticationComponentTest.php @@ -420,7 +420,7 @@ public function testRedirectAfterLogin(): void $component = new AuthenticationComponent($registry); $response = $component->redirectAfterLogin($url); - $this->assertSame('/cakephp/ok/path?value=key', $response?->getHeaderLine('Location')); + $this->assertSame(Router::url('/ok/path?value=key'), $response?->getHeaderLine('Location')); Configure::delete('App.base'); } From 97f2bdb9782ccefae005947341cddb173872a316 Mon Sep 17 00:00:00 2001 From: mscherer Date: Fri, 17 Apr 2026 12:23:46 +0200 Subject: [PATCH 5/5] Drop null parameter from redirectAfterLogin() The helper always has a sensible default ('/'), so accepting null as an explicit argument added no value and required a dead null-check branch. The return type stays nullable because Controller::redirect() itself can return null when a beforeRedirect event cancels the redirect. --- src/Controller/Component/AuthenticationComponent.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Controller/Component/AuthenticationComponent.php b/src/Controller/Component/AuthenticationComponent.php index 1836cccd..5ce0d7d3 100644 --- a/src/Controller/Component/AuthenticationComponent.php +++ b/src/Controller/Component/AuthenticationComponent.php @@ -379,15 +379,12 @@ public function getLoginRedirect(array|string|null $default = null): ?string * controller's redirect method so applications can use the plugin's * existing safe redirect parsing without manually reading query params. * - * @param array|string|null $default Default URL to use when no valid login redirect is available. + * @param array|string $default Default URL to use when no valid login redirect is available. * @return \Cake\Http\Response|null */ - public function redirectAfterLogin(array|string|null $default = '/'): ?Response + public function redirectAfterLogin(array|string $default = '/'): ?Response { $target = $this->getLoginRedirect($default) ?? $default; - if ($target === null) { - return null; - } return $this->getController()->redirect($target); }