JWTManager::create()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 3
eloc 7
c 5
b 0
f 0
nc 4
nop 2
dl 0
loc 15
ccs 0
cts 6
cp 0
crap 12
rs 10
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);
0 ignored issues
show
Deprecated Code introduced by
The function Lexik\Bundle\JWTAuthenti...:setUserIdentityField() has been deprecated: since 2.15, use {@see UserInterface::getUserIdentifier()} instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

143
        return /** @scrutinizer ignore-deprecated */ $this->decorated->setUserIdentityField($field);

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.

Loading history...
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149
    public function getUserIdentityField(): string
150
    {
151
        return $this->decorated->getUserIdentityField();
0 ignored issues
show
Deprecated Code introduced by
The function Lexik\Bundle\JWTAuthenti...:getUserIdentityField() has been deprecated: since 2.15, use {@see UserInterface::getUserIdentifier()} instead ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

151
        return /** @scrutinizer ignore-deprecated */ $this->decorated->getUserIdentityField();

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.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The method createFromPayload() does not exist on Lexik\Bundle\JWTAuthenti...WTTokenManagerInterface. Did you maybe mean create()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

167
        return $this->decorated->/** @scrutinizer ignore-call */ createFromPayload($user, $payload);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
168
    }
169
}
170