diff --git a/docs/en/authentication-component.md b/docs/en/authentication-component.md index a98f8731..a6c2321c 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 diff --git a/src/Controller/Component/AuthenticationComponent.php b/src/Controller/Component/AuthenticationComponent.php index a631927b..5ce0d7d3 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,24 @@ 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 $default Default URL to use when no valid login redirect is available. + * @return \Cake\Http\Response|null + */ + public function redirectAfterLogin(array|string $default = '/'): ?Response + { + $target = $this->getLoginRedirect($default) ?? $default; + + 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..16b71a0a 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(Router::url('/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 *