1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the Silverback API Components Bundle Project |
5
|
|
|
* |
6
|
|
|
* (c) Daniel West <[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
|
|
|
declare(strict_types=1); |
13
|
|
|
|
14
|
|
|
namespace Silverback\ApiComponentsBundle\Security; |
15
|
|
|
|
16
|
|
|
use Doctrine\ORM\OptimisticLockException; |
17
|
|
|
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidPayloadException; |
18
|
|
|
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException; |
19
|
|
|
use Lexik\Bundle\JWTAuthenticationBundle\Exception\UserNotFoundException; |
20
|
|
|
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\PreAuthenticationJWTUserToken; |
21
|
|
|
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; |
22
|
|
|
use Silverback\ApiComponentsBundle\Event\JWTRefreshedEvent; |
23
|
|
|
use Silverback\ApiComponentsBundle\RefreshToken\RefreshToken; |
24
|
|
|
use Silverback\ApiComponentsBundle\RefreshToken\Storage\RefreshTokenStorageInterface; |
25
|
|
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface; |
26
|
|
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; |
27
|
|
|
use Symfony\Component\Security\Core\Exception\UserNotFoundException as SymfonyUserNotFoundException; |
28
|
|
|
use Symfony\Component\Security\Core\User\UserInterface; |
29
|
|
|
use Symfony\Component\Security\Core\User\UserProviderInterface; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @author Vincent Chalamon <[email protected]> |
33
|
|
|
*/ |
34
|
|
|
final class JWTManager implements JWTTokenManagerInterface |
35
|
|
|
{ |
36
|
|
|
private JWTTokenManagerInterface $decorated; |
37
|
|
|
private EventDispatcherInterface $dispatcher; |
38
|
|
|
private UserProviderInterface $userProvider; |
39
|
|
|
private RefreshTokenStorageInterface $storage; |
40
|
|
|
|
41
|
|
|
public function __construct(JWTTokenManagerInterface $decorated, EventDispatcherInterface $dispatcher, UserProviderInterface $userProvider, RefreshTokenStorageInterface $storage) |
42
|
|
|
{ |
43
|
|
|
$this->decorated = $decorated; |
44
|
|
|
$this->dispatcher = $dispatcher; |
45
|
|
|
$this->userProvider = $userProvider; |
46
|
|
|
$this->storage = $storage; |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* {@inheritdoc} |
51
|
|
|
*/ |
52
|
|
|
public function create(UserInterface $user, ?RefreshToken $token = null): string |
53
|
|
|
{ |
54
|
|
|
try { |
55
|
|
|
if ($token) { |
56
|
|
|
$this->storage->createAndExpire($user, $token); |
57
|
|
|
} else { |
58
|
|
|
$this->storage->createAndExpireAll($user); |
59
|
|
|
} |
60
|
|
|
} catch (OptimisticLockException $exception) { |
61
|
|
|
// do nothing, we have already modified the refresh token. |
62
|
|
|
// we can continue to generate a jwt token, won't make any difference |
63
|
|
|
// if the user has a new one of these... |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
return $this->decorated->create($user); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* {@inheritdoc} |
71
|
|
|
*/ |
72
|
|
|
public function decode(TokenInterface $token): array|false |
73
|
|
|
{ |
74
|
|
|
// parse will be used for old symfony where PreAuthenticationJWTUserToken exists |
75
|
|
|
try { |
76
|
|
|
return $this->decorated->decode($token); |
77
|
|
|
} catch (JWTDecodeFailureException $exception) { |
78
|
|
|
$jwtUserToken = $this->handleJWTDecodeFailureException($exception); |
79
|
|
|
|
80
|
|
|
return $this->decorated->decode(new PreAuthenticationJWTUserToken($jwtUserToken)); |
81
|
|
|
} |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
public function parse(string $token): array |
85
|
|
|
{ |
86
|
|
|
try { |
87
|
|
|
return $this->decorated->parse($token); |
88
|
|
|
} catch (JWTDecodeFailureException $exception) { |
89
|
|
|
$jwtUserToken = $this->handleJWTDecodeFailureException($exception); |
90
|
|
|
|
91
|
|
|
return $this->decorated->parse($jwtUserToken); |
92
|
|
|
} |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
private function handleJWTDecodeFailureException(JWTDecodeFailureException $exception): string |
96
|
|
|
{ |
97
|
|
|
if (JWTDecodeFailureException::EXPIRED_TOKEN !== $exception->getReason()) { |
98
|
|
|
throw $exception; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
$payload = $exception->getPayload(); |
102
|
|
|
$idClaim = $this->getUserIdClaim(); |
103
|
|
|
|
104
|
|
|
if (!isset($payload[$idClaim])) { |
105
|
|
|
throw new InvalidPayloadException($idClaim); |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
$identity = $payload[$idClaim]; |
109
|
|
|
try { |
110
|
|
|
$user = $this->userProvider->loadUserByIdentifier($identity); |
111
|
|
|
} catch (SymfonyUserNotFoundException $e) { |
112
|
|
|
throw new UserNotFoundException($idClaim, $identity); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
$refreshToken = $this->resolveCurrentRefreshToken($user); |
116
|
|
|
if (!$refreshToken) { |
117
|
|
|
throw $exception; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$accessToken = $this->create($user, $refreshToken); |
121
|
|
|
|
122
|
|
|
$this->dispatcher->dispatch(new JWTRefreshedEvent($accessToken)); |
123
|
|
|
|
124
|
|
|
return $accessToken; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
private function resolveCurrentRefreshToken(UserInterface $user): ?RefreshToken |
128
|
|
|
{ |
129
|
|
|
// the refresh token could have just been expired and is being refreshed by another request |
130
|
|
|
$refreshToken = $this->storage->findOneByUser($user); |
131
|
|
|
if (!$refreshToken || $refreshToken->isExpired()) { |
132
|
|
|
return null; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
return $refreshToken; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* {@inheritdoc} |
140
|
|
|
*/ |
141
|
|
|
public function setUserIdentityField($field) |
142
|
|
|
{ |
143
|
|
|
return $this->decorated->setUserIdentityField($field); |
|
|
|
|
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* {@inheritdoc} |
148
|
|
|
*/ |
149
|
|
|
public function getUserIdentityField(): string |
150
|
|
|
{ |
151
|
|
|
return $this->decorated->getUserIdentityField(); |
|
|
|
|
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* {@inheritdoc} |
156
|
|
|
*/ |
157
|
|
|
public function getUserIdClaim(): string |
158
|
|
|
{ |
159
|
|
|
return $this->decorated->getUserIdClaim(); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* {@inheritdoc} |
164
|
|
|
*/ |
165
|
|
|
public function createFromPayload(UserInterface $user, array $payload): string |
166
|
|
|
{ |
167
|
|
|
return $this->decorated->createFromPayload($user, $payload); |
|
|
|
|
168
|
|
|
} |
169
|
|
|
} |
170
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.