Skip to content

Commit

Permalink
encrypt device code
Browse files Browse the repository at this point in the history
  • Loading branch information
hafezdivandari committed Oct 28, 2024
1 parent b7751db commit 74f7ed5
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 52 deletions.
16 changes: 2 additions & 14 deletions examples/src/Repositories/DeviceCodeRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,12 @@ public function persistDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity):
/**
* {@inheritdoc}
*/
public function getDeviceCodeEntityByDeviceCode($deviceCode): ?DeviceCodeEntityInterface
public function getDeviceCodeEntityByDeviceCode(string $deviceCode): ?DeviceCodeEntityInterface
{
$clientEntity = new ClientEntity();
$clientEntity->setIdentifier('myawesomeapp');

$deviceCodeEntity = new DeviceCodeEntity();

$deviceCodeEntity->setIdentifier($deviceCode);
$deviceCodeEntity->setExpiryDateTime(new DateTimeImmutable('now +1 hour'));
$deviceCodeEntity->setClient($clientEntity);
$deviceCodeEntity->setLastPolledAt(new DateTimeImmutable());

$scopes = [];
foreach ($scopes as $scope) {
$scopeEntity = new ScopeEntity();
$scopeEntity->setIdentifier($scope);
$deviceCodeEntity->addScope($scopeEntity);
}
$deviceCodeEntity->setInterval(5);

// The user identifier should be set when the user authenticates on the
// OAuth server, along with whether they approved the request
Expand Down
46 changes: 34 additions & 12 deletions src/Grant/DeviceCodeGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\ResponseTypes\DeviceCodeResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
use TypeError;

Expand Down Expand Up @@ -96,6 +97,7 @@ public function respondToDeviceAuthorizationRequest(ServerRequestInterface $requ
);

$response = new DeviceCodeResponse();
$response->setEncryptionKey($this->encryptionKey);

if ($this->includeVerificationUriComplete === true) {
$response->includeVerificationUriComplete();
Expand Down Expand Up @@ -177,38 +179,58 @@ public function respondToAccessTokenRequest(
*/
protected function validateDeviceCode(ServerRequestInterface $request, ClientEntityInterface $client): DeviceCodeEntityInterface
{
$deviceCode = $this->getRequestParameter('device_code', $request);
$encryptedDeviceCode = $this->getRequestParameter('device_code', $request);

if (is_null($deviceCode)) {
if (is_null($encryptedDeviceCode)) {
throw OAuthServerException::invalidRequest('device_code');
}

$deviceCodeEntity = $this->deviceCodeRepository->getDeviceCodeEntityByDeviceCode(
$deviceCode
);

if ($deviceCodeEntity instanceof DeviceCodeEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
try {
$deviceCodePayload = json_decode($this->decrypt($encryptedDeviceCode));
} catch (LogicException $e) {
throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code', $e);
}

throw OAuthServerException::invalidGrant();
if (!property_exists($deviceCodePayload, 'device_code_id')) {
throw OAuthServerException::invalidRequest('device_code', 'Device code malformed');
}

if (time() > $deviceCodeEntity->getExpiryDateTime()->getTimestamp()) {
if (time() > $deviceCodePayload->expire_time) {
throw OAuthServerException::expiredToken('device_code');
}

if ($this->deviceCodeRepository->isDeviceCodeRevoked($deviceCode) === true) {
if ($this->deviceCodeRepository->isDeviceCodeRevoked($deviceCodePayload->device_code_id) === true) {
throw OAuthServerException::invalidRequest('device_code', 'Device code has been revoked');
}

if ($deviceCodeEntity->getClient()->getIdentifier() !== $client->getIdentifier()) {
if ($deviceCodePayload->client_id !== $client->getIdentifier()) {
throw OAuthServerException::invalidRequest('device_code', 'Device code was not issued to this client');
}

$deviceCodeEntity = $this->deviceCodeRepository->getDeviceCodeEntityByDeviceCode(
$deviceCodePayload->device_code_id
);

if ($deviceCodeEntity instanceof DeviceCodeEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));

throw OAuthServerException::invalidGrant();
}

if ($this->deviceCodePolledTooSoon($deviceCodeEntity->getLastPolledAt()) === true) {
throw OAuthServerException::slowDown();
}

$deviceCodeEntity->setIdentifier($deviceCodePayload->device_code_id);
$deviceCodeEntity->setClient($client);
$deviceCodeEntity->setExpiryDateTime((new DateTimeImmutable())->setTimestamp($deviceCodePayload->expire_time));

$scopes = $this->validateScopes($deviceCodePayload->scopes);

foreach ($scopes as $scope) {
$deviceCodeEntity->addScope($scope);
}

return $deviceCodeEntity;
}

Expand Down
15 changes: 14 additions & 1 deletion src/ResponseTypes/DeviceCodeResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,21 @@ public function generateHttpResponse(ResponseInterface $response): ResponseInter
{
$expireDateTime = $this->deviceCodeEntity->getExpiryDateTime()->getTimestamp();

$payload = [
'client_id' => $this->deviceCodeEntity->getClient()->getIdentifier(),
'device_code_id' => $this->deviceCodeEntity->getIdentifier(),
'scopes' => $this->deviceCodeEntity->getScopes(),
'expire_time' => $expireDateTime,
];

$jsonPayload = json_encode($payload);

if ($jsonPayload === false) {
throw new LogicException('An error was encountered when JSON encoding the device code request response');
}

$responseParams = [
'device_code' => $this->deviceCodeEntity->getIdentifier(),
'device_code' => $this->encrypt($jsonPayload),
'user_code' => $this->deviceCodeEntity->getUserCode(),
'verification_uri' => $this->deviceCodeEntity->getVerificationUri(),
'expires_in' => $expireDateTime - time(),
Expand Down
106 changes: 85 additions & 21 deletions tests/Grant/DeviceCodeGrantTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
use PHPUnit\Framework\TestCase;

use function base64_encode;
use function json_encode;
use function random_bytes;
use function time;
use function uniqid;

class DeviceCodeGrantTest extends TestCase
Expand Down Expand Up @@ -352,31 +354,25 @@ public function testRespondToAccessTokenRequest(): void
$deviceCodeEntity = new DeviceCodeEntity();

$deviceCodeEntity->setUserIdentifier('baz');
$deviceCodeEntity->setIdentifier('deviceCodeEntityIdentifier');
$deviceCodeEntity->setUserCode('123456');
$deviceCodeEntity->setExpiryDateTime(new DateTimeImmutable('+1 hour'));
$deviceCodeEntity->setClient($client);
$deviceCodeEntity->addScope($scope);

$deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')
->with($deviceCodeEntity->getIdentifier())
->with('deviceCodeEntityIdentifier')
->willReturn($deviceCodeEntity);

$accessTokenEntity = new AccessTokenEntity();
$accessTokenEntity->addScope($scope);

$accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
$accessTokenRepositoryMock->method('getNewToken')
->with($client, $deviceCodeEntity->getScopes(), $deviceCodeEntity->getUserIdentifier())
->willReturn($accessTokenEntity);
$accessTokenRepositoryMock->method('getNewToken')->willReturn($accessTokenEntity);
$accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();

$refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
$refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf();
$refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity());

$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->expects(self::never())->method('getScopeEntityByIdentifier');
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope);
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);

$grant = new DeviceCodeGrant(
Expand All @@ -393,11 +389,21 @@ public function testRespondToAccessTokenRequest(): void
$grant->setEncryptionKey($this->cryptStub->getKey());
$grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key'));

$grant->completeDeviceAuthorizationRequest($deviceCodeEntity->getIdentifier(), 'baz', true);
$grant->completeDeviceAuthorizationRequest('deviceCodeEntityIdentifier', 'baz', true);

$serverRequest = (new ServerRequest())->withParsedBody([
'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code',
'device_code' => $deviceCodeEntity->getIdentifier(),
'device_code' => $this->cryptStub->doEncrypt(
json_encode(
[
'device_code_id' => 'deviceCodeEntityIdentifier',
'expire_time' => time() + 3600,
'client_id' => 'foo',
'scopes' => ['foo'],
],
JSON_THROW_ON_ERROR
)
),
'client_id' => 'foo',
]);

Expand Down Expand Up @@ -428,7 +434,17 @@ public function testRespondToRequestMissingClient(): void
$grant->setAccessTokenRepository($accessTokenRepositoryMock);

$serverRequest = (new ServerRequest())->withQueryParams([
'device_code' => uniqid(),
'device_code' => $this->cryptStub->doEncrypt(
json_encode(
[
'device_code_id' => uniqid(),
'expire_time' => time() + 3600,
'client_id' => 'foo',
'scopes' => ['foo'],
],
JSON_THROW_ON_ERROR
)
),
]);

$responseType = new StubResponseType();
Expand All @@ -455,8 +471,9 @@ public function testRespondToRequestMissingDeviceCode(): void
$deviceCodeEntity->setUserIdentifier('baz');
$deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCodeEntity);

$scope = new ScopeEntity();
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->expects(self::never())->method('getScopeEntityByIdentifier');
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope);
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);

$grant = new DeviceCodeGrant(
Expand Down Expand Up @@ -503,8 +520,10 @@ public function testIssueSlowDownError(): void
$deviceCodeEntity->setClient($client);
$deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCodeEntity);

$scope = new ScopeEntity();
$scope->setIdentifier('foo');
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->expects(self::never())->method('getScopeEntityByIdentifier');
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope);
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);

$grant = new DeviceCodeGrant(
Expand All @@ -522,7 +541,17 @@ public function testIssueSlowDownError(): void

$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'device_code' => uniqid(),
'device_code' => $this->cryptStub->doEncrypt(
json_encode(
[
'device_code_id' => uniqid(),
'expire_time' => time() + 3600,
'client_id' => 'foo',
'scopes' => ['foo'],
],
JSON_THROW_ON_ERROR
)
),
]);

$responseType = new StubResponseType();
Expand Down Expand Up @@ -551,8 +580,10 @@ public function testIssueAuthorizationPendingError(): void
$deviceCodeEntity->setClient($client);
$deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCodeEntity);

$scope = new ScopeEntity();
$scope->setIdentifier('foo');
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->expects(self::never())->method('getScopeEntityByIdentifier');
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope);
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);

$grant = new DeviceCodeGrant(
Expand All @@ -570,7 +601,17 @@ public function testIssueAuthorizationPendingError(): void

$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'device_code' => uniqid(),
'device_code' => $this->cryptStub->doEncrypt(
json_encode(
[
'device_code_id' => uniqid(),
'expire_time' => time() + 3600,
'client_id' => 'foo',
'scopes' => ['foo'],
],
JSON_THROW_ON_ERROR
)
),
]);

$responseType = new StubResponseType();
Expand Down Expand Up @@ -599,8 +640,9 @@ public function testIssueExpiredTokenError(): void
$deviceCodeEntity->setClient($client);
$deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCodeEntity);

$scope = new ScopeEntity();
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->expects(self::never())->method('getScopeEntityByIdentifier');
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope);
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);

$grant = new DeviceCodeGrant(
Expand All @@ -618,7 +660,17 @@ public function testIssueExpiredTokenError(): void

$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'device_code' => uniqid(),
'device_code' => $this->cryptStub->doEncrypt(
json_encode(
[
'device_code_id' => uniqid(),
'expire_time' => time() - 3600,
'client_id' => 'foo',
'scopes' => ['foo'],
],
JSON_THROW_ON_ERROR
)
),
]);

$responseType = new StubResponseType();
Expand Down Expand Up @@ -698,8 +750,10 @@ public function testIssueAccessDeniedError(): void
$deviceCode->setUserCode('12345678');
$deviceCodeRepositoryMock->method('getDeviceCodeEntityByDeviceCode')->willReturn($deviceCode);

$scope = new ScopeEntity();
$scope->setIdentifier('foo');
$scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
$scopeRepositoryMock->expects(self::never())->method('getScopeEntityByIdentifier');
$scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope);
$scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);

$grant = new DeviceCodeGrant(
Expand All @@ -719,7 +773,17 @@ public function testIssueAccessDeniedError(): void

$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'device_code' => $deviceCode->getIdentifier(),
'device_code' => $this->cryptStub->doEncrypt(
json_encode(
[
'device_code_id' => uniqid(),
'expire_time' => time() + 3600,
'client_id' => 'foo',
'scopes' => ['foo'],
],
JSON_THROW_ON_ERROR
),
),
]);

$responseType = new StubResponseType();
Expand Down
Loading

0 comments on commit 74f7ed5

Please sign in to comment.