Skip to content
This repository has been archived by the owner on Jun 8, 2024. It is now read-only.

Commit

Permalink
Merge pull request #3 from Bloemendaal/device-flow-grant-update
Browse files Browse the repository at this point in the history
Device flow grant update
  • Loading branch information
Bloemendaal authored Mar 8, 2021
2 parents 16f8901 + c69349b commit 70420f3
Show file tree
Hide file tree
Showing 27 changed files with 1,706 additions and 280 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@
Out of the box it supports the following grants:

* Authorization code grant
* Implicit grant
* Client credentials grant
* Resource owner password credentials grant
* Device authorization grant
* Implicit grant
* Refresh grant
* Resource owner password credentials grant

The following RFCs are implemented:

* [RFC6749 "OAuth 2.0"](https://tools.ietf.org/html/rfc6749)
* [RFC6750 " The OAuth 2.0 Authorization Framework: Bearer Token Usage"](https://tools.ietf.org/html/rfc6750)
* [RFC7519 "JSON Web Token (JWT)"](https://tools.ietf.org/html/rfc7519)
* [RFC7636 "Proof Key for Code Exchange by OAuth Public Clients"](https://tools.ietf.org/html/rfc7636)
* [RFC8628 "OAuth 2.0 Device Authorization Grant](https://tools.ietf.org/html/rfc8628)

This library was created by Alex Bilbie. Find him on Twitter at [@alexbilbie](https://twitter.com/alexbilbie).

Expand Down
29 changes: 29 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,32 @@ curl -X "POST" "http://localhost:4444/refresh_token.php/access_token" \
--data-urlencode "client_secret=abc123" \
--data-urlencode "refresh_token={{REFRESH_TOKEN}}"
```

## Testing the device authorization grant example

Send the following cURL request. This will return a device code which can be exchanged for an access token.

```
curl -X "POST" "http://localhost:4444/device_code.php/device_authorization" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Accept: 1.0" \
--data-urlencode "client_id=myawesomeapp" \
--data-urlencode "client_secret=abc123" \
--data-urlencode "scope=basic email"
```

We have set up the example so that a user ID is already associated with the device code. In a production application you
would implement an authorization view to allow a user to authorize the device.

Issue the following cURL request to exchange your device code for an access token. Replace `{{DEVICE_CODE}}` with the
device code returned from your first cURL post:

```
curl -X "POST" "http://localhost:4444/device_code.php/access_token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "Accept: 1.0" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
--data-urlencode "device_code={{DEVICE_CODE}}" \
--data-urlencode "client_id=myawesomeapp" \
--data-urlencode "client_secret=abc123"
```
104 changes: 104 additions & 0 deletions examples/public/device_code.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
/**
* @author Andrew Millington <[email protected]>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/

include __DIR__ . '/../vendor/autoload.php';

use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\DeviceCodeGrant;
use OAuth2ServerExamples\Repositories\AccessTokenRepository;
use OAuth2ServerExamples\Repositories\ClientRepository;
use OAuth2ServerExamples\Repositories\DeviceCodeRepository;
use OAuth2ServerExamples\Repositories\RefreshTokenRepository;
use OAuth2ServerExamples\Repositories\ScopeRepository;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App;
use Zend\Diactoros\Stream;

$app = new App([
'settings' => [
'displayErrorDetails' => true,
],
AuthorizationServer::class => function () {
// Init our repositories
$clientRepository = new ClientRepository();
$scopeRepository = new ScopeRepository();
$accessTokenRepository = new AccessTokenRepository();
$refreshTokenRepository = new RefreshTokenRepository();
$deviceCodeRepository = new DeviceCodeRepository();

$privateKeyPath = 'file://' . __DIR__ . '/../private.key';

// Set up the authorization server
$server = new AuthorizationServer(
$clientRepository,
$accessTokenRepository,
$scopeRepository,
$privateKeyPath,
'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen'
);

// Enable the device code grant on the server with a token TTL of 1 hour
$server->enableGrantType(
new DeviceCodeGrant(
$deviceCodeRepository,
$refreshTokenRepository,
new \DateInterval('PT10M'),
5
),
new \DateInterval('PT1H')
);

return $server;
},
]);

$app->post('/device_authorization', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) {
/* @var \League\OAuth2\Server\AuthorizationServer $server */
$server = $app->getContainer()->get(AuthorizationServer::class);

try {
$deviceAuthRequest = $server->validateDeviceAuthorizationRequest($request);

// Once the user has logged in, set the user on the authorization request
//$deviceAuthRequest->setUser();

// Once the user has approved or denied the client, update the status
//$deviceAuthRequest->setAuthorizationApproved(true);

// Return the HTTP redirect response
return $server->completeDeviceAuthorizationRequest($deviceAuthRequest, $response);
} catch (OAuthServerException $exception) {
return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
$body = new Stream('php://temp', 'r+');
$body->write($exception->getMessage());

return $response->withStatus(500)->withBody($body);
}
});

$app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) {
/* @var \League\OAuth2\Server\AuthorizationServer $server */
$server = $app->getContainer()->get(AuthorizationServer::class);

try {
return $server->respondToAccessTokenRequest($request, $response);
} catch (OAuthServerException $exception) {
return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
$body = new Stream('php://temp', 'r+');
$body->write($exception->getMessage());

return $response->withStatus(500)->withBody($body);
}
});

$app->run();
20 changes: 20 additions & 0 deletions examples/src/Entities/DeviceCodeEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
/**
* @author Andrew Millington <[email protected]>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/

namespace OAuth2ServerExamples\Entities;

use League\OAuth2\Server\Entities\DeviceCodeEntityInterface;
use League\OAuth2\Server\Entities\Traits\DeviceCodeTrait;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;

class DeviceCodeEntity implements DeviceCodeEntityInterface
{
use EntityTrait, DeviceCodeTrait, TokenEntityTrait;
}
63 changes: 63 additions & 0 deletions examples/src/Repositories/DeviceCodeRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php
/**
* @author Andrew Millington <[email protected]>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/

namespace OAuth2ServerExamples\Repositories;

use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\DeviceCodeEntityInterface;
use League\OAuth2\Server\Repositories\DeviceCodeRepositoryInterface;
use OAuth2ServerExamples\Entities\DeviceCodeEntity;

class DeviceCodeRepository implements DeviceCodeRepositoryInterface
{
/**
* {@inheritdoc}
*/
public function getNewDeviceCode()
{
return new DeviceCodeEntity();
}

/**
* {@inheritdoc}
*/
public function persistNewDeviceCode(DeviceCodeEntityInterface $deviceCodeEntity)
{
// Some logic to persist a new device code to a database
}

/**
* {@inheritdoc}
*/
public function getDeviceCodeEntityByDeviceCode($deviceCode, $grantType, ClientEntityInterface $clientEntity)
{
$deviceCode = new DeviceCodeEntity();

// The user identifier should be set when the user authenticates on the OAuth server
$deviceCode->setUserIdentifier(1);

return $deviceCode;
}

/**
* {@inheritdoc}
*/
public function revokeDeviceCode($codeId)
{
// Some logic to revoke device code
}

/**
* {@inheritdoc}
*/
public function isDeviceCodeRevoked($codeId)
{
// Some logic to check if a device code has been revoked
}
}
36 changes: 36 additions & 0 deletions src/AuthorizationServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use League\OAuth2\Server\RequestTypes\DeviceAuthorizationRequest;
use League\OAuth2\Server\ResponseTypes\AbstractResponseType;
use League\OAuth2\Server\ResponseTypes\BearerTokenResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
Expand Down Expand Up @@ -176,6 +177,41 @@ public function completeAuthorizationRequest(AuthorizationRequest $authRequest,
->generateHttpResponse($response);
}

/**
* Validate a device authorization request
*
* @param ServerRequestInterface $request
*
* @return DeviceAuthorizationRequest
*
* @throws OAuthServerException
*/
public function validateDeviceAuthorizationRequest(ServerRequestInterface $request)
{
foreach ($this->enabledGrantTypes as $grantType) {
if ($grantType->canRespondToDeviceAuthorizationRequest($request)) {
return $grantType->validateDeviceAuthorizationRequest($request);
}
}

throw OAuthServerException::unsupportedGrantType();
}

/**
* Complete a device authorization request
*
* @param DeviceAuthorizationRequest $deviceRequest
* @param ResponseInterface $response
*
* @return ResponseInterface
*/
public function completeDeviceAuthorizationRequest(DeviceAuthorizationRequest $deviceRequest, ResponseInterface $response)
{
return $this->enabledGrantTypes[$deviceRequest->getGrantTypeId()]
->completeDeviceAuthorizationRequest($deviceRequest)
->generateHttpResponse($response);
}

/**
* Return an access token response.
*
Expand Down
23 changes: 10 additions & 13 deletions src/AuthorizationValidators/BearerTokenValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,9 @@
use DateTimeZone;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Encoding\CannotDecodeContent;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Key\LocalFileReference;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token\InvalidTokenStructure;
use Lcobucci\JWT\Token\UnsupportedHeaderFound;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\ValidAt;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
Expand Down Expand Up @@ -92,21 +89,21 @@ public function validateAuthorization(ServerRequestInterface $request)
}

$header = $request->getHeader('authorization');
$jwt = \trim((string) \preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0]));
$jwt = \trim((string)\preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0]));

try {
// Attempt to parse and validate the JWT
// Attempt to parse the JWT
$token = $this->jwtConfiguration->parser()->parse($jwt);
} catch (\Lcobucci\JWT\Exception $exception) {
throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception);
}

try {
// Attempt to validate the JWT
$constraints = $this->jwtConfiguration->validationConstraints();

try {
$this->jwtConfiguration->validator()->assert($token, ...$constraints);
} catch (RequiredConstraintsViolated $exception) {
throw OAuthServerException::accessDenied('Access token could not be verified');
}
} catch (CannotDecodeContent | InvalidTokenStructure | UnsupportedHeaderFound $exception) {
throw OAuthServerException::accessDenied($exception->getMessage(), null, $exception);
$this->jwtConfiguration->validator()->assert($token, ...$constraints);
} catch (RequiredConstraintsViolated $exception) {
throw OAuthServerException::accessDenied('Access token could not be verified');
}

$claims = $token->claims();
Expand Down
33 changes: 33 additions & 0 deletions src/Entities/DeviceCodeEntityInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
/**
* @author Alex Bilbie <[email protected]>
* @copyright Copyright (c) Alex Bilbie
* @license http://mit-license.org/
*
* @link https://github.com/thephpleague/oauth2-server
*/

namespace League\OAuth2\Server\Entities;

interface DeviceCodeEntityInterface extends TokenInterface
{
/**
* @return string
*/
public function getUserCode();

/**
* @param string $userCode
*/
public function setUserCode($userCode);

/**
* @return string
*/
public function getVerificationUri();

/**
* @param string $verificationUri
*/
public function setVerificationUri($verificationUri);
}
2 changes: 1 addition & 1 deletion src/Entities/Traits/AccessTokenTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ private function convertToJWT()
->issuedAt(new DateTimeImmutable())
->canOnlyBeUsedAfter(new DateTimeImmutable())
->expiresAt($this->getExpiryDateTime())
->relatedTo((string) $this->getUserIdentifier())
->relatedTo((string)$this->getUserIdentifier())
->withClaim('scopes', $this->getScopes())
->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey());
}
Expand Down
Loading

0 comments on commit 70420f3

Please sign in to comment.