diff --git a/examples/public/device_code.php b/examples/public/device_code.php index 503a554ee..b88a67838 100644 --- a/examples/public/device_code.php +++ b/examples/public/device_code.php @@ -51,7 +51,7 @@ $deviceCodeRepository, $refreshTokenRepository, new \DateInterval('PT10M'), - 5 + 'http://foo/bar' ), new \DateInterval('PT1H') ); @@ -65,17 +65,17 @@ $server = $app->getContainer()->get(AuthorizationServer::class); try { - $deviceAuthRequest = $server->validateDeviceAuthorizationRequest($request); + $deviceCodeResponse = $server->respondToDeviceAuthorizationRequest($request, $response); - // TODO: I don't think this is right as the user can't approve the request via the same client... - // Once the user has logged in, set the user on the authorization request - //$deviceAuthRequest->setUserIdentifier(); + return $deviceCodeResponse; - // Once the user has approved or denied the client, update the status - //$deviceAuthRequest->setAuthorizationApproved(true); + // Extract the device code. Usually we would then assign the user ID to + // the device code but for the purposes of this example, we've hard + // coded it in the response above. + // $deviceCode = json_decode((string) $deviceCodeResponse->getBody()); - // Return the HTTP redirect response - return $server->completeDeviceAuthorizationRequest($deviceAuthRequest, $response); + // Once the user has logged in and approved the request, set the user on the device code + // $server->completeDeviceAuthorizationRequest($deviceCode->user_code, 1); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } catch (\Exception $exception) { diff --git a/examples/src/Repositories/DeviceCodeRepository.php b/examples/src/Repositories/DeviceCodeRepository.php index e4b76d368..6848d0235 100644 --- a/examples/src/Repositories/DeviceCodeRepository.php +++ b/examples/src/Repositories/DeviceCodeRepository.php @@ -35,10 +35,12 @@ public function persistDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity) /** * {@inheritdoc} */ - public function getDeviceCodeEntityByDeviceCode($deviceCode, $grantType, ClientEntityInterface $clientEntity) + public function getDeviceCodeEntityByDeviceCode($deviceCode) { $deviceCode = new DeviceCodeEntity(); + $deviceCode->setIdentifier('device_code_1234'); + // The user identifier should be set when the user authenticates on the OAuth server $deviceCode->setUserIdentifier(1); diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php index eaf4a8fc9..a9042abbb 100644 --- a/src/AuthorizationServer.php +++ b/src/AuthorizationServer.php @@ -207,17 +207,11 @@ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $requ /** * Complete a device authorization request - * - * @param DeviceAuthorizationRequest $deviceRequest - * @param ResponseInterface $response - * - * @return ResponseInterface */ - public function completeDeviceAuthorizationRequest(DeviceAuthorizationRequest $deviceRequest, ResponseInterface $response) + public function completeDeviceAuthorizationRequest(string $deviceCode, string|int $userId): ResponseInterface { - // TODO: Check why we aren't just using completeAuthorizationRequest - return $this->enabledGrantTypes[$deviceRequest->getGrantTypeId()] - ->completeDeviceAuthorizationRequest($deviceRequest) + return $this->enabledGrantTypes['urn:ietf:params:oauth:grant-type:device_code'] + ->completeDeviceAuthorizationRequest($deviceCode, $userId) ->generateHttpResponse($response); } diff --git a/src/Entities/DeviceCodeEntityInterface.php b/src/Entities/DeviceCodeEntityInterface.php index 571d7ee26..07a824cea 100644 --- a/src/Entities/DeviceCodeEntityInterface.php +++ b/src/Entities/DeviceCodeEntityInterface.php @@ -24,4 +24,16 @@ public function setVerificationUri(string $verificationUri); public function getLastPolledAt(): ?DateTimeImmutable; public function setLastPolledAt(DateTimeImmutable $lastPolledAt): void; + + public function getInterval(): int; + + public function setInterval(int $interval): void; + + public function getIntervalInAuthResponse(): bool; + + public function setIntervalInAuthResponse(bool $intervalInAuthResponse): bool; + + public function getUserApproved(): bool; + + public function setUserApproved(bool $userApproved): void; } diff --git a/src/Entities/Traits/DeviceCodeTrait.php b/src/Entities/Traits/DeviceCodeTrait.php index a9c0873de..0ea5160e3 100644 --- a/src/Entities/Traits/DeviceCodeTrait.php +++ b/src/Entities/Traits/DeviceCodeTrait.php @@ -15,6 +15,9 @@ trait DeviceCodeTrait { + private bool $userApproved = false; + private bool $intervalInAuthResponse = false; + private int $interval = 5; private string $userCode; private string $verificationUri; private ?DateTimeImmutable $lastPolledAt = null; @@ -59,4 +62,34 @@ public function setLastPolledAt(DateTimeImmutable $lastPolledAt): void { $this->lastPolledAt = $lastPolledAt; } + + public function getInterval(): int + { + return $this->interval; + } + + public function setInterval(int $interval): void + { + $this->interval = $interval; + } + + public function getIntervalInAuthResponse(): bool + { + return $this->intervalInAuthResponse; + } + + public function setIntervalInAuthResponse(bool $intervalInAuthResponse): bool + { + return $this->intervalInAuthResponse = $intervalInAuthResponse; + } + + public function getUserApproved(): bool + { + return $this->userApproved; + } + + public function setUserApproved(bool $userApproved): void + { + $this->userApproved = $userApproved; + } } diff --git a/src/Grant/AbstractGrant.php b/src/Grant/AbstractGrant.php index 8162a573d..1671a8202 100644 --- a/src/Grant/AbstractGrant.php +++ b/src/Grant/AbstractGrant.php @@ -641,7 +641,7 @@ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $requ /** * {@inheritdoc} */ - public function completeDeviceAuthorizationRequest(string $deviceCode, string|int $userId) + public function completeDeviceAuthorizationRequest(string $deviceCode, string|int $userId, bool $userApproved) { throw new LogicException('This grant cannot complete a device authorization request'); } diff --git a/src/Grant/DeviceCodeGrant.php b/src/Grant/DeviceCodeGrant.php index 13e0ef634..6fb0d3c42 100644 --- a/src/Grant/DeviceCodeGrant.php +++ b/src/Grant/DeviceCodeGrant.php @@ -41,37 +41,18 @@ */ class DeviceCodeGrant extends AbstractGrant { - /** - * @var DeviceCodeRepositoryInterface - */ - protected $deviceCodeRepository; - - /** - * @var DateInterval - */ - private $deviceCodeTTL; + protected DeviceCodeRepositoryInterface $deviceCodeRepository; + private DateInterval $deviceCodeTTL; + private bool $intervalVisibility = false; + private int $retryInterval; + private string $verificationUri; - /** - * @var int - */ - private $retryInterval; - - /** - * @var string - */ - private $verificationUri; - - /** - * @param DeviceCodeRepositoryInterface $deviceCodeRepository - * @param RefreshTokenRepositoryInterface $refreshTokenRepository - * @param DateInterval $deviceCodeTTL - * @param int $retryInterval - */ public function __construct( DeviceCodeRepositoryInterface $deviceCodeRepository, RefreshTokenRepositoryInterface $refreshTokenRepository, DateInterval $deviceCodeTTL, - $retryInterval = 5 + string $verificationUri, + int $retryInterval = 5 ) { $this->setDeviceCodeRepository($deviceCodeRepository); $this->setRefreshTokenRepository($refreshTokenRepository); @@ -79,6 +60,7 @@ public function __construct( $this->refreshTokenTTL = new DateInterval('P1M'); $this->deviceCodeTTL = $deviceCodeTTL; + $this->setVerificationUri($verificationUri); $this->retryInterval = $retryInterval; } @@ -107,14 +89,8 @@ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $requ $client = $this->getClientEntityOrFail($clientId, $request); - // TODO: Make sure the grant type is set... - $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope)); - // TODO: Don't think I need the deviceauthorizationrequest any more. Might repurpose it... - // $deviceAuthorizationRequest = new DeviceAuthorizationRequest(); - // $deviceAuthorizationRequest->setGrantTypeId($this->getIdentifier()); - $deviceCode = $this->issueDeviceCode( $this->deviceCodeTTL, $client, @@ -122,6 +98,8 @@ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $requ $scopes ); + // TODO: Why do we need this? Why not just generate a random number? Is it a security concern? + // TODO: Do I need to set the interval in this? $payload = [ 'device_code_id' => $deviceCode->getIdentifier(), 'user_code' => $deviceCode->getUserCode(), @@ -143,11 +121,12 @@ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $requ /** * {@inheritdoc} */ - public function completeDeviceAuthorizationRequest(string $deviceCode, string|int $userId) + public function completeDeviceAuthorizationRequest(string $deviceCode, string|int $userId, bool $approved) { $deviceCode = $this->deviceCodeRepository->getDeviceCodeEntityByDeviceCode($deviceCode); $deviceCode->setUserIdentifier($userId); + $deviceCode->setUserApproved($approved); $this->deviceCodeRepository->persistDeviceCode($deviceCode); } @@ -161,27 +140,22 @@ public function respondToAccessTokenRequest( DateInterval $accessTokenTTL ) { // Validate request - // TODO: Check that the correct grant type has been sent $client = $this->validateClient($request); $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope)); $deviceCode = $this->validateDeviceCode($request, $client); - // TODO: Should the last poll and user check be done in the validateDeviceCode method? - $lastPoll = $deviceCode->getLastPolledAt(); - - if ($lastPoll !== null && $lastPoll->getTimestamp() + $this->retryInterval > time()) { - throw OAuthServerException::slowDown(); - } - $deviceCode->setLastPolledAt(new DateTimeImmutable()); - $this->deviceCodeRepository->persistDeviceCode($deviceCode); - // if device code has no user associated, respond with pending + // If device code has no user associated, respond with pending if (is_null($deviceCode->getUserIdentifier())) { throw OAuthServerException::authorizationPending(); } + if ($deviceCode->getUserApproved() === false) { + throw OAuthServerException::accessDenied(); + } + // Finalize the requested scopes $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, (string) $deviceCode->getUserIdentifier()); @@ -238,10 +212,14 @@ protected function validateDeviceCode(ServerRequestInterface $request, ClientEnt } $deviceCode = $this->deviceCodeRepository->getDeviceCodeEntityByDeviceCode( - $deviceCodePayload->device_code_id, - $this->getIdentifier(), - $client - ); + $deviceCodePayload->device_code_id, + $this->getIdentifier(), + $client + ); + + if ($this->deviceCodePolledTooSoon($deviceCode->getLastPolledAt()) === true) { + throw OAuthServerException::slowDown(); + } if ($deviceCode instanceof DeviceCodeEntityInterface === false) { $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); @@ -252,6 +230,11 @@ protected function validateDeviceCode(ServerRequestInterface $request, ClientEnt return $deviceCode; } + private function deviceCodePolledTooSoon(?DateTimeImmutable $lastPoll): bool + { + return $lastPoll !== null && $lastPoll->getTimestamp() + $this->retryInterval > time(); + } + /** * @param string $encryptedDeviceCode * @@ -320,6 +303,10 @@ protected function issueDeviceCode( $deviceCode->setClient($client); $deviceCode->setVerificationUri($verificationUri); + if ($this->getIntervalVisibility() === true) { + $deviceCode->setInterval($this->retryInterval); + } + foreach ($scopes as $scope) { $deviceCode->addScope($scope); } @@ -370,4 +357,14 @@ protected function generateUniqueUserCode($length = 8) } // @codeCoverageIgnoreEnd } + + public function setIntervalVisibility(bool $intervalVisibility): void + { + $this->intervalVisibility = $intervalVisibility; + } + + public function getIntervalVisibility(): bool + { + return $this->intervalVisibility; + } } diff --git a/src/Grant/GrantTypeInterface.php b/src/Grant/GrantTypeInterface.php index 2ccb0f1a6..87fee0a8e 100644 --- a/src/Grant/GrantTypeInterface.php +++ b/src/Grant/GrantTypeInterface.php @@ -133,7 +133,7 @@ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $requ * * @return ResponseTypeInterface */ - public function completeDeviceAuthorizationRequest(string $deviceCode, string|int $userId); + public function completeDeviceAuthorizationRequest(string $deviceCode, string|int $userId, bool $userApproved); /** * Set the client repository. diff --git a/src/ResponseTypes/DeviceCodeResponse.php b/src/ResponseTypes/DeviceCodeResponse.php index bb3193b9a..33b3282d9 100644 --- a/src/ResponseTypes/DeviceCodeResponse.php +++ b/src/ResponseTypes/DeviceCodeResponse.php @@ -15,17 +15,12 @@ use LogicException; use Psr\Http\Message\ResponseInterface; +use function time; + class DeviceCodeResponse extends AbstractResponseType { - /** - * @var DeviceCodeEntityInterface - */ - protected $deviceCode; - - /** - * @var string - */ - protected $payload; + protected DeviceCodeEntityInterface $deviceCode; + protected string $payload; /** * {@inheritdoc} @@ -38,11 +33,14 @@ public function generateHttpResponse(ResponseInterface $response) 'device_code' => $this->payload, 'user_code' => $this->deviceCode->getUserCode(), 'verification_uri' => $this->deviceCode->getVerificationUri(), - 'expires_in' => $expireDateTime - \time(), - // TODO: Add interval in here + 'expires_in' => $expireDateTime - time(), // TODO: Potentially add in verification_uri_complete - it is optional ]; + if ($this->deviceCode->getIntervalInAuthResponse() === true) { + $responseParams['interval'] = $this->deviceCode->getInterval(); + } + $responseParams = \json_encode($responseParams); if ($responseParams === false) { diff --git a/tests/Grant/DeviceCodeGrantTest.php b/tests/Grant/DeviceCodeGrantTest.php index cb82d6111..537019568 100644 --- a/tests/Grant/DeviceCodeGrantTest.php +++ b/tests/Grant/DeviceCodeGrantTest.php @@ -53,7 +53,7 @@ public function testGetIdentifier() $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, new DateInterval('PT10M'), - 5 + "http://foo/bar" ); $this->assertEquals('urn:ietf:params:oauth:grant-type:device_code', $grant->getIdentifier()); @@ -64,7 +64,8 @@ public function testCanRespondToDeviceAuthorizationRequest() $grant = new DeviceCodeGrant( $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), - new DateInterval('PT10M') + new DateInterval('PT10M'), + "http://foo/bar" ); $request = (new ServerRequest())->withParsedBody([ @@ -75,7 +76,7 @@ public function testCanRespondToDeviceAuthorizationRequest() $this->assertTrue($grant->canRespondToDeviceAuthorizationRequest($request)); } - public function testValidateDeviceAuthorizationRequest() + public function testRespondToDeviceAuthorizationRequest() { $client = new ClientEntity(); $client->setIdentifier('foo'); @@ -93,15 +94,15 @@ public function testValidateDeviceAuthorizationRequest() $grant = new DeviceCodeGrant( $deviceCodeRepository, - $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), // TODO: Does this have a refersh token? - new DateInterval('PT10M') + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new DateInterval('PT10M'), + "http://foo/bar" ); $grant->setClientRepository($clientRepositoryMock); $grant->setDefaultScope(self::DEFAULT_SCOPE); $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setScopeRepository($scopeRepositoryMock); - $grant->setVerificationUri('http://foo/bar'); $request = (new ServerRequest())->withParsedBody([ 'client_id' => 'foo', @@ -126,7 +127,8 @@ public function testValidateDeviceAuthorizationRequestMissingClient() $grant = new DeviceCodeGrant( $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), - new DateInterval('PT10M') + new DateInterval('PT10M'), + 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); @@ -156,7 +158,8 @@ public function testValidateDeviceAuthorizationRequestEmptyScope() $grant = new DeviceCodeGrant( $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), - new DateInterval('PT10M') + new DateInterval('PT10M'), + 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); @@ -183,7 +186,8 @@ public function testValidateDeviceAuthorizationRequestClientMismatch() $grant = new DeviceCodeGrant( $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(), $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), - new DateInterval('PT10M') + new DateInterval('PT10M'), + 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); $grant->setScopeRepository($scopeRepositoryMock); @@ -210,13 +214,13 @@ public function testCompleteDeviceAuthorizationRequest() $grant = new DeviceCodeGrant( $deviceCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), - new DateInterval('PT10M') + new DateInterval('PT10M'), + 'http://foo/bar', ); - $grant->setVerificationUri('http://foo/bar'); $grant->setEncryptionKey($this->cryptStub->getKey()); - $grant->completeDeviceAuthorizationRequest($deviceCode->getUserCode(), 'userId'); + $grant->completeDeviceAuthorizationRequest($deviceCode->getUserCode(), 'userId', true); $this->assertEquals('userId', $deviceCode->getUserIdentifier()); } @@ -261,11 +265,11 @@ public function testDeviceAuthorizationResponse() $deviceCodeGrant = new DeviceCodeGrant( $deviceCodeRepository, $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), - new DateInterval('PT10M') + new DateInterval('PT10M'), + 'http://foo/bar' ); $deviceCodeGrant->setEncryptionKey($this->cryptStub->getKey()); - $deviceCodeGrant->setVerificationUri('http://foo/bar'); $server->enableGrantType($deviceCodeGrant); @@ -273,11 +277,11 @@ public function testDeviceAuthorizationResponse() $responseObject = json_decode($response->getBody()->__toString()); - $this->assertObjectHasAttribute('device_code', $responseObject); - $this->assertObjectHasAttribute('user_code', $responseObject); - $this->assertObjectHasAttribute('verification_uri', $responseObject); + $this->assertObjectHasProperty('device_code', $responseObject); + $this->assertObjectHasProperty('user_code', $responseObject); + $this->assertObjectHasProperty('verification_uri', $responseObject); // TODO: $this->assertObjectHasAttribute('verification_uri_complete', $responseObject); - $this->assertObjectHasAttribute('expires_in', $responseObject); + $this->assertObjectHasProperty('expires_in', $responseObject); // TODO: $this->assertObjectHasAttribute('interval', $responseObject); } @@ -297,10 +301,13 @@ public function testRespondToAccessTokenRequest() $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); $deviceCodeRepositoryMock = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); - $deviceCodeEntity = new DeviceCodeEntity(); - $deviceCodeEntity->setUserIdentifier('baz'); - $deviceCodeEntity->setIdentifier('deviceCodeEntityIdentifier'); - $deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCodeEntity); + $deviceCode = new DeviceCodeEntity(); + + $deviceCode->setUserIdentifier('baz'); + $deviceCode->setIdentifier('deviceCodeEntityIdentifier'); + $deviceCode->setUserCode('123456'); + + $deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCode); $scope = new ScopeEntity(); $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); @@ -310,7 +317,8 @@ public function testRespondToAccessTokenRequest() $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, - new DateInterval('PT10M') + new DateInterval('PT10M'), + 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); @@ -320,6 +328,8 @@ public function testRespondToAccessTokenRequest() $grant->setEncryptionKey($this->cryptStub->getKey()); $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $grant->completeDeviceAuthorizationRequest($deviceCode->getUserCode(), 1, true); + $serverRequest = (new ServerRequest())->withParsedBody([ 'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code', 'device_code' => $this->cryptStub->doEncrypt( @@ -356,7 +366,8 @@ public function testRespondToRequestMissingClient() $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, - new DateInterval('PT10M') + new DateInterval('PT10M'), + 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); @@ -408,7 +419,8 @@ public function testRespondToRequestMissingDeviceCode() $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, - new DateInterval('PT10M') + new DateInterval('PT10M'), + 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); @@ -453,7 +465,8 @@ public function testIssueSlowDownError() $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, - new DateInterval('PT10M') + new DateInterval('PT10M'), + 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); @@ -509,7 +522,8 @@ function testIssueAuthorizationPendingError() $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, - new DateInterval('PT10M') + new DateInterval('PT10M'), + 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); @@ -565,7 +579,8 @@ function testIssueExpiredTokenError() $grant = new DeviceCodeGrant( $deviceCodeRepositoryMock, $refreshTokenRepositoryMock, - new DateInterval('PT10M') + new DateInterval('PT10M'), + 'http://foo/bar' ); $grant->setClientRepository($clientRepositoryMock); @@ -598,6 +613,113 @@ function testIssueExpiredTokenError() $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); } - // NEED TO ADD IN TESTS FOR: - // access_denied - for this one, we need to add it to the completeDeviceAuthorizationRequest method + public function testIntervalVisibility() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $deviceCode = new DeviceCodeEntity(); + + $deviceCode->setIntervalInAuthResponse(true); + + $deviceCodeRepository = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); + $deviceCodeRepository->method('getNewDeviceCode')->willReturn($deviceCode); + + $scope = new ScopeEntity(); + $scope->setIdentifier('basic'); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); + + $grant = new DeviceCodeGrant( + $deviceCodeRepository, + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new DateInterval('PT10M'), + "http://foo/bar" + ); + + $grant->setClientRepository($clientRepositoryMock); + $grant->setDefaultScope(self::DEFAULT_SCOPE); + $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setScopeRepository($scopeRepositoryMock); + + $request = (new ServerRequest())->withParsedBody([ + 'client_id' => 'foo', + 'scope' => 'basic', + ]); + + $deviceCodeResponse = $grant + ->respondToDeviceAuthorizationRequest($request) + ->generateHttpResponse(new Response()); + + $deviceCode = json_decode((string) $deviceCodeResponse->getBody()); + + $this->assertObjectHasProperty('interval', $deviceCode); + $this->assertEquals(5, $deviceCode->interval); + } + + public function testIssueAccessDeniedError(): void + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $deviceCodeRepositoryMock = $this->getMockBuilder(DeviceCodeRepositoryInterface::class)->getMock(); + + $deviceCode = new DeviceCodeEntity(); + + $deviceCode->setUserCode('12345678'); + $deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCode); + + $scope = new ScopeEntity(); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $grant = new DeviceCodeGrant( + $deviceCodeRepositoryMock, + $refreshTokenRepositoryMock, + new DateInterval('PT10M'), + 'http://foo/bar' + ); + + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setDefaultScope(self::DEFAULT_SCOPE); + $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $grant->completeDeviceAuthorizationRequest($deviceCode->getUserCode(), 1, false); + + $serverRequest = (new ServerRequest())->withParsedBody([ + 'client_id' => 'foo', + 'device_code' => $this->cryptStub->doEncrypt( + \json_encode( + [ + 'device_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'user_code' => '12345678', + 'scopes' => ['foo'], + 'verification_uri' => 'http://foo/bar', + ] + ) + ), + ]); + + $responseType = new StubResponseType(); + + $this->expectException(\League\OAuth2\Server\Exception\OAuthServerException::class); + $this->expectExceptionCode(9); + + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new DateInterval('PT5M')); + + } } diff --git a/tests/ResponseTypes/BearerResponseTypeTest.php b/tests/ResponseTypes/BearerResponseTypeTest.php index 1921d3844..0626ddd71 100644 --- a/tests/ResponseTypes/BearerResponseTypeTest.php +++ b/tests/ResponseTypes/BearerResponseTypeTest.php @@ -59,9 +59,9 @@ public function testGenerateHttpResponse() $response->getBody()->rewind(); $json = \json_decode($response->getBody()->getContents()); $this->assertEquals('Bearer', $json->token_type); - $this->assertObjectHasAttribute('expires_in', $json); - $this->assertObjectHasAttribute('access_token', $json); - $this->assertObjectHasAttribute('refresh_token', $json); + $this->assertObjectHasProperty('expires_in', $json); + $this->assertObjectHasProperty('access_token', $json); + $this->assertObjectHasProperty('refresh_token', $json); } public function testGenerateHttpResponseWithExtraParams() @@ -103,11 +103,11 @@ public function testGenerateHttpResponseWithExtraParams() $response->getBody()->rewind(); $json = \json_decode($response->getBody()->getContents()); $this->assertEquals('Bearer', $json->token_type); - $this->assertObjectHasAttribute('expires_in', $json); - $this->assertObjectHasAttribute('access_token', $json); - $this->assertObjectHasAttribute('refresh_token', $json); + $this->assertObjectHasProperty('expires_in', $json); + $this->assertObjectHasProperty('access_token', $json); + $this->assertObjectHasProperty('refresh_token', $json); - $this->assertObjectHasAttribute('foo', $json); + $this->assertObjectHasProperty('foo', $json); $this->assertEquals('bar', $json->foo); } diff --git a/tests/ResponseTypes/DeviceCodeResponseTypeTest.php b/tests/ResponseTypes/DeviceCodeResponseTypeTest.php index 111f4a7c0..88044ca68 100644 --- a/tests/ResponseTypes/DeviceCodeResponseTypeTest.php +++ b/tests/ResponseTypes/DeviceCodeResponseTypeTest.php @@ -49,10 +49,10 @@ public function testGenerateHttpResponse() $response->getBody()->rewind(); $json = \json_decode($response->getBody()->getContents()); - $this->assertObjectHasAttribute('expires_in', $json); - $this->assertObjectHasAttribute('device_code', $json); + $this->assertObjectHasProperty('expires_in', $json); + $this->assertObjectHasProperty('device_code', $json); $this->assertEquals('test', $json->device_code); - $this->assertObjectHasAttribute('verification_uri', $json); - $this->assertObjectHasAttribute('user_code', $json); + $this->assertObjectHasProperty('verification_uri', $json); + $this->assertObjectHasProperty('user_code', $json); } }