diff --git a/composer.json b/composer.json index 89cabf93..55357050 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,6 @@ "php": ">=5.5.9", "league/event": "~2.1", "zendframework/zend-diactoros": "~1.1", - "namshi/jose": "^6.0", "lcobucci/jwt": "^3.1", "paragonie/random_compat": "^1.1" }, @@ -65,5 +64,8 @@ "branch-alias": { "dev-V5-WIP": "5.0-dev" } + }, + "suggest": { + "league/plates": "Required for parsing authorization code templates" } } diff --git a/examples/composer.json b/examples/composer.json index 0b85ac70..f0bf2564 100644 --- a/examples/composer.json +++ b/examples/composer.json @@ -7,7 +7,8 @@ ], "require": { "slim/slim": "3.0.*", - "league/oauth2-server": "dev-V5-WIP" + "league/oauth2-server": "dev-V5-WIP", + "league/plates": "^3.1" }, "autoload": { "psr-4": { diff --git a/examples/composer.lock b/examples/composer.lock index 251ab498..776fb37a 100644 --- a/examples/composer.lock +++ b/examples/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "ff6f832d21c141662627622e68079ca5", - "content-hash": "f08d5c7c3ede910150d75ad3c56d1b46", + "hash": "143453cc35e7f499b130b6460222dc5a", + "content-hash": "1ea46581fb6db25f323a37a45ef74f95", "packages": [ { "name": "container-interop/container-interop", @@ -145,9 +145,14 @@ { "name": "league/oauth2-server", "version": "dev-V5-WIP", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-server.git", + "reference": "95919a688e29c911d1e4e83112cacd18f719700f" + }, "dist": { "type": "path", - "url": "../", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/95919a688e29c911d1e4e83112cacd18f719700f", "reference": "168e7640c6e8217b7e961151de522810b3edce6e", "shasum": null }, @@ -214,6 +219,58 @@ "server" ] }, + { + "name": "league/plates", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/plates.git", + "reference": "2d8569e9f140a70d6a05db38006926f7547cb802" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/plates/zipball/2d8569e9f140a70d6a05db38006926f7547cb802", + "reference": "2d8569e9f140a70d6a05db38006926f7547cb802", + "shasum": "" + }, + "require-dev": { + "mikey179/vfsstream": "~1.4.0", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~1.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Plates\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Reinink", + "email": "jonathan@reinink.ca", + "role": "Developer" + } + ], + "description": "Plates, the native PHP template system that's fast, easy to use and easy to extend.", + "homepage": "http://platesphp.com", + "keywords": [ + "league", + "package", + "templates", + "templating", + "views" + ], + "time": "2015-07-09 02:14:40" + }, { "name": "namshi/jose", "version": "6.1.0", diff --git a/examples/public/auth_code.php b/examples/public/auth_code.php new file mode 100644 index 00000000..761f7ae3 --- /dev/null +++ b/examples/public/auth_code.php @@ -0,0 +1,83 @@ + function () { + + // Init our repositories + $clientRepository = new ClientRepository(); + $scopeRepository = new ScopeRepository(); + $accessTokenRepository = new AccessTokenRepository(); + $userRepository = new UserRepository(); + $refreshTokenRepository = new RefreshTokenRepository(); + $authCodeRepository = new AuthCodeRepository(); + + $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; + $publicKeyPath = 'file://' . __DIR__ . '/../public.key'; + + // Setup the authorization server + $server = new Server( + $clientRepository, + $accessTokenRepository, + $scopeRepository, + $privateKeyPath, + $publicKeyPath + ); + + // Enable the password grant on the server with a token TTL of 1 hour + $server->enableGrantType( + new AuthCodeGrant( + $authCodeRepository, + $refreshTokenRepository, + $userRepository, + new \DateInterval('PT10M') + ), + new \DateInterval('PT1H') + ); + + return $server; + }, +]); + +$app->any('/authorize', function (Request $request, Response $response) { + /** @var Server $server */ + $server = $this->get(Server::class); + try { + return $server->respondToRequest($request, $response); + } catch (OAuthServerException $e) { + return $e->generateHttpResponse($response); + } catch (\Exception $e) { + return $response->withStatus(500)->write($e->getMessage()); + } +}); + +$app->post('/access_token', function (Request $request, Response $response) { + /** @var Server $server */ + $server = $this->get(Server::class); + try { + return $server->respondToRequest($request, $response); + } catch (OAuthServerException $e) { + return $e->generateHttpResponse($response); + } catch (\Exception $e) { + return $response->withStatus(500)->write($e->getMessage()); + } +}); + +$app->run(); diff --git a/examples/public/middleware_authentication.php b/examples/public/middleware_authentication.php index f9b525dc..d928e19d 100644 --- a/examples/public/middleware_authentication.php +++ b/examples/public/middleware_authentication.php @@ -1,5 +1,7 @@ [ 'secret' => password_hash('abc123', PASSWORD_BCRYPT), 'name' => 'My Awesome App', - 'redirect_uri' => '' + 'redirect_uri' => 'http://foo/bar' ] ]; @@ -30,7 +30,7 @@ class ClientRepository implements ClientRepositoryInterface } // Check if redirect URI is valid - if ($redirectUri !== null && $redirectUri !== $clients[$clientIdentifier]['redirectUri']) { + if ($redirectUri !== null && $redirectUri !== $clients[$clientIdentifier]['redirect_uri']) { return null; } diff --git a/src/Entities/AuthorizationCodeRequestEntity.php b/src/Entities/AuthorizationCodeRequestEntity.php new file mode 100644 index 00000000..bd401b24 --- /dev/null +++ b/src/Entities/AuthorizationCodeRequestEntity.php @@ -0,0 +1,79 @@ +clientId; + } + + /** + * @return null|string + */ + public function getRedirectUri() + { + return $this->redirectUri; + } + + /** + * @return null|string + */ + public function getScope() + { + return $this->scope; + } + + /** + * @return null|string + */ + public function getState() + { + return $this->state; + } + + /** + * AuthorizationCodeRequestEntity constructor. + * + * @param string $clientId + * @param string|null $redirectUri + * @param string|null $scope + * @param string|null $state + */ + public function __construct($clientId, $redirectUri = null, $scope = null, $state = null) + { + $this->clientId = $clientId; + $this->redirectUri = $redirectUri; + $this->scope = $scope; + $this->state = $state; + } + + public function __sleep() + { + return ['clientId', 'redirectUri', 'scope', 'state']; + } +} diff --git a/src/Exception/OAuthServerException.php b/src/Exception/OAuthServerException.php index 35c38dd2..bd386a28 100644 --- a/src/Exception/OAuthServerException.php +++ b/src/Exception/OAuthServerException.php @@ -197,13 +197,20 @@ class OAuthServerException extends \Exception /** * Access denied * - * @param null|string $hint + * @param string|null $hint + * @param string|null $redirectUri * * @return static */ - public static function accessDenied($hint = null) + public static function accessDenied($hint = null, $redirectUri = null) { - return new static('The server denied the request.', 'access_denied', 401, $hint); + return new static( + 'The resource owner or authorization server denied the request.', + 'access_denied', + 401, + $hint, + $redirectUri + ); } /** diff --git a/src/Grant/AbstractGrant.php b/src/Grant/AbstractGrant.php index 9d22418b..3ccf3ad2 100644 --- a/src/Grant/AbstractGrant.php +++ b/src/Grant/AbstractGrant.php @@ -15,6 +15,7 @@ use League\Event\EmitterAwareTrait; use League\Event\EmitterInterface; use League\Event\Event; use League\OAuth2\Server\Entities\AccessTokenEntity; +use League\OAuth2\Server\Entities\AuthCodeEntity; use League\OAuth2\Server\Entities\Interfaces\ClientEntityInterface; use League\OAuth2\Server\Entities\RefreshTokenEntity; use League\OAuth2\Server\Entities\ScopeEntity; @@ -34,13 +35,6 @@ abstract class AbstractGrant implements GrantTypeInterface const SCOPE_DELIMITER_STRING = ' '; - /** - * Grant identifier - * - * @var string - */ - protected $identifier = ''; - /** * Grant responds with * @@ -139,14 +133,6 @@ abstract class AbstractGrant implements GrantTypeInterface $this->refreshTokenTTL = $refreshTokenTTL; } - /** - * {@inheritdoc} - */ - public function getIdentifier() - { - return $this->identifier; - } - /** * {@inheritdoc} */ @@ -156,14 +142,20 @@ abstract class AbstractGrant implements GrantTypeInterface } /** + * Validate the client + * * @param \Psr\Http\Message\ServerRequestInterface $request + * @param bool $validateSecret + * @param bool $validateRedirectUri * * @return \League\OAuth2\Server\Entities\Interfaces\ClientEntityInterface - * * @throws \League\OAuth2\Server\Exception\OAuthServerException */ - protected function validateClient(ServerRequestInterface $request) - { + protected function validateClient( + ServerRequestInterface $request, + $validateSecret = true, + $validateRedirectUri = false + ) { $clientId = $this->getRequestParameter( 'client_id', $request, @@ -178,15 +170,20 @@ abstract class AbstractGrant implements GrantTypeInterface $request, $this->getServerParameter('PHP_AUTH_PW', $request) ); - if (is_null($clientSecret)) { + if (is_null($clientSecret) && $validateSecret === true) { throw OAuthServerException::invalidRequest('client_secret', null, '`%s` parameter is missing'); } + $redirectUri = $this->getRequestParameter('redirect_uri', $request, null); + if (is_null($redirectUri) && $validateRedirectUri === true) { + throw OAuthServerException::invalidRequest('redirect_uri', null, '`%s` parameter is missing'); + } + $client = $this->clientRepository->getClientEntity( - $this->getIdentifier(), $clientId, $clientSecret, - null + $redirectUri, + $this->getIdentifier() ); if (!$client instanceof ClientEntityInterface) { @@ -199,6 +196,8 @@ abstract class AbstractGrant implements GrantTypeInterface } /** + * Validate scopes in the request + * * @param \Psr\Http\Message\ServerRequestInterface $request * @param \League\OAuth2\Server\Entities\Interfaces\ClientEntityInterface $client * @param string $redirectUri @@ -249,9 +248,36 @@ abstract class AbstractGrant implements GrantTypeInterface */ protected function getRequestParameter($parameter, ServerRequestInterface $request, $default = null) { - return (is_array($request->getParsedBody()) && isset($request->getParsedBody()[$parameter])) - ? $request->getParsedBody()[$parameter] - : $default; + $requestParameters = (array) $request->getParsedBody(); + return isset($requestParameters[$parameter]) ? $requestParameters[$parameter] : $default; + } + + /** + * Retrieve query string parameter. + * + * @param string $parameter + * @param \Psr\Http\Message\ServerRequestInterface $request + * @param mixed $default + * + * @return null|string + */ + protected function getQueryStringParameter($parameter, ServerRequestInterface $request, $default = null) + { + return isset($request->getQueryParams()[$parameter]) ? $request->getQueryParams()[$parameter] : $default; + } + + /** + * Retrieve cookie parameter. + * + * @param string $parameter + * @param \Psr\Http\Message\ServerRequestInterface $request + * @param mixed $default + * + * @return null|string + */ + protected function getCookieParameter($parameter, ServerRequestInterface $request, $default = null) + { + return isset($request->getCookieParams()[$parameter]) ? $request->getCookieParams()[$parameter] : $default; } /** @@ -265,10 +291,12 @@ abstract class AbstractGrant implements GrantTypeInterface */ protected function getServerParameter($parameter, ServerRequestInterface $request, $default = null) { - return (isset($request->getServerParams()[$parameter])) ? $request->getServerParams()[$parameter] : $default; + return isset($request->getServerParams()[$parameter]) ? $request->getServerParams()[$parameter] : $default; } /** + * Issue an access token + * * @param \DateInterval $tokenTTL * @param \League\OAuth2\Server\Entities\Interfaces\ClientEntityInterface $client * @param string $userIdentifier @@ -295,6 +323,39 @@ abstract class AbstractGrant implements GrantTypeInterface return $accessToken; } + /** + * Issue an auth code + * + * @param \DateInterval $tokenTTL + * @param \League\OAuth2\Server\Entities\Interfaces\ClientEntityInterface $client + * @param string $userIdentifier + * @param string $redirectUri + * @param array $scopes + * + * @return \League\OAuth2\Server\Entities\AuthCodeEntity + * @throws \League\OAuth2\Server\Exception\OAuthServerException + */ + protected function issueAuthCode( + \DateInterval $tokenTTL, + ClientEntityInterface $client, + $userIdentifier, + $redirectUri, + array $scopes = [] + ) { + $authCode = new AuthCodeEntity(); + $authCode->setIdentifier(SecureKey::generate()); + $authCode->setExpiryDateTime((new \DateTime())->add($tokenTTL)); + $authCode->setClient($client); + $authCode->setUserIdentifier($userIdentifier); + $authCode->setRedirectUri($redirectUri); + + foreach ($scopes as $scope) { + $authCode->addScope($scope); + } + + return $authCode; + } + /** * @param \League\OAuth2\Server\Entities\AccessTokenEntity $accessToken * @@ -315,10 +376,11 @@ abstract class AbstractGrant implements GrantTypeInterface */ public function canRespondToRequest(ServerRequestInterface $request) { + $requestParameters = (array) $request->getParsedBody(); + return ( - is_array($request->getParsedBody()) - && isset($request->getParsedBody()['grant_type']) - && $request->getParsedBody()['grant_type'] === $this->identifier + array_key_exists('grant_type', $requestParameters) + && $requestParameters['grant_type'] === $this->getIdentifier() ); } } diff --git a/src/Grant/AuthCodeGrant.php b/src/Grant/AuthCodeGrant.php index a7ae36bb..560b8ad1 100644 --- a/src/Grant/AuthCodeGrant.php +++ b/src/Grant/AuthCodeGrant.php @@ -1,303 +1,357 @@ - * @copyright Copyright (c) Alex Bilbie - * @license http://mit-license.org/ - * @link https://github.com/thephpleague/oauth2-server - */ namespace League\OAuth2\Server\Grant; -use League\Event\Emitter; -use League\Event\Event; -use League\OAuth2\Server\Entities\AccessTokenEntity; -use League\OAuth2\Server\Entities\Interfaces\AuthCodeEntityInterface; -use League\OAuth2\Server\Entities\Interfaces\ClientEntityInterface; -use League\OAuth2\Server\Exception\InvalidClientException; -use League\OAuth2\Server\Exception\InvalidRequestException; -use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; -use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; -use League\OAuth2\Server\Repositories\ClientRepositoryInterface; -use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; -use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; -use League\OAuth2\Server\TokenTypes\TokenTypeInterface; -use League\OAuth2\Server\Utils\SecureKey; -use Symfony\Component\HttpFoundation\Request; use DateInterval; +use League\OAuth2\Server\Entities\Interfaces\ClientEntityInterface; +use League\OAuth2\Server\Entities\Interfaces\UserEntityInterface; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; +use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; +use League\OAuth2\Server\Repositories\UserRepositoryInterface; +use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; +use League\OAuth2\Server\Utils\KeyCrypt; +use League\Plates\Engine; +use League\Event\Event; +use Psr\Http\Message\ServerRequestInterface; +use Zend\Diactoros\Response; +use Zend\Diactoros\Uri; -/** - * Auth code grant class - */ class AuthCodeGrant extends AbstractGrant { + /** + * @var \DateInterval + */ + private $authCodeTTL; /** * @var \League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface */ - protected $authCodeRepository; + private $authCodeRepository; + + /** + * @var \League\OAuth2\Server\Repositories\UserRepositoryInterface + */ + private $userRepository; + + /** + * @var null|string + */ + private $pathToLoginTemplate; + + /** + * @var null|string + */ + private $pathToAuthorizeTemplate; + + /** + * @var \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface + */ + private $refreshTokenRepository; /** - * @param \League\Event\Emitter $emitter - * @param \League\OAuth2\Server\Repositories\ClientRepositoryInterface $clientRepository - * @param \League\OAuth2\Server\Repositories\ScopeRepositoryInterface $scopeRepository - * @param \League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface $accessTokenRepository * @param \League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface $authCodeRepository * @param \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface $refreshTokenRepository + * @param \League\OAuth2\Server\Repositories\UserRepositoryInterface $userRepository + * @param \DateInterval $authCodeTTL + * @param string|null $pathToLoginTemplate + * @param string|null $pathToAuthorizeTemplate */ public function __construct( - Emitter $emitter, - ClientRepositoryInterface $clientRepository, - ScopeRepositoryInterface $scopeRepository, - AccessTokenRepositoryInterface $accessTokenRepository, AuthCodeRepositoryInterface $authCodeRepository, - RefreshTokenRepositoryInterface $refreshTokenRepository = null + RefreshTokenRepositoryInterface $refreshTokenRepository, + UserRepositoryInterface $userRepository, + \DateInterval $authCodeTTL, + $pathToLoginTemplate = null, + $pathToAuthorizeTemplate = null ) { $this->authCodeRepository = $authCodeRepository; $this->refreshTokenRepository = $refreshTokenRepository; - parent::__construct($emitter, $clientRepository, $scopeRepository, $accessTokenRepository); + $this->userRepository = $userRepository; + $this->authCodeTTL = $authCodeTTL; + $this->pathToLoginTemplate = ($pathToLoginTemplate === null) + ? __DIR__ . '/../ResponseTypes/DefaultTemplates/login_user.php' + : $this->pathToLoginTemplate; + $this->pathToAuthorizeTemplate = ($pathToLoginTemplate === null) + ? __DIR__ . '/../ResponseTypes/DefaultTemplates/authorize_client.php' + : $this->pathToAuthorizeTemplate; + $this->refreshTokenTTL = new \DateInterval('P1M'); } - /** - * Grant identifier - * - * @var string - */ - protected $identifier = 'authorization_code'; /** - * Response type + * Respond to an authorization request * - * @var string + * @param \Psr\Http\Message\ServerRequestInterface $request + * + * @return \Psr\Http\Message\ResponseInterface + * @throws \League\OAuth2\Server\Exception\OAuthServerException */ - protected $responseType = 'code'; - - /** - * AuthServer instance - * - * @var \League\OAuth2\Server\AuthorizationServer - */ - protected $server = null; - - /** - * Access token expires in override - * - * @var int - */ - protected $accessTokenTTL = null; - - /** - * The TTL of the auth token - * - * @var integer - */ - protected $authTokenTTL = 600; - - /** - * Override the default access token expire time - * - * @param int $authTokenTTL - * - * @return void - */ - public function setAuthTokenTTL($authTokenTTL) - { - $this->authTokenTTL = $authTokenTTL; - } - - /** - * Check authorize parameters - * - * @return array Authorize request parameters - * - * @throws - */ - /*public function checkAuthorizeParams() - { - // Get required params - $clientId = $request->query->get('client_id', null); + protected function respondToAuthorizationRequest( + ServerRequestInterface $request + ) { + $clientId = $this->getQueryStringParameter( + 'client_id', + $request, + $this->getServerParameter('PHP_AUTH_USER', $request) + ); if (is_null($clientId)) { - throw new InvalidRequestException('client_id'); + throw OAuthServerException::invalidRequest('client_id', null, '`%s` parameter is missing'); } - $redirectUri = $request->query->get('redirect_uri', null); + $redirectUri = $this->getQueryStringParameter('redirect_uri', $request, null); if (is_null($redirectUri)) { - throw new InvalidRequestException('redirect_uri'); + throw OAuthServerException::invalidRequest('redirect_uri', null, '`%s` parameter is missing'); } - // Validate client ID and redirect URI - $client = $this->server->getClientStorage()->get( + $client = $this->clientRepository->getClientEntity( $clientId, null, $redirectUri, $this->getIdentifier() ); - if (($client instanceof ClientEntity) === false) { - $this->server->getEventEmitter()->emit(new Event\ClientAuthenticationFailedEvent($request)); - throw new Exception\InvalidClientException(); + if ($client instanceof ClientEntityInterface === false) { + $this->emitter->emit(new Event('client.authentication.failed', $request)); + + throw OAuthServerException::invalidClient(); } - $state = $request->query->get('state', null); - if ($this->server->stateParamRequired() === true && is_null($state)) { - throw new InvalidRequestException('state', $redirectUri); - } - - $responseType = $request->query->get('response_type', null); - if (is_null($responseType)) { - throw new InvalidRequestException('response_type', $redirectUri); - } - - // Ensure response type is one that is recognised - if (!in_array($responseType, $this->server->getResponseTypes())) { - throw new Exception\UnsupportedResponseTypeException($responseType, $redirectUri); - } - - // Validate any scopes that are in the request - $scopeParam = $request->query->get('scope', ''); - $scopes = $this->validateScopes($scopeParam, $client, $redirectUri); - - return [ - 'client' => $client, - 'redirect_uri' => $redirectUri, - 'state' => $state, - 'response_type' => $responseType, - 'scopes' => $scopes - ]; - }*/ - - /** - * Parse a new authorize request - * - * @param string $type The session owner's type - * @param string $typeId The session owner's ID - * @param array $authParams The authorize request $_GET parameters - * - * @return string An authorisation code - */ - /*public function newAuthorizeRequest($type, $typeId, $authParams = []) - { - // Create a new session - $session = new SessionEntity($this->server); - $session->setOwner($type, $typeId); - $session->associateClient($authParams['client']); - $session->save(); - - // Create a new auth code - $authCode = new AuthCodeEntity($this->server); - $authCode->setId(SecureKey::generate()); - $authCode->setRedirectUri($authParams['redirect_uri']); - $authCode->setExpireTime(time() + $this->authTokenTTL); - - foreach ($authParams['scopes'] as $scope) { - $authCode->associateScope($scope); - } - - $authCode->setSession($session); - $authCode->save(); - - return $authCode->generateRedirectUri($authParams['state']); - }*/ - - /** - * Return an access token - * - * @param \Symfony\Component\HttpFoundation\Request $request - * @param \League\OAuth2\Server\TokenTypes\TokenTypeInterface $tokenType - * @param \DateInterval $accessTokenTTL - * @param string $scopeDelimiter - * - * @return \League\OAuth2\Server\TokenTypes\TokenTypeInterface - * @throws \League\OAuth2\Server\Exception\InvalidClientException - * @throws \League\OAuth2\Server\Exception\InvalidGrantException - * @throws \League\OAuth2\Server\Exception\InvalidRequestException - */ - public function getAccessTokenAsType( - Request $request, - TokenTypeInterface $tokenType, - DateInterval $accessTokenTTL, - $scopeDelimiter = ' ' - ) { - // Get the required params - $clientId = $request->request->get('client_id', $request->getUser()); - if (is_null($clientId)) { - throw new InvalidRequestException('client_id', ''); - } - - $clientSecret = $request->request->get('client_secret', - $request->getPassword()); - if (is_null($clientSecret)) { - throw new InvalidRequestException('client_secret'); - } - - $redirectUri = $request->request->get('redirect_uri', null); - if (is_null($redirectUri)) { - throw new InvalidRequestException('redirect_uri'); - } - - // Validate client ID and client secret - $client = $this->clientRepository->get( - $clientId, - $clientSecret, - $redirectUri, - $this->getIdentifier() + $scopes = $this->validateScopes($request, $client, $redirectUri); + $queryString = http_build_query($request->getQueryParams()); + $postbackUri = new Uri( + sprintf( + '//%s%s', + $request->getServerParams()['HTTP_HOST'], + $request->getServerParams()['REQUEST_URI'] + ) ); - if (($client instanceof ClientEntityInterface) === false) { - $this->emitter->emit(new Event('client.authentication.failed', $request)); - throw new InvalidClientException(); + $userId = null; + $userHasApprovedClient = null; + if ($this->getRequestParameter('action', $request, null) !== null) { + $userHasApprovedClient = ($this->getRequestParameter('action', $request) === 'approve'); } - // Validate the auth code - $authCode = $request->request->get('code', null); - if (is_null($authCode)) { - throw new InvalidRequestException('code'); + // Check if the user has been authenticated + $oauthCookie = $this->getCookieParameter('oauth_authorize_request', $request, null); + if ($oauthCookie !== null) { + try { + $oauthCookiePayload = json_decode(KeyCrypt::decrypt($oauthCookie, $this->pathToPublicKey)); + if (is_object($oauthCookiePayload)) { + $userId = $oauthCookiePayload->user_id; + } + } catch (\LogicException $e) { + throw OAuthServerException::serverError($e->getMessage()); + } } - $code = $this->authCodeRepository->get($authCode); - if (($code instanceof AuthCodeEntityInterface) === false) { - throw new InvalidRequestException('code'); + // The username + password might be available in $_POST + $usernameParameter = $this->getRequestParameter('username', $request, null); + $passwordParameter = $this->getRequestParameter('password', $request, null); + + $loginError = null; + + // Assert if the user has logged in already + if ($userId === null && $usernameParameter !== null && $passwordParameter !== null) { + $userEntity = $this->userRepository->getUserEntityByUserCredentials( + $usernameParameter, + $passwordParameter + ); + + if ($userEntity instanceof UserEntityInterface) { + $userId = $userEntity->getIdentifier(); + } else { + $loginError = 'Incorrect username or password'; + } } - // Ensure the auth code hasn't expired - if ($code->isExpired() === true) { - throw new InvalidRequestException('code'); + // The user hasn't logged in yet so show a login form + if ($userId === null) { + $engine = new Engine(dirname($this->pathToLoginTemplate)); + $html = $engine->render( + 'login_user', + [ + 'error' => $loginError, + 'postback_uri' => (string) $postbackUri->withQuery($queryString), + ] + ); + + return new Response\HtmlResponse($html); } - // Check redirect URI presented matches redirect URI originally used in authorize request - if ($code->getRedirectUri() !== $redirectUri) { - throw new InvalidRequestException('redirect_uri'); + + // The user hasn't approved the client yet so show an authorize form + if ($userId !== null && $userHasApprovedClient === null) { + $engine = new Engine(dirname($this->pathToAuthorizeTemplate)); + $html = $engine->render( + 'authorize_client', + [ + 'client' => $client, + 'scopes' => $scopes, + 'postback_uri' => (string) $postbackUri->withQuery($queryString), + ] + ); + + return new Response\HtmlResponse( + $html, + 200, + [ + 'Set-Cookie' => sprintf( + 'oauth_authorize_request=%s; Expires=%s', + urlencode(KeyCrypt::encrypt( + json_encode([ + 'user_id' => $userId, + ]), + $this->pathToPrivateKey + )), + (new \DateTime())->add(new \DateInterval('PT5M'))->format('D, d M Y H:i:s e') + ), + ] + ); } - // Generate the access token - $accessToken = new AccessTokenEntity($this->server); - $accessToken->setIdentifier(SecureKey::generate()); - $expirationDateTime = (new \DateTime())->add($accessTokenTTL); - $accessToken->setExpiryDateTime($expirationDateTime); - $accessToken->setClient($client); + $stateParameter = $this->getQueryStringParameter('state', $request); - foreach ($code->getScopes() as $scope) { - $accessToken->addScope($scope); + $redirectUri = new Uri($redirectUri); + parse_str($redirectUri->getQuery(), $redirectPayload); + if ($stateParameter !== null) { + $redirectPayload['state'] = $stateParameter; } - $tokenType->setAccessToken($accessToken); + if ($userHasApprovedClient === true) { + $authCode = $this->issueAuthCode( + $this->authCodeTTL, + $client, + $userId, + $redirectUri, + $scopes + ); + $this->authCodeRepository->persistNewAuthCode($authCode); - // Associate a refresh token if set - if ($this->refreshTokenRepository instanceof RefreshTokenRepositoryInterface) { -// $refreshToken = new RefreshTokenEntity($this->server); -// $refreshToken->setId(SecureKey::generate()); -// $refreshToken->setExpireTime($this->server->getGrantType('refresh_token')->getRefreshTokenTTL() + time()); -// $tokenType->setParam('refresh_token', $refreshToken->getId()); -// $refreshToken->setAccessToken($accessToken); + $redirectPayload['code'] = KeyCrypt::encrypt( + json_encode( + [ + 'client_id' => $authCode->getClient()->getIdentifier(), + 'auth_code_id' => $authCode->getIdentifier(), + 'scopes' => $authCode->getScopes(), + 'user_id' => $authCode->getUserIdentifier(), + 'expire_time' => (new \DateTime())->add($this->authCodeTTL)->format('U'), + ] + ), + $this->pathToPrivateKey + ); + + return new Response\RedirectResponse($redirectUri->withQuery(http_build_query($redirectPayload))); } - // Expire the auth code - $this->authCodeRepository->delete($code); + $exception = OAuthServerException::accessDenied('The user denied the request', (string) $redirectUri); + return $exception->generateHttpResponse(); + } - // Save the access token - $this->accessTokenRepository->create($accessToken); + /** + * Respond to an access token request + * + * @param \Psr\Http\Message\ServerRequestInterface $request + * @param \League\OAuth2\Server\ResponseTypes\ResponseTypeInterface $responseType + * @param \DateInterval $accessTokenTTL + * + * @return \League\OAuth2\Server\ResponseTypes\ResponseTypeInterface + * @throws \League\OAuth2\Server\Exception\OAuthServerException + */ + protected function respondToAccessTokenRequest( + ServerRequestInterface $request, + ResponseTypeInterface $responseType, + DateInterval $accessTokenTTL + ) { + // Validate request + $client = $this->validateClient($request); + $encryptedAuthCode = $this->getRequestParameter('code', $request, null); - return $tokenType; + if ($encryptedAuthCode === null) { + throw OAuthServerException::invalidRequest('code'); + } + + // Validate the authorization code + try { + $authCodePayload = json_decode(KeyCrypt::decrypt($encryptedAuthCode, $this->pathToPublicKey)); + if (time() > $authCodePayload->expire_time) { + throw OAuthServerException::invalidRequest('code', 'Authorization code has expired'); + } + + if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) { + throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked'); + } + + if ($authCodePayload->client_id !== $client->getIdentifier()) { + throw OAuthServerException::invalidRequest('code', 'Authorization code was not issued to this client'); + } + } catch (\LogicException $e) { + throw OAuthServerException::invalidRequest('code', null, 'Cannot decrypt the authorization code'); + } + + // Issue and persist access + refresh tokens + $accessToken = $this->issueAccessToken( + $accessTokenTTL, + $client, + $authCodePayload->user_id, + $authCodePayload->scopes + ); + $refreshToken = $this->issueRefreshToken($accessToken); + $this->accessTokenRepository->persistNewAccessToken($accessToken); + $this->refreshTokenRepository->persistNewRefreshToken($refreshToken); + + // Inject tokens into response type + $responseType->setAccessToken($accessToken); + $responseType->setRefreshToken($refreshToken); + + return $responseType; + } + + /** + * @inheritdoc + */ + public function canRespondToRequest(ServerRequestInterface $request) + { + return ( + ( + isset($request->getQueryParams()['response_type']) + && $request->getQueryParams()['response_type'] === 'code' + && isset($request->getQueryParams()['client_id']) + ) || (parent::canRespondToRequest($request)) + ); + } + + /** + * Return the grant identifier that can be used in matching up requests + * + * @return string + */ + public function getIdentifier() + { + return 'authorization_code'; + } + + /** + * @inheritdoc + */ + public function respondToRequest( + ServerRequestInterface $request, + ResponseTypeInterface $responseType, + \DateInterval $accessTokenTTL + ) { + if ( + isset($request->getQueryParams()['response_type']) + && $request->getQueryParams()['response_type'] === 'code' + && isset($request->getQueryParams()['client_id']) + ) { + return $this->respondToAuthorizationRequest($request); + } elseif ( + isset($request->getParsedBody()['grant_type']) + && $request->getParsedBody()['grant_type'] === 'authorization_code' + ) { + return $this->respondToAccessTokenRequest($request, $responseType, $accessTokenTTL); + } else { + throw OAuthServerException::serverError('respondToRequest() should not have been called'); + } } } diff --git a/src/Grant/ClientCredentialsGrant.php b/src/Grant/ClientCredentialsGrant.php index 918586f9..cf6ce268 100644 --- a/src/Grant/ClientCredentialsGrant.php +++ b/src/Grant/ClientCredentialsGrant.php @@ -19,13 +19,6 @@ use Psr\Http\Message\ServerRequestInterface; */ class ClientCredentialsGrant extends AbstractGrant { - /** - * Grant identifier - * - * @var string - */ - protected $identifier = 'client_credentials'; - /** * @inheritdoc */ @@ -47,4 +40,12 @@ class ClientCredentialsGrant extends AbstractGrant return $responseType; } + + /** + * @inheritdoc + */ + public function getIdentifier() + { + return 'client_credentials'; + } } diff --git a/src/Grant/GrantTypeInterface.php b/src/Grant/GrantTypeInterface.php index ece1d752..698639d9 100644 --- a/src/Grant/GrantTypeInterface.php +++ b/src/Grant/GrantTypeInterface.php @@ -31,7 +31,7 @@ interface GrantTypeInterface extends EmitterAwareInterface public function setRefreshTokenTTL(\DateInterval $refreshTokenTTL); /** - * Return the identifier + * Return the grant identifier that can be used in matching up requests * * @return string */ diff --git a/src/Grant/PasswordGrant.php b/src/Grant/PasswordGrant.php index 852dedb2..00ffa149 100644 --- a/src/Grant/PasswordGrant.php +++ b/src/Grant/PasswordGrant.php @@ -24,13 +24,6 @@ use Psr\Http\Message\ServerRequestInterface; */ class PasswordGrant extends AbstractGrant { - /** - * Grant identifier - * - * @var string - */ - protected $identifier = 'password'; - /** * @var \League\OAuth2\Server\Repositories\UserRepositoryInterface */ @@ -109,4 +102,12 @@ class PasswordGrant extends AbstractGrant return $user; } + + /** + * @inheritdoc + */ + public function getIdentifier() + { + return 'password'; + } } diff --git a/src/Grant/RefreshTokenGrant.php b/src/Grant/RefreshTokenGrant.php index d5c7e854..b43792ee 100644 --- a/src/Grant/RefreshTokenGrant.php +++ b/src/Grant/RefreshTokenGrant.php @@ -23,13 +23,6 @@ use Psr\Http\Message\ServerRequestInterface; */ class RefreshTokenGrant extends AbstractGrant { - /** - * Grant identifier - * - * @var string - */ - protected $identifier = 'refresh_token'; - /** * @var \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface */ @@ -133,4 +126,12 @@ class RefreshTokenGrant extends AbstractGrant return $refreshTokenData; } + + /** + * @inheritdoc + */ + public function getIdentifier() + { + return 'refresh_token'; + } } diff --git a/src/Middleware/AuthenticationServerMiddleware.php b/src/Middleware/AuthenticationServerMiddleware.php index 14ac1c32..2dd553e3 100644 --- a/src/Middleware/AuthenticationServerMiddleware.php +++ b/src/Middleware/AuthenticationServerMiddleware.php @@ -6,6 +6,7 @@ use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Server; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Zend\Diactoros\Stream; class AuthenticationServerMiddleware { @@ -38,9 +39,10 @@ class AuthenticationServerMiddleware } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } catch (\Exception $exception) { - $response->getBody()->write($exception->getMessage()); + $body = new Stream('php://temp', 'r+'); + $body->write($exception->getMessage()); - return $response->withStatus(500); + return $response->withStatus(500)->withBody($body); } if (in_array($response->getStatusCode(), [400, 401, 500])) { diff --git a/src/Middleware/ResourceServerMiddleware.php b/src/Middleware/ResourceServerMiddleware.php index 1794cdce..0f0b20ae 100644 --- a/src/Middleware/ResourceServerMiddleware.php +++ b/src/Middleware/ResourceServerMiddleware.php @@ -6,6 +6,7 @@ use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Server; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Zend\Diactoros\Stream; class ResourceServerMiddleware { @@ -34,13 +35,14 @@ class ResourceServerMiddleware public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next) { try { - $request = $this->server->getResponseType()->determineAccessTokenInHeader($request); + $request = $this->server->validateRequest($request); } catch (OAuthServerException $exception) { return $exception->generateHttpResponse($response); } catch (\Exception $exception) { - $response->getBody()->write($exception->getMessage()); + $body = new Stream('php://temp', 'r+'); + $body->write($exception->getMessage()); - return $response->withStatus(500); + return $response->withStatus(500)->withBody($body); } // Pass the request and response on to the next responder in the chain diff --git a/src/Repositories/AuthCodeRepositoryInterface.php b/src/Repositories/AuthCodeRepositoryInterface.php index 481106d3..a6742092 100644 --- a/src/Repositories/AuthCodeRepositoryInterface.php +++ b/src/Repositories/AuthCodeRepositoryInterface.php @@ -19,27 +19,25 @@ use League\OAuth2\Server\Entities\Interfaces\AuthCodeEntityInterface; interface AuthCodeRepositoryInterface extends RepositoryInterface { /** - * Get the auth code + * Persists a new auth code to permanent storage * - * @param string $code - * - * @return \League\OAuth2\Server\Entities\Interfaces\AuthCodeEntityInterface + * @param \League\OAuth2\Server\Entities\Interfaces\AuthCodeEntityInterface $authCodeEntity */ - public function getAuthCodeEntityByCodeString($code); + public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity); /** - * Persist a new authorization code + * Revoke an auth code * - * @param string $code The authorization code string - * @param integer $expireTime Token expire time - * @param string $redirectUri Client redirect uri + * @param string $codeId */ - public function persistNewAuthCode($code, $expireTime, $redirectUri); + public function revokeAuthCode($codeId); /** - * Delete an access token + * Check if the auth code has been revoked * - * @param \League\OAuth2\Server\Entities\Interfaces\AuthCodeEntityInterface $token The access token to delete + * @param string $codeId + * + * @return bool Return true if this code has been revoked */ - public function deleteAuthCodeEntity(AuthCodeEntityInterface $token); + public function isAuthCodeRevoked($codeId); } diff --git a/src/ResponseTypes/BearerTokenResponse.php b/src/ResponseTypes/BearerTokenResponse.php index df30e94e..d1b88218 100644 --- a/src/ResponseTypes/BearerTokenResponse.php +++ b/src/ResponseTypes/BearerTokenResponse.php @@ -65,12 +65,13 @@ class BearerTokenResponse extends AbstractResponseType $responseParams['refresh_token'] = $refreshToken; } - $response + $response = $response ->withStatus(200) ->withHeader('pragma', 'no-cache') ->withHeader('cache-control', 'no-store') - ->withHeader('content-type', 'application/json;charset=UTF-8') - ->getBody()->write(json_encode($responseParams)); + ->withHeader('content-type', 'application/json; charset=UTF-8'); + + $response->getBody()->write(json_encode($responseParams)); return $response; } diff --git a/src/ResponseTypes/DefaultTemplates/authorize_client.php b/src/ResponseTypes/DefaultTemplates/authorize_client.php new file mode 100644 index 00000000..81587174 --- /dev/null +++ b/src/ResponseTypes/DefaultTemplates/authorize_client.php @@ -0,0 +1,35 @@ + + +
+ ++ Do you want to authorize =$this->e($client->getName())?> to access the following data? +
+ +