Passed
Pull Request — master (#1074)
by Andrew
01:51
created

DeviceCodeGrant::validateDeviceCode()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 39
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 8.1426

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 7
eloc 20
nc 7
nop 2
dl 0
loc 39
ccs 15
cts 21
cp 0.7143
crap 8.1426
rs 8.6666
c 2
b 1
f 0
1
<?php
2
/**
3
 * OAuth 2.0 Device Code grant.
4
 *
5
 * @author      Alex Bilbie <[email protected]>
6
 * @copyright   Copyright (c) Alex Bilbie
7
 * @license     http://mit-license.org/
8
 *
9
 * @link        https://github.com/thephpleague/oauth2-server
10
 */
11
12
namespace League\OAuth2\Server\Grant;
13
14
use DateInterval;
15
use DateTimeImmutable;
16
use Error;
17
use Exception;
18
use League\OAuth2\Server\Entities\ClientEntityInterface;
19
use League\OAuth2\Server\Entities\DeviceCodeEntityInterface;
20
use League\OAuth2\Server\Exception\OAuthServerException;
21
use League\OAuth2\Server\Repositories\DeviceCodeRepositoryInterface;
22
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
23
use League\OAuth2\Server\RequestEvent;
24
use League\OAuth2\Server\RequestTypes\DeviceAuthorizationRequest;
25
use League\OAuth2\Server\ResponseTypes\DeviceCodeResponse;
26
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
27
use LogicException;
28
use Psr\Http\Message\ServerRequestInterface;
29
use TypeError;
30
31
/**
32
 * Device Code grant class.
33
 */
34
class DeviceCodeGrant extends AbstractGrant
35
{
36
    /**
37
     * @var DeviceCodeRepositoryInterface
38
     */
39
    protected $deviceCodeRepository;
40
41
    /**
42
     * @var DateInterval
43
     */
44
    private $deviceCodeTTL;
45
46
    /**
47
     * @var int
48
     */
49
    private $retryInterval;
50
51
    /**
52
     * @var string
53
     */
54
    private $verificationUri;
55
56
    /**
57
     * @param DeviceCodeRepositoryInterface   $deviceCodeRepository
58
     * @param RefreshTokenRepositoryInterface $refreshTokenRepository
59
     * @param DateInterval                    $deviceCodeTTL
60
     * @param int                             $retryInterval
61
     */
62 9
    public function __construct(
63
        DeviceCodeRepositoryInterface $deviceCodeRepository,
64
        RefreshTokenRepositoryInterface $refreshTokenRepository,
65
        DateInterval $deviceCodeTTL,
66
        $retryInterval = 5
67
    ) {
68 9
        $this->setDeviceCodeRepository($deviceCodeRepository);
69 9
        $this->setRefreshTokenRepository($refreshTokenRepository);
70
71 9
        $this->refreshTokenTTL = new DateInterval('P1M');
72
73 9
        $this->deviceCodeTTL = $deviceCodeTTL;
74 9
        $this->retryInterval = $retryInterval;
75 9
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80 1
    public function canRespondToDeviceAuthorizationRequest(ServerRequestInterface $request)
81
    {
82 1
        return true;
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88 3
    public function validateDeviceAuthorizationRequest(ServerRequestInterface $request)
89
    {
90 3
        $clientId = $this->getRequestParameter(
91 3
            'client_id',
92 3
            $request,
93 3
            $this->getServerParameter('PHP_AUTH_USER', $request)
94
        );
95
96 3
        if ($clientId === null) {
97 1
            throw OAuthServerException::invalidRequest('client_id');
98
        }
99
100 2
        $client = $this->getClientEntityOrFail($clientId, $request);
101
102 1
        $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
103
104 1
        $deviceAuthorizationRequest = new DeviceAuthorizationRequest();
105 1
        $deviceAuthorizationRequest->setGrantTypeId($this->getIdentifier());
106 1
        $deviceAuthorizationRequest->setClient($client);
107 1
        $deviceAuthorizationRequest->setScopes($scopes);
108
109 1
        return $deviceAuthorizationRequest;
110
    }
111
112
    /**
113
     * {@inheritdoc}
114
     */
115 1
    public function completeDeviceAuthorizationRequest(DeviceAuthorizationRequest $deviceRequest)
116
    {
117 1
        $deviceCode = $this->issueDeviceCode(
118 1
            $this->deviceCodeTTL,
119 1
            $deviceRequest->getClient(),
120 1
            $this->verificationUri,
121 1
            $deviceRequest->getScopes()
122
        );
123
124
        $payload = [
125 1
            'client_id' => $deviceCode->getClient()->getIdentifier(),
126 1
            'device_code_id' => $deviceCode->getIdentifier(),
127 1
            'scopes' => $deviceCode->getScopes(),
128 1
            'user_code' => $deviceCode->getUserCode(),
129 1
            'expire_time' => $deviceCode->getExpiryDateTime()->getTimestamp(),
130 1
            'verification_uri' => $deviceCode->getVerificationUri(),
131
        ];
132
133 1
        $jsonPayload = \json_encode($payload);
134
135 1
        if ($jsonPayload === false) {
136
            throw new LogicException('An error was encountered when JSON encoding the authorization request response');
137
        }
138
139 1
        $response = new DeviceCodeResponse();
140 1
        $response->setDeviceCode($deviceCode);
141 1
        $response->setPayload($this->encrypt($jsonPayload));
142
143 1
        return $response;
144
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 3
    public function respondToAccessTokenRequest(
150
        ServerRequestInterface $request,
151
        ResponseTypeInterface $responseType,
152
        DateInterval $accessTokenTTL
153
    ) {
154
        // Validate request
155 3
        $client = $this->validateClient($request);
156 2
        $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
157 2
        $deviceCode = $this->validateDeviceCode($request, $client);
158
159
        // TODO: if the request is too fast, respond with slow down
160
161
162
        // if device code has no user associated, respond with pending
163 1
        if (\is_null($deviceCode->getUserIdentifier())) {
164
            throw OAuthServerException::authorizationPending();
165
        }
166
167
        // Finalize the requested scopes
168 1
        $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, (string) $deviceCode->getUserIdentifier());
169
170
        // Issue and persist new access token
171 1
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, (string) $deviceCode->getUserIdentifier(), $finalizedScopes);
172 1
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
173 1
        $responseType->setAccessToken($accessToken);
174
175
        // Issue and persist new refresh token if given
176 1
        $refreshToken = $this->issueRefreshToken($accessToken);
177
178 1
        if ($refreshToken !== null) {
179 1
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
180 1
            $responseType->setRefreshToken($refreshToken);
181
        }
182
183 1
        $this->deviceCodeRepository->revokeDeviceCode($deviceCode->getIdentifier());
184
185 1
        return $responseType;
186
    }
187
188
    /**
189
     * @param ServerRequestInterface $request
190
     * @param ClientEntityInterface  $client
191
     *
192
     * @throws OAuthServerException
193
     *
194
     * @return DeviceCodeEntityInterface
195
     */
196 2
    protected function validateDeviceCode(ServerRequestInterface $request, ClientEntityInterface $client)
197
    {
198 2
        $encryptedDeviceCode = $this->getRequestParameter('device_code', $request);
199
200 2
        if (\is_null($encryptedDeviceCode)) {
201 1
            throw OAuthServerException::invalidRequest('device_code');
202
        }
203
204 1
        $deviceCodePayload = $this->decodeDeviceCode($encryptedDeviceCode);
205
206 1
        if (!\property_exists($deviceCodePayload, 'device_code_id')) {
207
            throw OAuthServerException::invalidRequest('device_code', 'Device code malformed');
208
        }
209
210 1
        if (\time() > $deviceCodePayload->expire_time) {
211
            throw OAuthServerException::expiredToken('device_code');
212
        }
213
214 1
        if ($this->deviceCodeRepository->isDeviceCodeRevoked($deviceCodePayload->device_code_id) === true) {
215
            throw OAuthServerException::invalidRequest('device_code', 'Device code has been revoked');
216
        }
217
218 1
        if ($deviceCodePayload->client_id !== $client->getIdentifier()) {
219
            throw OAuthServerException::invalidRequest('device_code', 'Device code was not issued to this client');
220
        }
221
222 1
        $deviceCode = $this->deviceCodeRepository->getDeviceCodeEntityByDeviceCode(
223 1
            $deviceCodePayload->device_code_id,
224 1
            $this->getIdentifier(),
225 1
            $client
226
        );
227
228 1
        if ($deviceCode instanceof DeviceCodeEntityInterface === false) {
229
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
230
231
            throw OAuthServerException::invalidGrant();
232
        }
233
234 1
        return $deviceCode;
235
    }
236
237
    /**
238
     * @param string $encryptedDeviceCode
239
     *
240
     * @throws OAuthServerException
241
     *
242
     * @return \stdClass
243
     */
244 1
    protected function decodeDeviceCode($encryptedDeviceCode)
245
    {
246
        try {
247 1
            return \json_decode($this->decrypt($encryptedDeviceCode));
248
        } catch (LogicException $e) {
249
            throw OAuthServerException::invalidRequest('device_code', 'Cannot decrypt the device code', $e);
250
        }
251
    }
252
253
    /**
254
     * Set the verification uri
255
     *
256
     * @param string $verificationUri
257
     */
258
    public function setVerificationUri($verificationUri)
259
    {
260
        $this->verificationUri = $verificationUri;
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     */
266 4
    public function getIdentifier()
267
    {
268 4
        return 'urn:ietf:params:oauth:grant-type:device_code';
269
    }
270
271
    /**
272
     * @param DeviceCodeRepositoryInterface $deviceCodeRepository
273
     */
274 9
    public function setDeviceCodeRepository(DeviceCodeRepositoryInterface $deviceCodeRepository)
275
    {
276 9
        $this->deviceCodeRepository = $deviceCodeRepository;
277 9
    }
278
279
    /**
280
     * Issue a device code.
281
     *
282
     * @param DateInterval           $deviceCodeTTL
283
     * @param ClientEntityInterface  $client
284
     * @param string                 $verificationUri
285
     * @param ScopeEntityInterface[] $scopes
286
     *
287
     * @return DeviceCodeEntityInterface
288
     *
289
     * @throws OAuthServerException
290
     * @throws UniqueTokenIdentifierConstraintViolationException
291
     */
292 1
    protected function issueDeviceCode(
293
        DateInterval $deviceCodeTTL,
294
        ClientEntityInterface $client,
295
        $verificationUri,
296
        array $scopes = []
297
    ) {
298 1
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
299
300 1
        $deviceCode = $this->deviceCodeRepository->getNewDeviceCode();
301 1
        $deviceCode->setExpiryDateTime((new DateTimeImmutable())->add($deviceCodeTTL));
302 1
        $deviceCode->setClient($client);
303 1
        $deviceCode->setVerificationUri($verificationUri);
304
305 1
        foreach ($scopes as $scope) {
306
            $deviceCode->addScope($scope);
307
        }
308
309 1
        while ($maxGenerationAttempts-- > 0) {
310 1
            $deviceCode->setIdentifier($this->generateUniqueIdentifier());
311 1
            $deviceCode->setUserCode($this->generateUniqueUserCode());
312
            try {
313 1
                $this->deviceCodeRepository->persistNewDeviceCode($deviceCode);
314
315 1
                return $deviceCode;
316
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
0 ignored issues
show
Bug introduced by
The type League\OAuth2\Server\Gra...raintViolationException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
317
                if ($maxGenerationAttempts === 0) {
318
                    throw $e;
319
                }
320
            }
321
        }
322
    }
323
324
    /**
325
     * Generate a new unique user code.
326
     *
327
     * @param int $length
328
     *
329
     * @return string
330
     *
331
     * @throws OAuthServerException
332
     */
333 1
    protected function generateUniqueUserCode($length = 8)
334
    {
335
        try {
336 1
            $userCode = '';
337 1
            $userCodeCharacters = 'BCDFGHJKLMNPQRSTVWXZ';
338
339 1
            while (\strlen($userCode) < $length) {
340 1
                $userCode .= $userCodeCharacters[\random_int(0, 19)];
341
            }
342
343 1
            return $userCode;
344
            // @codeCoverageIgnoreStart
345
        } catch (TypeError $e) {
346
            throw OAuthServerException::serverError('An unexpected error has occurred', $e);
347
        } catch (Error $e) {
348
            throw OAuthServerException::serverError('An unexpected error has occurred', $e);
349
        } catch (Exception $e) {
350
            // If you get this message, the CSPRNG failed hard.
351
            throw OAuthServerException::serverError('Could not generate a random string', $e);
352
        }
353
        // @codeCoverageIgnoreEnd
354
    }
355
}
356