Passed
Pull Request — master (#1410)
by
unknown
34:35
created

DeviceCodeGrant::respondToAccessTokenRequest()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 57
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 8.0029

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 8
eloc 27
c 4
b 0
f 0
nc 7
nop 3
dl 0
loc 57
ccs 27
cts 28
cp 0.9643
crap 8.0029
rs 8.4444

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * OAuth 2.0 Device Code grant.
5
 *
6
 * @author      Andrew Millington <[email protected]>
7
 * @copyright   Copyright (c) Alex Bilbie
8
 * @license     http://mit-license.org/
9
 *
10
 * @link        https://github.com/thephpleague/oauth2-server
11
 */
12
13
declare(strict_types=1);
14
15
namespace League\OAuth2\Server\Grant;
16
17
use DateInterval;
18
use DateTimeImmutable;
19
use Error;
20
use Exception;
21
use League\OAuth2\Server\Entities\ClientEntityInterface;
22
use League\OAuth2\Server\Entities\DeviceCodeEntityInterface;
23
use League\OAuth2\Server\Entities\ScopeEntityInterface;
24
use League\OAuth2\Server\Exception\OAuthServerException;
25
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
26
use League\OAuth2\Server\Repositories\DeviceCodeRepositoryInterface;
27
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
28
use League\OAuth2\Server\RequestEvent;
29
use League\OAuth2\Server\ResponseTypes\DeviceCodeResponse;
30
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
31
use Psr\Http\Message\ServerRequestInterface;
32
use TypeError;
33
34
use function is_null;
35
use function random_int;
36
use function strlen;
37
use function time;
38
39
/**
40
 * Device Code grant class.
41
 */
42
class DeviceCodeGrant extends AbstractGrant
43
{
44
    protected DeviceCodeRepositoryInterface $deviceCodeRepository;
45
    private bool $includeVerificationUriComplete = false;
46
    private bool $intervalVisibility = false;
47
    private string $verificationUri;
48
49 17
    public function __construct(
50
        DeviceCodeRepositoryInterface $deviceCodeRepository,
51
        RefreshTokenRepositoryInterface $refreshTokenRepository,
52
        private DateInterval $deviceCodeTTL,
53
        string $verificationUri,
54
        private readonly int $defaultInterval = 5
55
    ) {
56 17
        $this->setDeviceCodeRepository($deviceCodeRepository);
57 17
        $this->setRefreshTokenRepository($refreshTokenRepository);
58
59 17
        $this->refreshTokenTTL = new DateInterval('P1M');
60
61 17
        $this->setVerificationUri($verificationUri);
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67 2
    public function canRespondToDeviceAuthorizationRequest(ServerRequestInterface $request): bool
68
    {
69 2
        return true;
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75 7
    public function respondToDeviceAuthorizationRequest(ServerRequestInterface $request): DeviceCodeResponse
76
    {
77 7
        $clientId = $this->getRequestParameter(
78 7
            'client_id',
79 7
            $request,
80 7
            $this->getServerParameter('PHP_AUTH_USER', $request)
81 7
        );
82
83 7
        if ($clientId === null) {
84 2
            throw OAuthServerException::invalidRequest('client_id');
85
        }
86
87 5
        $client = $this->getClientEntityOrFail($clientId, $request);
88
89 4
        $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
90
91 4
        $deviceCodeEntity = $this->issueDeviceCode(
92 4
            $this->deviceCodeTTL,
93 4
            $client,
94 4
            $this->verificationUri,
95 4
            $scopes
96 4
        );
97
98 4
        $response = new DeviceCodeResponse();
99
100 4
        if ($this->includeVerificationUriComplete === true) {
101 1
            $response->includeVerificationUriComplete();
102
        }
103
104 4
        if ($this->intervalVisibility === true) {
105
            $response->includeInterval();
106 4
        }
107
108
        $response->setDeviceCodeEntity($deviceCodeEntity);
109
110
        return $response;
111
    }
112 3
113
    /**
114 3
     * {@inheritdoc}
115
     */
116 3
    public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void
117
    {
118
        $deviceCode = $this->deviceCodeRepository->getDeviceCodeEntityByDeviceCode($deviceCode);
119
120 3
        if ($deviceCode instanceof DeviceCodeEntityInterface === false) {
121
            throw OAuthServerException::invalidRequest('device_code', 'Device code does not exist');
122
        }
123
124 3
        if ($userId === '') {
125 3
            throw OAuthServerException::invalidRequest('user_id', 'User ID is required');
126
        }
127 3
128
        $deviceCode->setUserIdentifier($userId);
129
        $deviceCode->setUserApproved($userApproved);
130
131
        $this->deviceCodeRepository->persistDeviceCode($deviceCode);
132
    }
133 7
134
    /**
135
     * {@inheritdoc}
136
     */
137
    public function respondToAccessTokenRequest(
138
        ServerRequestInterface $request,
139 7
        ResponseTypeInterface $responseType,
140 6
        DateInterval $accessTokenTTL
141 6
    ): ResponseTypeInterface {
142
        // Validate request
143 3
        $client = $this->validateClient($request);
144 3
        $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
145
        $deviceCodeEntity = $this->validateDeviceCode($request, $client);
146
147 3
        // If device code has no user associated, respond with pending or slow down
148 1
        if (is_null($deviceCodeEntity->getUserIdentifier())) {
149
            $shouldSlowDown = false;
150
151 2
            if ($this->deviceCodePolledTooSoon($deviceCodeEntity) === true) {
152 1
                $deviceCodeEntity->setInterval($deviceCodeEntity->getInterval() + 5);
153
154
                $shouldSlowDown = true;
155
            }
156 1
157
            $deviceCodeEntity->setLastPolledAt(new DateTimeImmutable());
158
            $this->deviceCodeRepository->persistDeviceCode($deviceCodeEntity);
159 1
160 1
            if ($shouldSlowDown) {
161 1
                throw OAuthServerException::slowDown(
162
                    interval: $this->intervalVisibility ? $deviceCodeEntity->getInterval() : null
163
                );
164 1
            }
165
166 1
            throw OAuthServerException::authorizationPending(
167 1
                interval: $this->intervalVisibility ? $deviceCodeEntity->getInterval() : null
168 1
            );
169
        }
170
171 1
        if ($deviceCodeEntity->getUserApproved() === false) {
172
            throw OAuthServerException::accessDenied();
173 1
        }
174
175
        // Finalize the requested scopes
176
        $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $deviceCodeEntity->getUserIdentifier());
177
178
        // Issue and persist new access token
179 6
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $deviceCodeEntity->getUserIdentifier(), $finalizedScopes);
180
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
181 6
        $responseType->setAccessToken($accessToken);
182
183 6
        // Issue and persist new refresh token if given
184 1
        $refreshToken = $this->issueRefreshToken($accessToken);
185
186
        if ($refreshToken !== null) {
187 5
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
188 5
            $responseType->setRefreshToken($refreshToken);
189 5
        }
190
191 5
        $this->deviceCodeRepository->revokeDeviceCode($deviceCodeEntity->getIdentifier());
192
193
        return $responseType;
194
    }
195
196
    /**
197 5
     * @throws OAuthServerException
198 1
     */
199
    protected function validateDeviceCode(ServerRequestInterface $request, ClientEntityInterface $client): DeviceCodeEntityInterface
200
    {
201 4
        $deviceCode = $this->getRequestParameter('device_code', $request);
202
203
        if (is_null($deviceCode)) {
204
            throw OAuthServerException::invalidRequest('device_code');
205 4
        }
206
207
        $deviceCodeEntity = $this->deviceCodeRepository->getDeviceCodeEntityByDeviceCode(
208
            $deviceCode
209 4
        );
210 1
211
        if ($deviceCodeEntity instanceof DeviceCodeEntityInterface === false) {
212
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
213 3
214
            throw OAuthServerException::invalidGrant();
215
        }
216 4
217
        if (time() > $deviceCodeEntity->getExpiryDateTime()->getTimestamp()) {
218 4
            throw OAuthServerException::expiredToken('device_code');
219
        }
220
221
        if ($this->deviceCodeRepository->isDeviceCodeRevoked($deviceCode) === true) {
222
            throw OAuthServerException::invalidRequest('device_code', 'Device code has been revoked');
223
        }
224 17
225
        if ($deviceCodeEntity->getClient()->getIdentifier() !== $client->getIdentifier()) {
226 17
            throw OAuthServerException::invalidRequest('device_code', 'Device code was not issued to this client');
227
        }
228
229
        return $deviceCodeEntity;
230
    }
231
232 8
    private function deviceCodePolledTooSoon(DeviceCodeEntityInterface $deviceCodeEntity): bool
233
    {
234 8
        $lastPoll = $deviceCodeEntity->getLastPolledAt();
235
236
        return $lastPoll !== null && $lastPoll->getTimestamp() + $deviceCodeEntity->getInterval() > time();
237 17
    }
238
239 17
    /**
240
     * Set the verification uri
241
     */
242
    public function setVerificationUri(string $verificationUri): void
243
    {
244
        $this->verificationUri = $verificationUri;
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250 4
    public function getIdentifier(): string
251
    {
252
        return 'urn:ietf:params:oauth:grant-type:device_code';
253
    }
254
255
    private function setDeviceCodeRepository(DeviceCodeRepositoryInterface $deviceCodeRepository): void
256 4
    {
257
        $this->deviceCodeRepository = $deviceCodeRepository;
258 4
    }
259 4
260 4
    /**
261 4
     * Issue a device code.
262
     *
263 4
     * @param ScopeEntityInterface[] $scopes
264 1
     *
265
     * @throws OAuthServerException
266
     * @throws UniqueTokenIdentifierConstraintViolationException
267 4
     */
268 4
    protected function issueDeviceCode(
269
        DateInterval $deviceCodeTTL,
270
        ClientEntityInterface $client,
271 4
        string $verificationUri,
272 4
        array $scopes = [],
273 4
    ): DeviceCodeEntityInterface {
274
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
275
276 4
        $deviceCode = $this->deviceCodeRepository->getNewDeviceCode();
277
        $deviceCode->setExpiryDateTime((new DateTimeImmutable())->add($deviceCodeTTL));
278 4
        $deviceCode->setClient($client);
279
        $deviceCode->setVerificationUri($verificationUri);
280
        $deviceCode->setInterval($this->defaultInterval);
281
282
        foreach ($scopes as $scope) {
283
            $deviceCode->addScope($scope);
284
        }
285
286
        while ($maxGenerationAttempts-- > 0) {
287
            $deviceCode->setIdentifier($this->generateUniqueIdentifier());
288
            $deviceCode->setUserCode($this->generateUserCode());
289
290
            try {
291
                $this->deviceCodeRepository->persistDeviceCode($deviceCode);
292
293
                return $deviceCode;
294
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
295 4
                if ($maxGenerationAttempts === 0) {
296
                    throw $e;
297
                }
298 4
            }
299 4
        }
300
301 4
        // This should never be hit. It is here to work around a PHPStan false error
302 4
        return $deviceCode;
303
    }
304
305 4
    /**
306
     * Generate a new user code.
307
     *
308
     * @throws OAuthServerException
309
     */
310
    protected function generateUserCode(int $length = 8): string
311
    {
312
        try {
313
            $userCode = '';
314
            $userCodeCharacters = 'BCDFGHJKLMNPQRSTVWXZ';
315
316 1
            while (strlen($userCode) < $length) {
317
                $userCode .= $userCodeCharacters[random_int(0, 19)];
318 1
            }
319
320
            return $userCode;
321 4
            // @codeCoverageIgnoreStart
322
        } catch (TypeError | Error $e) {
323 4
            throw OAuthServerException::serverError('An unexpected error has occurred', $e);
324
        } catch (Exception $e) {
325
            // If you get this message, the CSPRNG failed hard.
326 1
            throw OAuthServerException::serverError('Could not generate a random string', $e);
327
        }
328 1
        // @codeCoverageIgnoreEnd
329
    }
330
331
    public function setIntervalVisibility(bool $intervalVisibility): void
332
    {
333
        $this->intervalVisibility = $intervalVisibility;
334
    }
335
336
    public function getIntervalVisibility(): bool
337
    {
338
        return $this->intervalVisibility;
339
    }
340
341
    public function setIncludeVerificationUriComplete(bool $includeVerificationUriComplete): void
342
    {
343
        $this->includeVerificationUriComplete = $includeVerificationUriComplete;
344
    }
345
}
346