Skip to content

Commit 88842ec

Browse files
weaverryanfabpot
authored andcommitted
[Security] Magic login link authentication
1 parent 19005ec commit 88842ec

13 files changed

+770
-0
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\Authenticator;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
17+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
18+
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
19+
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
20+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
21+
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
22+
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
23+
use Symfony\Component\Security\Http\HttpUtils;
24+
use Symfony\Component\Security\Http\LoginLink\Exception\InvalidLoginLinkAuthenticationException;
25+
use Symfony\Component\Security\Http\LoginLink\Exception\InvalidLoginLinkExceptionInterface;
26+
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;
27+
28+
/**
29+
* @author Ryan Weaver <[email protected]>
30+
* @experimental in 5.2
31+
*/
32+
final class LoginLinkAuthenticator extends AbstractAuthenticator implements InteractiveAuthenticatorInterface
33+
{
34+
private $loginLinkHandler;
35+
private $httpUtils;
36+
private $successHandler;
37+
private $failureHandler;
38+
private $options;
39+
40+
public function __construct(LoginLinkHandlerInterface $loginLinkHandler, HttpUtils $httpUtils, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options)
41+
{
42+
$this->loginLinkHandler = $loginLinkHandler;
43+
$this->httpUtils = $httpUtils;
44+
$this->successHandler = $successHandler;
45+
$this->failureHandler = $failureHandler;
46+
$this->options = $options;
47+
}
48+
49+
public function supports(Request $request): ?bool
50+
{
51+
return $this->httpUtils->checkRequestPath($request, $this->options['check_route']);
52+
}
53+
54+
public function authenticate(Request $request): PassportInterface
55+
{
56+
$username = $request->get('user');
57+
58+
if (!$username) {
59+
throw new InvalidLoginLinkAuthenticationException('Missing user from link.');
60+
}
61+
62+
return new SelfValidatingPassport(
63+
new UserBadge($username, function () use ($request) {
64+
try {
65+
$user = $this->loginLinkHandler->consumeLoginLink($request);
66+
} catch (InvalidLoginLinkExceptionInterface $e) {
67+
throw new InvalidLoginLinkAuthenticationException('Login link could not be validated.', 0, $e);
68+
}
69+
70+
return $user;
71+
}),
72+
[]
73+
);
74+
}
75+
76+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
77+
{
78+
return $this->successHandler->onAuthenticationSuccess($request, $token);
79+
}
80+
81+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
82+
{
83+
return $this->failureHandler->onAuthenticationFailure($request, $exception);
84+
}
85+
86+
public function isInteractive(): bool
87+
{
88+
return true;
89+
}
90+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\LoginLink\Exception;
13+
14+
/**
15+
* @author Ryan Weaver <[email protected]>
16+
* @experimental in 5.2
17+
*/
18+
class ExpiredLoginLinkException extends \Exception implements InvalidLoginLinkExceptionInterface
19+
{
20+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\LoginLink\Exception;
13+
14+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
15+
16+
/**
17+
* Thrown when a login link is invalid.
18+
*
19+
* @author Ryan Weaver <[email protected]>
20+
* @experimental in 5.2
21+
*/
22+
class InvalidLoginLinkAuthenticationException extends AuthenticationException
23+
{
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
public function getMessageKey()
28+
{
29+
return 'Invalid or expired login link.';
30+
}
31+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\LoginLink\Exception;
13+
14+
/**
15+
* @author Ryan Weaver <[email protected]>
16+
* @experimental in 5.2
17+
*/
18+
class InvalidLoginLinkException extends \Exception implements InvalidLoginLinkExceptionInterface
19+
{
20+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\LoginLink\Exception;
13+
14+
/**
15+
* @author Ryan Weaver <[email protected]>
16+
* @experimental in 5.2
17+
*/
18+
interface InvalidLoginLinkExceptionInterface extends \Throwable
19+
{
20+
}

LoginLink/ExpiredLoginLinkStorage.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\LoginLink;
13+
14+
use Psr\Cache\CacheItemPoolInterface;
15+
16+
/**
17+
* @final
18+
*/
19+
class ExpiredLoginLinkStorage
20+
{
21+
private $cache;
22+
private $lifetime;
23+
24+
public function __construct(CacheItemPoolInterface $cache, int $lifetime)
25+
{
26+
$this->cache = $cache;
27+
$this->lifetime = $lifetime;
28+
}
29+
30+
public function countUsages(string $hash): int
31+
{
32+
$key = rawurlencode($hash);
33+
if (!$this->cache->hasItem($key)) {
34+
return 0;
35+
}
36+
37+
return $this->cache->getItem($key)->get();
38+
}
39+
40+
public function incrementUsages(string $hash): void
41+
{
42+
$item = $this->cache->getItem(rawurlencode($hash));
43+
44+
if (!$item->isHit()) {
45+
$item->expiresAfter($this->lifetime);
46+
}
47+
48+
$item->set($this->countUsages($hash) + 1);
49+
$this->cache->save($item);
50+
}
51+
}

LoginLink/LoginLinkDetails.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\LoginLink;
13+
14+
/**
15+
* @author Ryan Weaver <[email protected]>
16+
* @experimental in 5.2
17+
*/
18+
class LoginLinkDetails
19+
{
20+
private $url;
21+
private $expiresAt;
22+
23+
public function __construct(string $url, \DateTimeImmutable $expiresAt)
24+
{
25+
$this->url = $url;
26+
$this->expiresAt = $expiresAt;
27+
}
28+
29+
public function getUrl(): string
30+
{
31+
return $this->url;
32+
}
33+
34+
public function getExpiresAt(): \DateTimeImmutable
35+
{
36+
return $this->expiresAt;
37+
}
38+
39+
public function __toString()
40+
{
41+
return $this->url;
42+
}
43+
}

0 commit comments

Comments
 (0)