Passed
Push — master ( 902d5f...34a14d )
by Andrew
03:57 queued 02:02
created

DeviceCodeGrant::validateDeviceCode()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 31
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6.4689

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 15
c 1
b 0
f 0
nc 6
nop 2
dl 0
loc 31
ccs 13
cts 17
cp 0.7647
crap 6.4689
rs 9.2222
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 18
    public function __construct(
50
        DeviceCodeRepositoryInterface $deviceCodeRepository,
51
        RefreshTokenRepositoryInterface $refreshTokenRepository,
52
        private DateInterval $deviceCodeTTL,
53
        string $verificationUri,
54
        private readonly int $retryInterval = 5
55
    ) {
56 18
        $this->setDeviceCodeRepository($deviceCodeRepository);
57 18
        $this->setRefreshTokenRepository($refreshTokenRepository);
58
59 18
        $this->refreshTokenTTL = new DateInterval('P1M');
60
61 18
        $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 8
    public function respondToDeviceAuthorizationRequest(ServerRequestInterface $request): DeviceCodeResponse
76
    {
77 8
        $clientId = $this->getRequestParameter(
78 8
            'client_id',
79 8
            $request,
80 8
            $this->getServerParameter('PHP_AUTH_USER', $request)
81 8
        );
82
83 8
        if ($clientId === null) {
84 2
            throw OAuthServerException::invalidRequest('client_id');
85
        }
86
87 6
        $client = $this->getClientEntityOrFail($clientId, $request);
88
89 5
        $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
90
91 5
        $deviceCodeEntity = $this->issueDeviceCode(
92 5
            $this->deviceCodeTTL,
93 5
            $client,
94 5
            $this->verificationUri,
95 5
            $scopes
96 5
        );
97
98 5
        $response = new DeviceCodeResponse();
99
100 5
        if ($this->includeVerificationUriComplete === true) {
101 1
            $response->includeVerificationUriComplete();
102
        }
103
104 5
        if ($this->intervalVisibility === true) {
105 2
            $response->includeInterval();
106
        }
107
108 5
        $response->setDeviceCodeEntity($deviceCodeEntity);
109
110 5
        return $response;
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116 3
    public function completeDeviceAuthorizationRequest(string $deviceCode, string $userId, bool $userApproved): void
117
    {
118 3
        $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
            throw OAuthServerException::invalidRequest('user_id', 'User ID is required');
126
        }
127
128 3
        $deviceCode->setUserIdentifier($userId);
129 3
        $deviceCode->setUserApproved($userApproved);
130
131 3
        $this->deviceCodeRepository->persistDeviceCode($deviceCode);
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137 7
    public function respondToAccessTokenRequest(
138
        ServerRequestInterface $request,
139
        ResponseTypeInterface $responseType,
140
        DateInterval $accessTokenTTL
141
    ): ResponseTypeInterface {
142
        // Validate request
143 7
        $client = $this->validateClient($request);
144 6
        $deviceCodeEntity = $this->validateDeviceCode($request, $client);
145
146
        // If device code has no user associated, respond with pending or slow down
147 4
        if (is_null($deviceCodeEntity->getUserIdentifier())) {
148 2
            $shouldSlowDown = $this->deviceCodePolledTooSoon($deviceCodeEntity->getLastPolledAt());
149
150 2
            $deviceCodeEntity->setLastPolledAt(new DateTimeImmutable());
151 2
            $this->deviceCodeRepository->persistDeviceCode($deviceCodeEntity);
152
153 2
            if ($shouldSlowDown) {
154 1
                throw OAuthServerException::slowDown();
155
            }
156
157 1
            throw OAuthServerException::authorizationPending();
158
        }
159
160 2
        if ($deviceCodeEntity->getUserApproved() === false) {
161 1
            throw OAuthServerException::accessDenied();
162
        }
163
164
        // Finalize the requested scopes
165 1
        $finalizedScopes = $this->scopeRepository->finalizeScopes($deviceCodeEntity->getScopes(), $this->getIdentifier(), $client, $deviceCodeEntity->getUserIdentifier());
166
167
        // Issue and persist new access token
168 1
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $deviceCodeEntity->getUserIdentifier(), $finalizedScopes);
169 1
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
170 1
        $responseType->setAccessToken($accessToken);
171
172
        // Issue and persist new refresh token if given
173 1
        $refreshToken = $this->issueRefreshToken($accessToken);
174
175 1
        if ($refreshToken !== null) {
176 1
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
177 1
            $responseType->setRefreshToken($refreshToken);
178
        }
179
180 1
        $this->deviceCodeRepository->revokeDeviceCode($deviceCodeEntity->getIdentifier());
181
182 1
        return $responseType;
183
    }
184
185
    /**
186
     * @throws OAuthServerException
187
     */
188 6
    protected function validateDeviceCode(ServerRequestInterface $request, ClientEntityInterface $client): DeviceCodeEntityInterface
189
    {
190 6
        $deviceCode = $this->getRequestParameter('device_code', $request);
191
192 6
        if (is_null($deviceCode)) {
193 1
            throw OAuthServerException::invalidRequest('device_code');
194
        }
195
196 5
        $deviceCodeEntity = $this->deviceCodeRepository->getDeviceCodeEntityByDeviceCode(
197 5
            $deviceCode
198 5
        );
199
200 5
        if ($deviceCodeEntity instanceof DeviceCodeEntityInterface === false) {
201
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
202
203
            throw OAuthServerException::invalidGrant();
204
        }
205
206 5
        if (time() > $deviceCodeEntity->getExpiryDateTime()->getTimestamp()) {
207 1
            throw OAuthServerException::expiredToken('device_code');
208
        }
209
210 4
        if ($this->deviceCodeRepository->isDeviceCodeRevoked($deviceCode) === true) {
211
            throw OAuthServerException::invalidRequest('device_code', 'Device code has been revoked');
212
        }
213
214 4
        if ($deviceCodeEntity->getClient()->getIdentifier() !== $client->getIdentifier()) {
215
            throw OAuthServerException::invalidRequest('device_code', 'Device code was not issued to this client');
216
        }
217
218 4
        return $deviceCodeEntity;
219
    }
220
221 2
    private function deviceCodePolledTooSoon(?DateTimeImmutable $lastPoll): bool
222
    {
223 2
        return $lastPoll !== null && $lastPoll->getTimestamp() + $this->retryInterval > time();
224
    }
225
226
    /**
227
     * Set the verification uri
228
     */
229 18
    public function setVerificationUri(string $verificationUri): void
230
    {
231 18
        $this->verificationUri = $verificationUri;
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237 8
    public function getIdentifier(): string
238
    {
239 8
        return 'urn:ietf:params:oauth:grant-type:device_code';
240
    }
241
242 18
    private function setDeviceCodeRepository(DeviceCodeRepositoryInterface $deviceCodeRepository): void
243
    {
244 18
        $this->deviceCodeRepository = $deviceCodeRepository;
245
    }
246
247
    /**
248
     * Issue a device code.
249
     *
250
     * @param ScopeEntityInterface[] $scopes
251
     *
252
     * @throws OAuthServerException
253
     * @throws UniqueTokenIdentifierConstraintViolationException
254
     */
255 5
    protected function issueDeviceCode(
256
        DateInterval $deviceCodeTTL,
257
        ClientEntityInterface $client,
258
        string $verificationUri,
259
        array $scopes = [],
260
    ): DeviceCodeEntityInterface {
261 5
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
262
263 5
        $deviceCode = $this->deviceCodeRepository->getNewDeviceCode();
264 5
        $deviceCode->setExpiryDateTime((new DateTimeImmutable())->add($deviceCodeTTL));
265 5
        $deviceCode->setClient($client);
266 5
        $deviceCode->setVerificationUri($verificationUri);
267 5
        $deviceCode->setInterval($this->retryInterval);
268
269 5
        foreach ($scopes as $scope) {
270 5
            $deviceCode->addScope($scope);
271
        }
272
273 5
        while ($maxGenerationAttempts-- > 0) {
274 5
            $deviceCode->setIdentifier($this->generateUniqueIdentifier());
275 5
            $deviceCode->setUserCode($this->generateUserCode());
276
277
            try {
278 5
                $this->deviceCodeRepository->persistDeviceCode($deviceCode);
279
280 5
                return $deviceCode;
281
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
282
                if ($maxGenerationAttempts === 0) {
283
                    throw $e;
284
                }
285
            }
286
        }
287
288
        // This should never be hit. It is here to work around a PHPStan false error
289
        return $deviceCode;
290
    }
291
292
    /**
293
     * Generate a new user code.
294
     *
295
     * @throws OAuthServerException
296
     */
297 5
    protected function generateUserCode(int $length = 8): string
298
    {
299
        try {
300 5
            $userCode = '';
301 5
            $userCodeCharacters = 'BCDFGHJKLMNPQRSTVWXZ';
302
303 5
            while (strlen($userCode) < $length) {
304 5
                $userCode .= $userCodeCharacters[random_int(0, 19)];
305
            }
306
307 5
            return $userCode;
308
            // @codeCoverageIgnoreStart
309
        } catch (TypeError | Error $e) {
310
            throw OAuthServerException::serverError('An unexpected error has occurred', $e);
311
        } catch (Exception $e) {
312
            // If you get this message, the CSPRNG failed hard.
313
            throw OAuthServerException::serverError('Could not generate a random string', $e);
314
        }
315
        // @codeCoverageIgnoreEnd
316
    }
317
318 2
    public function setIntervalVisibility(bool $intervalVisibility): void
319
    {
320 2
        $this->intervalVisibility = $intervalVisibility;
321
    }
322
323
    public function getIntervalVisibility(): bool
324
    {
325
        return $this->intervalVisibility;
326
    }
327
328 1
    public function setIncludeVerificationUriComplete(bool $includeVerificationUriComplete): void
329
    {
330 1
        $this->includeVerificationUriComplete = $includeVerificationUriComplete;
331
    }
332
}
333

 

OSZAR »