Skip to content

Commit ed41893

Browse files
authored
Merge pull request #787 from cakephp/feat/redirect-after-login-helper
Add redirectAfterLogin() helper
2 parents 179719a + 97f2bdb commit ed41893

6 files changed

Lines changed: 150 additions & 21 deletions

File tree

docs/en/authentication-component.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,34 @@ option or by calling `disableIdentityCheck` from the controller's `beforeFilter(
121121
```php
122122
$this->Authentication->disableIdentityCheck();
123123
```
124+
125+
## Redirecting after login
126+
127+
For the common post-login redirect flow, use `redirectAfterLogin()`:
128+
129+
```php
130+
public function login(): ?\Cake\Http\Response
131+
{
132+
$result = $this->Authentication->getResult();
133+
134+
if ($result && $result->isValid()) {
135+
return $this->Authentication->redirectAfterLogin('/home');
136+
}
137+
138+
return null;
139+
}
140+
```
141+
142+
This uses the plugin's validated login redirect target from the current
143+
request when available and falls back to the default you provide.
144+
145+
If you need to inspect the validated target before redirecting, use
146+
`getLoginRedirect()` instead:
147+
148+
```php
149+
$target = $this->Authentication->getLoginRedirect('/home');
150+
return $this->redirect($target);
151+
```
152+
153+
Avoid reading raw `redirect` query string parameters and passing them directly
154+
to the controller's `redirect()` method.

docs/en/authenticators.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -577,8 +577,8 @@ $service->setConfig([
577577
]);
578578
```
579579

580-
Then in your controller's login method you can use `getLoginRedirect()` to get
581-
the redirect target safely from the query string parameter:
580+
Then in your controller's login method you can use
581+
`redirectAfterLogin()` for the common safe post-login redirect flow:
582582

583583
```php
584584
public function login(): ?\Cake\Http\Response
@@ -587,19 +587,21 @@ public function login(): ?\Cake\Http\Response
587587

588588
// Regardless of POST or GET, redirect if user is logged in
589589
if ($result->isValid()) {
590-
// Use the redirect parameter if present.
591-
$target = $this->Authentication->getLoginRedirect();
592-
if (!$target) {
593-
$target = ['controller' => 'Pages', 'action' => 'display', 'home'];
594-
}
595-
596-
return $this->redirect($target);
590+
return $this->Authentication->redirectAfterLogin([
591+
'controller' => 'Pages',
592+
'action' => 'display',
593+
'home',
594+
]);
597595
}
598596

599597
return null;
600598
}
601599
```
602600

601+
If you need to inspect the validated target before redirecting, you can still
602+
use `getLoginRedirect()` directly and handle the response yourself. Avoid
603+
passing raw query string parameters to the controller's `redirect()` method.
604+
603605
## Having Multiple Authentication Flows
604606

605607
In an application that provides both an API and a web interface

docs/en/index.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,7 @@ public function login(): ?\Cake\Http\Response
172172
$result = $this->Authentication->getResult();
173173
// If the user is logged in send them away.
174174
if ($result && $result->isValid()) {
175-
$target = $this->Authentication->getLoginRedirect() ?? '/home';
176-
177-
return $this->redirect($target);
175+
return $this->Authentication->redirectAfterLogin('/home');
178176
}
179177
if ($this->request->is('post')) {
180178
$this->Flash->error('Invalid username or password');

docs/en/migration-from-the-authcomponent.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,8 @@ $service->setConfig([
286286
]);
287287
```
288288

289-
Then in your controller's login method you can use `getLoginRedirect()` to get
290-
the redirect target safely from the query string parameter:
289+
Then in your controller's login method you can use
290+
`redirectAfterLogin()` for the common safe post-login redirect flow:
291291

292292
```php
293293
public function login(): ?\Cake\Http\Response
@@ -296,19 +296,20 @@ public function login(): ?\Cake\Http\Response
296296

297297
// Regardless of POST or GET, redirect if user is logged in
298298
if ($result->isValid()) {
299-
// Use the redirect parameter if present.
300-
$target = $this->Authentication->getLoginRedirect();
301-
if (!$target) {
302-
$target = ['controller' => 'Pages', 'action' => 'display', 'home'];
303-
}
304-
305-
return $this->redirect($target);
299+
return $this->Authentication->redirectAfterLogin([
300+
'controller' => 'Pages',
301+
'action' => 'display',
302+
'home',
303+
]);
306304
}
307305

308306
return null;
309307
}
310308
```
311309

310+
If you need to inspect the validated target before redirecting, you can still
311+
use `getLoginRedirect()` directly and then call `redirect()` yourself.
312+
312313
## Migrating Hashing Upgrade Logic
313314

314315
If your application uses `AuthComponent`’s hash upgrade

src/Controller/Component/AuthenticationComponent.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use Cake\Controller\Component;
3030
use Cake\Event\EventDispatcherInterface;
3131
use Cake\Event\EventDispatcherTrait;
32+
use Cake\Http\Response;
3233
use Cake\Routing\Router;
3334
use Cake\Utility\Hash;
3435
use Exception;
@@ -370,6 +371,24 @@ public function getLoginRedirect(array|string|null $default = null): ?string
370371
return $this->getAuthenticationService()->getLoginRedirect($this->getController()->getRequest()) ?? $default;
371372
}
372373

374+
/**
375+
* Redirect after a successful login using the validated login redirect
376+
* target from the current request when available.
377+
*
378+
* This is a convenience wrapper around `getLoginRedirect()` plus the
379+
* controller's redirect method so applications can use the plugin's
380+
* existing safe redirect parsing without manually reading query params.
381+
*
382+
* @param array|string $default Default URL to use when no valid login redirect is available.
383+
* @return \Cake\Http\Response|null
384+
*/
385+
public function redirectAfterLogin(array|string $default = '/'): ?Response
386+
{
387+
$target = $this->getLoginRedirect($default) ?? $default;
388+
389+
return $this->getController()->redirect($target);
390+
}
391+
373392
/**
374393
* Get the Controller callbacks this Component is interested in.
375394
*

tests/TestCase/Controller/Component/AuthenticationComponentTest.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,84 @@ public function testGetLoginRedirect(): void
397397
Configure::delete('App.base');
398398
}
399399

400+
/**
401+
* testRedirectAfterLogin
402+
*
403+
* @return void
404+
*/
405+
public function testRedirectAfterLogin(): void
406+
{
407+
Configure::write('App.base', '/cakephp');
408+
$url = ['controller' => 'Users', 'action' => 'dashboard'];
409+
Router::createRouteBuilder('/')
410+
->connect('/dashboard', $url);
411+
412+
$this->service->setConfig('queryParam', 'redirect');
413+
$request = $this->request
414+
->withAttribute('identity', $this->identity)
415+
->withAttribute('authentication', $this->service)
416+
->withQueryParams(['redirect' => 'ok/path?value=key']);
417+
418+
$controller = new Controller($request);
419+
$registry = new ComponentRegistry($controller);
420+
$component = new AuthenticationComponent($registry);
421+
422+
$response = $component->redirectAfterLogin($url);
423+
$this->assertSame(Router::url('/ok/path?value=key'), $response?->getHeaderLine('Location'));
424+
425+
Configure::delete('App.base');
426+
}
427+
428+
/**
429+
* testRedirectAfterLoginFallsBackToDefaultForAbsoluteUrls
430+
*
431+
* @return void
432+
*/
433+
public function testRedirectAfterLoginFallsBackToDefaultForAbsoluteUrls(): void
434+
{
435+
$url = ['controller' => 'Users', 'action' => 'dashboard'];
436+
Router::createRouteBuilder('/')
437+
->connect('/dashboard', $url);
438+
439+
$this->service->setConfig('queryParam', 'redirect');
440+
$request = $this->request
441+
->withAttribute('identity', $this->identity)
442+
->withAttribute('authentication', $this->service)
443+
->withQueryParams(['redirect' => 'https://evil.example/phish']);
444+
445+
$controller = new Controller($request);
446+
$registry = new ComponentRegistry($controller);
447+
$component = new AuthenticationComponent($registry);
448+
449+
$response = $component->redirectAfterLogin($url);
450+
$this->assertSame('/dashboard', $response?->getHeaderLine('Location'));
451+
}
452+
453+
/**
454+
* testRedirectAfterLoginFallsBackToDefaultForProtocolRelativeUrls
455+
*
456+
* @return void
457+
*/
458+
public function testRedirectAfterLoginFallsBackToDefaultForProtocolRelativeUrls(): void
459+
{
460+
$url = ['controller' => 'Users', 'action' => 'dashboard'];
461+
Router::createRouteBuilder('/')
462+
->connect('/dashboard', $url);
463+
464+
$this->service->setConfig('queryParam', 'redirect');
465+
$request = $this->request
466+
->withAttribute('identity', $this->identity)
467+
->withAttribute('authentication', $this->service)
468+
->withQueryParams(['redirect' => '//evil.example/phish']);
469+
470+
$controller = new Controller($request);
471+
$registry = new ComponentRegistry($controller);
472+
$component = new AuthenticationComponent($registry);
473+
474+
$response = $component->redirectAfterLogin($url);
475+
$this->assertSame('/dashboard', $response?->getHeaderLine('Location'));
476+
}
477+
400478
/**
401479
* testAfterIdentifyEvent
402480
*

0 commit comments

Comments
 (0)