Passed
Pull Request — master (#1074)
by Andrew
02:07
created

DeviceCodeGrant::setDeviceCodeRepository()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 1
b 0
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\Entities\ScopeEntityInterface;
21
use League\OAuth2\Server\Exception\OAuthServerException;
22
use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException;
23
use League\OAuth2\Server\Repositories\DeviceCodeRepositoryInterface;
24
use League\OAuth2\Server\Repositories\DeviceAuthorizationRequestRepository;
25
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
26
use League\OAuth2\Server\RequestEvent;
27
use League\OAuth2\Server\RequestTypes\DeviceAuthorizationRequest;
28
use League\OAuth2\Server\ResponseTypes\DeviceCodeResponse;
29
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
30
use LogicException;
31
use Psr\Http\Message\ServerRequestInterface;
32
use TypeError;
33
34
/**
35
 * Device Code grant class.
36
 */
37
class DeviceCodeGrant extends AbstractGrant
38
{
39
    /**
40
     * @var DeviceCodeRepositoryInterface
41
     */
42
    protected $deviceCodeRepository;
43
44
    /**
45
     * @var DeviceAuthorizationRequestRepository
46
     */
47
    protected $deviceAuthorizationRequestRepository;
48
49
    /**
50
     * @var DateInterval
51
     */
52
    private $deviceCodeTTL;
53
54
    /**
55
     * @var int
56
     */
57
    private $retryInterval;
58
59
    /**
60
     * @var string
61
     */
62
    private $verificationUri;
63
64
    /**
65
     * @param DeviceCodeRepositoryInterface        $deviceCodeRepository
66
     * @param DeviceAuthorizationRequestRepository $deviceAuthorizationRequestRepository,
67
     * @param RefreshTokenRepositoryInterface      $refreshTokenRepository
68
     * @param DateInterval                         $deviceCodeTTL
69
     * @param int                                  $retryInterval
70
     */
71 10
    public function __construct(
72
        DeviceCodeRepositoryInterface $deviceCodeRepository,
73
        DeviceAuthorizationRequestRepository $deviceAuthorizationRequestRepository,
74
        RefreshTokenRepositoryInterface $refreshTokenRepository,
75
        DateInterval $deviceCodeTTL,
76
        $retryInterval = 5
77
    ) {
78 10
        $this->setDeviceCodeRepository($deviceCodeRepository);
79 10
        $this->setDeviceAuthorizationRequestRepository($deviceAuthorizationRequestRepository);
80 10
        $this->setRefreshTokenRepository($refreshTokenRepository);
81
82 10
        $this->refreshTokenTTL = new DateInterval('P1M');
83
84 10
        $this->deviceCodeTTL = $deviceCodeTTL;
85 10
        $this->retryInterval = $retryInterval;
86 10
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91 1
    public function canRespondToDeviceAuthorizationRequest(ServerRequestInterface $request)
92
    {
93 1
        return true;
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99 3
    public function validateDeviceAuthorizationRequest(ServerRequestInterface $request)
100
    {
101 3
        $clientId = $this->getRequestParameter(
102 3
            'client_id',
103
            $request,
104 3
            $this->getServerParameter('PHP_AUTH_USER', $request)
105
        );
106
107 3
        if ($clientId === null) {
108 1
            throw OAuthServerException::invalidRequest('client_id');
109
        }
110
111 2
        $client = $this->getClientEntityOrFail($clientId, $request);
112
113 1
        $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
114
115 1
        $deviceAuthorizationRequest = new DeviceAuthorizationRequest();
116 1
        $deviceAuthorizationRequest->setGrantTypeId($this->getIdentifier());
117 1
        $deviceAuthorizationRequest->setClient($client);
118 1
        $deviceAuthorizationRequest->setScopes($scopes);
119
120 1
        return $deviceAuthorizationRequest;
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126 1
    public function completeDeviceAuthorizationRequest(DeviceAuthorizationRequest $deviceRequest)
127
    {
128 1
        $deviceCode = $this->issueDeviceCode(
129 1
            $this->deviceCodeTTL,
130 1
            $deviceRequest->getClient(),
131 1
            $this->verificationUri,
132 1
            $deviceRequest->getScopes()
133
        );
134
135
        $payload = [
136 1
            'client_id' => $deviceCode->getClient()->getIdentifier(),
137 1
            'device_code_id' => $deviceCode->getIdentifier(),
138 1
            'scopes' => $deviceCode->getScopes(),
139 1
            'user_code' => $deviceCode->getUserCode(),
140 1
            'expire_time' => $deviceCode->getExpiryDateTime()->getTimestamp(),
141 1
            'verification_uri' => $deviceCode->getVerificationUri(),
142
        ];
143
144 1
        $jsonPayload = \json_encode($payload);
145
146 1
        if ($jsonPayload === false) {
147
            throw new LogicException('An error was encountered when JSON encoding the authorization request response');
148
        }
149
150 1
        $response = new DeviceCodeResponse();
151 1
        $response->setDeviceCode($deviceCode);
152 1
        $response->setPayload($this->encrypt($jsonPayload));
153
154 1
        return $response;
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160 4
    public function respondToAccessTokenRequest(
161
        ServerRequestInterface $request,
162
        ResponseTypeInterface $responseType,
163
        DateInterval $accessTokenTTL
164
    ) {
165
        // Validate request
166 4
        $client = $this->validateClient($request);
167 3
        $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
168 3
        $deviceCode = $this->validateDeviceCode($request, $client);
169
170 2
        $lastRequest = $this->deviceAuthorizationRequestRepository->getLast($client->getIdentifier());
171
172 2
        if ($lastRequest !== null && $lastRequest->getTimestamp() + $this->retryInterval > \time()) {
173 1
            throw OAuthServerException::slowDown();
174
        }
175
176 1
        $this->deviceAuthorizationRequestRepository->persist($deviceCode);
0 ignored issues
show
Unused Code introduced by
The call to League\OAuth2\Server\Rep...stRepository::persist() has too many arguments starting with $deviceCode. ( Ignorable by Annotation )

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

176
        $this->deviceAuthorizationRequestRepository->/** @scrutinizer ignore-call */ 
177
                                                     persist($deviceCode);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
177
178
        // if device code has no user associated, respond with pending
179 1
        if (\is_null($deviceCode->getUserIdentifier())) {
180
            throw OAuthServerException::authorizationPending();
181
        }
182
183
        // Finalize the requested scopes
184 1
        $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, (string) $deviceCode->getUserIdentifier());
185
186
        // Issue and persist new access token
187 1
        $accessToken = $this->issueAccessToken($accessTokenTTL, $client, (string) $deviceCode->getUserIdentifier(), $finalizedScopes);
188 1
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
189 1
        $responseType->setAccessToken($accessToken);
190
191
        // Issue and persist new refresh token if given
192 1
        $refreshToken = $this->issueRefreshToken($accessToken);
193
194 1
        if ($refreshToken !== null) {
195 1
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_ISSUED, $request));
196 1
            $responseType->setRefreshToken($refreshToken);
197
        }
198
199 1
        $this->deviceCodeRepository->revokeDeviceCode($deviceCode->getIdentifier());
200
201 1
        return $responseType;
202
    }
203
204
    /**
205
     * @param ServerRequestInterface $request
206
     * @param ClientEntityInterface  $client
207
     *
208
     * @throws OAuthServerException
209
     *
210
     * @return DeviceCodeEntityInterface
211
     */
212 3
    protected function validateDeviceCode(ServerRequestInterface $request, ClientEntityInterface $client)
213
    {
214 3
        $encryptedDeviceCode = $this->getRequestParameter('device_code', $request);
215
216 3
        if (\is_null($encryptedDeviceCode)) {
217 1
            throw OAuthServerException::invalidRequest('device_code');
218
        }
219
220 2
        $deviceCodePayload = $this->decodeDeviceCode($encryptedDeviceCode);
221
222 2
        if (!\property_exists($deviceCodePayload, 'device_code_id')) {
223
            throw OAuthServerException::invalidRequest('device_code', 'Device code malformed');
224
        }
225
226 2
        if (\time() > $deviceCodePayload->expire_time) {
227
            throw OAuthServerException::expiredToken('device_code');
228
        }
229
230 2
        if ($this->deviceCodeRepository->isDeviceCodeRevoked($deviceCodePayload->device_code_id) === true) {
231
            throw OAuthServerException::invalidRequest('device_code', 'Device code has been revoked');
232
        }
233
234 2
        if ($deviceCodePayload->client_id !== $client->getIdentifier()) {
235
            throw OAuthServerException::invalidRequest('device_code', 'Device code was not issued to this client');
236
        }
237
238 2
        $deviceCode = $this->deviceCodeRepository->getDeviceCodeEntityByDeviceCode(
239 2
            $deviceCodePayload->device_code_id,
240 2
            $this->getIdentifier(),
241
            $client
242
        );
243
244 2
        if ($deviceCode instanceof DeviceCodeEntityInterface === false) {
245
            $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
246
247
            throw OAuthServerException::invalidGrant();
248
        }
249
250 2
        return $deviceCode;
251
    }
252
253
    /**
254
     * @param string $encryptedDeviceCode
255
     *
256
     * @throws OAuthServerException
257
     *
258
     * @return \stdClass
259
     */
260 2
    protected function decodeDeviceCode($encryptedDeviceCode)
261
    {
262
        try {
263 2
            return \json_decode($this->decrypt($encryptedDeviceCode));
264
        } catch (LogicException $e) {
265
            throw OAuthServerException::invalidRequest('device_code', 'Cannot decrypt the device code', $e);
266
        }
267
    }
268
269
    /**
270
     * Set the verification uri
271
     *
272
     * @param string $verificationUri
273
     */
274
    public function setVerificationUri($verificationUri)
275
    {
276
        $this->verificationUri = $verificationUri;
277
    }
278
279
    /**
280
     * {@inheritdoc}
281
     */
282 5
    public function getIdentifier()
283
    {
284 5
        return 'urn:ietf:params:oauth:grant-type:device_code';
285
    }
286
287
    /**
288
     * @param DeviceCodeRepositoryInterface $deviceCodeRepository
289
     */
290 10
    private function setDeviceCodeRepository(DeviceCodeRepositoryInterface $deviceCodeRepository)
291
    {
292 10
        $this->deviceCodeRepository = $deviceCodeRepository;
293 10
    }
294
295
    /**
296
     * @param DeviceAuthorizationRequestRepository $deviceAuthorizationRequestRepository
297
     */
298 10
    private function setDeviceAuthorizationRequestRepository(
299
        DeviceAuthorizationRequestRepository $deviceAuthorizationRequestRepository
300
    ) {
301 10
        $this->deviceAuthorizationRequestRepository = $deviceAuthorizationRequestRepository;
302 10
    }
303
304
    /**
305
     * Issue a device code.
306
     *
307
     * @param DateInterval           $deviceCodeTTL
308
     * @param ClientEntityInterface  $client
309
     * @param string                 $verificationUri
310
     * @param ScopeEntityInterface[] $scopes
311
     *
312
     * @return DeviceCodeEntityInterface
313
     *
314
     * @throws OAuthServerException
315
     * @throws UniqueTokenIdentifierConstraintViolationException
316
     */
317 1
    protected function issueDeviceCode(
318
        DateInterval $deviceCodeTTL,
319
        ClientEntityInterface $client,
320
        $verificationUri,
321
        array $scopes = []
322
    ) {
323 1
        $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;
324
325 1
        $deviceCode = $this->deviceCodeRepository->getNewDeviceCode();
326 1
        $deviceCode->setExpiryDateTime((new DateTimeImmutable())->add($deviceCodeTTL));
327 1
        $deviceCode->setClient($client);
328 1
        $deviceCode->setVerificationUri($verificationUri);
329
330 1
        foreach ($scopes as $scope) {
331
            $deviceCode->addScope($scope);
332
        }
333
334 1
        while ($maxGenerationAttempts-- > 0) {
335 1
            $deviceCode->setIdentifier($this->generateUniqueIdentifier());
336 1
            $deviceCode->setUserCode($this->generateUniqueUserCode());
337
            try {
338 1
                $this->deviceCodeRepository->persistNewDeviceCode($deviceCode);
339
340 1
                return $deviceCode;
341
            } catch (UniqueTokenIdentifierConstraintViolationException $e) {
342
                if ($maxGenerationAttempts === 0) {
343
                    throw $e;
344
                }
345
            }
346
        }
347
    }
348
349
    /**
350
     * Generate a new unique user code.
351
     *
352
     * @param int $length
353
     *
354
     * @return string
355
     *
356
     * @throws OAuthServerException
357
     */
358 1
    protected function generateUniqueUserCode($length = 8)
359
    {
360
        try {
361 1
            $userCode = '';
362 1
            $userCodeCharacters = 'BCDFGHJKLMNPQRSTVWXZ';
363
364 1
            while (\strlen($userCode) < $length) {
365 1
                $userCode .= $userCodeCharacters[\random_int(0, 19)];
366
            }
367
368 1
            return $userCode;
369
            // @codeCoverageIgnoreStart
370
        } catch (TypeError $e) {
371
            throw OAuthServerException::serverError('An unexpected error has occurred', $e);
372
        } catch (Error $e) {
373
            throw OAuthServerException::serverError('An unexpected error has occurred', $e);
374
        } catch (Exception $e) {
375
            // If you get this message, the CSPRNG failed hard.
376
            throw OAuthServerException::serverError('Could not generate a random string', $e);
377
        }
378
        // @codeCoverageIgnoreEnd
379
    }
380
}
381