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

completeDeviceAuthorizationRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 2.0005

Importance

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