diff --git a/src/Entities/Interfaces/ClientEntityInterface.php b/src/Entities/Interfaces/ClientEntityInterface.php index c0d0e812..2761c214 100644 --- a/src/Entities/Interfaces/ClientEntityInterface.php +++ b/src/Entities/Interfaces/ClientEntityInterface.php @@ -11,6 +11,7 @@ interface ClientEntityInterface /** * Set the client's identifier + * * @param $identifier */ public function setIdentifier($identifier); @@ -23,7 +24,42 @@ interface ClientEntityInterface /** * Set the client's name + * * @param string $name */ public function setName($name); + + /** + * @param string $secret + */ + public function setSecret($secret); + + /** + * Validate the secret provided by the client + * + * @param string $submittedSecret + * + * @return boolean + */ + public function validateSecret($submittedSecret); + + /** + * Set the client's redirect uri + * + * @param string $redirectUri + */ + public function setRedirectUri($redirectUri); + + /** + * Returns the registered redirect URI + * + * @return string + */ + public function getRedirectUri(); + + /** + * Returns true if the client is capable of keeping it's secrets secret + * @return boolean + */ + public function canKeepASecret(); } diff --git a/src/Entities/Traits/ClientEntityTrait.php b/src/Entities/Traits/ClientEntityTrait.php index 80e61b93..bac3ccda 100644 --- a/src/Entities/Traits/ClientEntityTrait.php +++ b/src/Entities/Traits/ClientEntityTrait.php @@ -9,8 +9,17 @@ trait ClientEntityTrait protected $name; /** - * Get the client's name - * @return string + * @var string + */ + protected $secret; + + /** + * @var string + */ + protected $redirectUri; + + /** + * @inheritdoc */ public function getName() { @@ -18,11 +27,50 @@ trait ClientEntityTrait } /** - * Set the client's name - * @param string $name + * @inheritdoc */ public function setName($name) { $this->name = $name; } + + /** + * @inheritdoc + */ + public function canKeepASecret() + { + return $this->secret !== null; + } + + /** + * @inheritdoc + */ + public function setSecret($secret) + { + $this->secret = $secret; + } + + /** + * @inheritdoc + */ + public function validateSecret($submittedSecret) + { + return strcmp((string) $submittedSecret, $this->secret) === 0; + } + + /** + * @inheritdoc + */ + public function setRedirectUri($redirectUri) + { + $this->redirectUri = $redirectUri; + } + + /** + * @inheritdoc + */ + public function getRedirectUri() + { + return $this->redirectUri; + } } diff --git a/src/Grant/AbstractGrant.php b/src/Grant/AbstractGrant.php index 4611c788..b2d06179 100644 --- a/src/Grant/AbstractGrant.php +++ b/src/Grant/AbstractGrant.php @@ -21,8 +21,11 @@ use League\OAuth2\Server\Entities\RefreshTokenEntity; use League\OAuth2\Server\Entities\ScopeEntity; use League\OAuth2\Server\Exception\OAuthServerException; 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 OAuth2ServerExamples\Repositories\AuthCodeRepository; use Psr\Http\Message\ServerRequestInterface; /** @@ -54,6 +57,16 @@ abstract class AbstractGrant implements GrantTypeInterface */ protected $scopeRepository; + /** + * @var \League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface + */ + private $authCodeRepository; + + /** + * @var \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface + */ + private $refreshTokenRepository; + /** * @var string */ @@ -93,6 +106,22 @@ abstract class AbstractGrant implements GrantTypeInterface $this->scopeRepository = $scopeRepository; } + /** + * @param \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface $refreshTokenRepository + */ + public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository) + { + $this->refreshTokenRepository = $refreshTokenRepository; + } + + /** + * @param \League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface $authCodeRepository + */ + public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository) + { + $this->authCodeRepository = $authCodeRepository; + } + /** * @param string $pathToPrivateKey */ @@ -125,21 +154,32 @@ abstract class AbstractGrant implements GrantTypeInterface $this->refreshTokenTTL = $refreshTokenTTL; } + /** + * @return AuthCodeRepositoryInterface + */ + protected function getAuthCodeRepository() + { + return $this->authCodeRepository; + } + + /** + * @return RefreshTokenRepositoryInterface + */ + protected function getRefreshTokenRepository() + { + return $this->refreshTokenRepository; + } + /** * 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, - $validateSecret = true, - $validateRedirectUri = false - ) { + protected function validateClient(ServerRequestInterface $request) + { $clientId = $this->getRequestParameter( 'client_id', $request, @@ -149,30 +189,34 @@ abstract class AbstractGrant implements GrantTypeInterface throw OAuthServerException::invalidRequest('client_id', null, '`%s` parameter is missing'); } + $client = $this->clientRepository->getClientEntity( + $clientId, + $this->getIdentifier() + ); + + if (!$client instanceof ClientEntityInterface) { + throw OAuthServerException::invalidClient(); + } + + // If the client is confidential require the client secret $clientSecret = $this->getRequestParameter( 'client_secret', $request, $this->getServerParameter('PHP_AUTH_PW', $request) ); - if (is_null($clientSecret) && $validateSecret === true) { + + if ($client->canKeepASecret() && is_null($clientSecret)) { 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'); + if ($client->canKeepASecret() && $client->validateSecret($clientSecret) === false) { + $this->getEmitter()->emit(new Event('client.authentication.failed', $request)); + throw OAuthServerException::invalidClient(); } - $client = $this->clientRepository->getClientEntity( - $clientId, - $clientSecret, - $redirectUri, - $this->getIdentifier() - ); - - if (!$client instanceof ClientEntityInterface) { - $this->getEmitter()->emit(new Event('client.authentication.failed', $request)); - + // If a redirect URI is provided ensure it matches what is pre-registered + $redirectUri = $this->getRequestParameter('redirect_uri', $request, null); + if ($redirectUri !== null && (strcmp($client->getRedirectUri(), $redirectUri) !== 0)) { throw OAuthServerException::invalidClient(); } @@ -303,6 +347,8 @@ abstract class AbstractGrant implements GrantTypeInterface $accessToken->addScope($scope); } + $this->accessTokenRepository->persistNewAccessToken($accessToken); + return $accessToken; } @@ -336,6 +382,8 @@ abstract class AbstractGrant implements GrantTypeInterface $authCode->addScope($scope); } + $this->authCodeRepository->persistNewAuthCode($authCode); + return $authCode; } @@ -351,6 +399,8 @@ abstract class AbstractGrant implements GrantTypeInterface $refreshToken->setExpiryDateTime((new \DateTime())->add($this->refreshTokenTTL)); $refreshToken->setAccessToken($accessToken); + $this->refreshTokenRepository->persistNewRefreshToken($refreshToken); + return $refreshToken; } diff --git a/src/Grant/AuthCodeGrant.php b/src/Grant/AuthCodeGrant.php index 560b8ad1..13b8a984 100644 --- a/src/Grant/AuthCodeGrant.php +++ b/src/Grant/AuthCodeGrant.php @@ -23,10 +23,6 @@ class AuthCodeGrant extends AbstractGrant * @var \DateInterval */ private $authCodeTTL; - /** - * @var \League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface - */ - private $authCodeRepository; /** * @var \League\OAuth2\Server\Repositories\UserRepositoryInterface @@ -43,10 +39,6 @@ class AuthCodeGrant extends AbstractGrant */ private $pathToAuthorizeTemplate; - /** - * @var \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface - */ - private $refreshTokenRepository; /** * @param \League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface $authCodeRepository @@ -64,8 +56,8 @@ class AuthCodeGrant extends AbstractGrant $pathToLoginTemplate = null, $pathToAuthorizeTemplate = null ) { - $this->authCodeRepository = $authCodeRepository; - $this->refreshTokenRepository = $refreshTokenRepository; + $this->setAuthCodeRepository($authCodeRepository); + $this->setRefreshTokenRepository($refreshTokenRepository); $this->userRepository = $userRepository; $this->authCodeTTL = $authCodeTTL; $this->pathToLoginTemplate = ($pathToLoginTemplate === null) @@ -89,26 +81,7 @@ class AuthCodeGrant extends AbstractGrant protected function respondToAuthorizationRequest( ServerRequestInterface $request ) { - $clientId = $this->getQueryStringParameter( - 'client_id', - $request, - $this->getServerParameter('PHP_AUTH_USER', $request) - ); - if (is_null($clientId)) { - throw OAuthServerException::invalidRequest('client_id', null, '`%s` parameter is missing'); - } - - $redirectUri = $this->getQueryStringParameter('redirect_uri', $request, null); - if (is_null($redirectUri)) { - throw OAuthServerException::invalidRequest('redirect_uri', null, '`%s` parameter is missing'); - } - - $client = $this->clientRepository->getClientEntity( - $clientId, - null, - $redirectUri, - $this->getIdentifier() - ); + $client = $this->validateClient($request); if ($client instanceof ClientEntityInterface === false) { $this->emitter->emit(new Event('client.authentication.failed', $request)); @@ -116,7 +89,7 @@ class AuthCodeGrant extends AbstractGrant throw OAuthServerException::invalidClient(); } - $scopes = $this->validateScopes($request, $client, $redirectUri); + $scopes = $this->validateScopes($request, $client, $client->getRedirectUri()); $queryString = http_build_query($request->getQueryParams()); $postbackUri = new Uri( sprintf( @@ -168,8 +141,9 @@ class AuthCodeGrant extends AbstractGrant // The user hasn't logged in yet so show a login form if ($userId === null) { $engine = new Engine(dirname($this->pathToLoginTemplate)); + $pathParts = explode(DIRECTORY_SEPARATOR, $this->pathToLoginTemplate); $html = $engine->render( - 'login_user', + end($pathParts), [ 'error' => $loginError, 'postback_uri' => (string) $postbackUri->withQuery($queryString), @@ -183,8 +157,9 @@ class AuthCodeGrant extends AbstractGrant // The user hasn't approved the client yet so show an authorize form if ($userId !== null && $userHasApprovedClient === null) { $engine = new Engine(dirname($this->pathToAuthorizeTemplate)); + $pathParts = explode(DIRECTORY_SEPARATOR, $this->pathToAuthorizeTemplate); $html = $engine->render( - 'authorize_client', + end($pathParts), [ 'client' => $client, 'scopes' => $scopes, @@ -212,7 +187,7 @@ class AuthCodeGrant extends AbstractGrant $stateParameter = $this->getQueryStringParameter('state', $request); - $redirectUri = new Uri($redirectUri); + $redirectUri = new Uri($client->getRedirectUri()); parse_str($redirectUri->getQuery(), $redirectPayload); if ($stateParameter !== null) { $redirectPayload['state'] = $stateParameter; @@ -226,7 +201,6 @@ class AuthCodeGrant extends AbstractGrant $redirectUri, $scopes ); - $this->authCodeRepository->persistNewAuthCode($authCode); $redirectPayload['code'] = KeyCrypt::encrypt( json_encode( @@ -263,6 +237,12 @@ class AuthCodeGrant extends AbstractGrant ResponseTypeInterface $responseType, DateInterval $accessTokenTTL ) { + // The redirect URI is required in this request + $redirectUri = $this->getQueryStringParameter('redirect_uri', $request, null); + if (is_null($redirectUri)) { + throw OAuthServerException::invalidRequest('redirect_uri', null, '`%s` parameter is missing'); + } + // Validate request $client = $this->validateClient($request); $encryptedAuthCode = $this->getRequestParameter('code', $request, null); @@ -278,7 +258,7 @@ class AuthCodeGrant extends AbstractGrant throw OAuthServerException::invalidRequest('code', 'Authorization code has expired'); } - if ($this->authCodeRepository->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) { + if ($this->getAuthCodeRepository()->isAuthCodeRevoked($authCodePayload->auth_code_id) === true) { throw OAuthServerException::invalidRequest('code', 'Authorization code has been revoked'); } @@ -297,8 +277,6 @@ class AuthCodeGrant extends AbstractGrant $authCodePayload->scopes ); $refreshToken = $this->issueRefreshToken($accessToken); - $this->accessTokenRepository->persistNewAccessToken($accessToken); - $this->refreshTokenRepository->persistNewRefreshToken($refreshToken); // Inject tokens into response type $responseType->setAccessToken($accessToken); diff --git a/src/Grant/ClientCredentialsGrant.php b/src/Grant/ClientCredentialsGrant.php index cf6ce268..f5881e12 100644 --- a/src/Grant/ClientCredentialsGrant.php +++ b/src/Grant/ClientCredentialsGrant.php @@ -33,7 +33,6 @@ class ClientCredentialsGrant extends AbstractGrant // Issue and persist access token $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $client->getIdentifier(), $scopes); - $this->accessTokenRepository->persistNewAccessToken($accessToken); // Inject access token into response type $responseType->setAccessToken($accessToken); diff --git a/src/Grant/PasswordGrant.php b/src/Grant/PasswordGrant.php index 00ffa149..0b30bfe2 100644 --- a/src/Grant/PasswordGrant.php +++ b/src/Grant/PasswordGrant.php @@ -29,11 +29,6 @@ class PasswordGrant extends AbstractGrant */ private $userRepository; - /** - * @var \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface - */ - private $refreshTokenRepository; - /** * @param \League\OAuth2\Server\Repositories\UserRepositoryInterface $userRepository * @param \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface $refreshTokenRepository @@ -43,7 +38,7 @@ class PasswordGrant extends AbstractGrant RefreshTokenRepositoryInterface $refreshTokenRepository ) { $this->userRepository = $userRepository; - $this->refreshTokenRepository = $refreshTokenRepository; + $this->setRefreshTokenRepository($refreshTokenRepository); $this->refreshTokenTTL = new \DateInterval('P1M'); } @@ -64,8 +59,6 @@ class PasswordGrant extends AbstractGrant // Issue and persist new tokens $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $scopes); $refreshToken = $this->issueRefreshToken($accessToken); - $this->accessTokenRepository->persistNewAccessToken($accessToken); - $this->refreshTokenRepository->persistNewRefreshToken($refreshToken); // Inject tokens into response $responseType->setAccessToken($accessToken); diff --git a/src/Grant/RefreshTokenGrant.php b/src/Grant/RefreshTokenGrant.php index b43792ee..adaa2306 100644 --- a/src/Grant/RefreshTokenGrant.php +++ b/src/Grant/RefreshTokenGrant.php @@ -12,6 +12,7 @@ namespace League\OAuth2\Server\Grant; use League\Event\Event; +use League\OAuth2\Server\Entities\ScopeEntity; use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; @@ -23,17 +24,12 @@ use Psr\Http\Message\ServerRequestInterface; */ class RefreshTokenGrant extends AbstractGrant { - /** - * @var \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface - */ - private $refreshTokenRepository; - /** * @param \League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface $refreshTokenRepository */ public function __construct(RefreshTokenRepositoryInterface $refreshTokenRepository) { - $this->refreshTokenRepository = $refreshTokenRepository; + $this->setRefreshTokenRepository($refreshTokenRepository); $this->refreshTokenTTL = new \DateInterval('P1M'); } @@ -47,13 +43,17 @@ class RefreshTokenGrant extends AbstractGrant \DateInterval $accessTokenTTL ) { // Validate request - $client = $this->validateClient($request); + $client = $this->validateClient($request); $oldRefreshToken = $this->validateOldRefreshToken($request, $client->getIdentifier()); - $scopes = $this->validateScopes($request, $client); + $scopes = $this->validateScopes($request, $client); // If no new scopes are requested then give the access token the original session scopes if (count($scopes) === 0) { - $scopes = $oldRefreshToken['scopes']; + $scopes = array_map(function ($scopeId) { + $scope = new ScopeEntity(); + $scope->setIdentifier($scopeId); + return $scope; + }, $oldRefreshToken['scopes']); } else { // The OAuth spec says that a refreshed access token can have the original scopes or fewer so ensure // the request doesn't include any new scopes @@ -68,13 +68,13 @@ class RefreshTokenGrant extends AbstractGrant // Expire old tokens $this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']); - $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']); + $this->getRefreshTokenRepository()->revokeRefreshToken($oldRefreshToken['refresh_token_id']); // Issue and persist new tokens $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken['user_id'], $scopes); $refreshToken = $this->issueRefreshToken($accessToken); $this->accessTokenRepository->persistNewAccessToken($accessToken); - $this->refreshTokenRepository->persistNewRefreshToken($refreshToken); + $this->getRefreshTokenRepository()->persistNewRefreshToken($refreshToken); // Inject tokens into response $responseType->setAccessToken($accessToken); @@ -120,7 +120,7 @@ class RefreshTokenGrant extends AbstractGrant throw OAuthServerException::invalidRefreshToken('Token has expired'); } - if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) { + if ($this->getRefreshTokenRepository()->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) { throw OAuthServerException::invalidRefreshToken('Token has been revoked'); } diff --git a/src/Repositories/ClientRepositoryInterface.php b/src/Repositories/ClientRepositoryInterface.php index 3bec9452..eb88a6cd 100644 --- a/src/Repositories/ClientRepositoryInterface.php +++ b/src/Repositories/ClientRepositoryInterface.php @@ -19,12 +19,10 @@ interface ClientRepositoryInterface extends RepositoryInterface /** * Get a client * - * @param string $grantType The grant type used * @param string $clientIdentifier The client's identifier - * @param string|null $clientSecret The client's secret - * @param string|null $redirectUri The client's redirect URI + * @param string $grantType The grant type used * * @return \League\OAuth2\Server\Entities\Interfaces\ClientEntityInterface */ - public function getClientEntity($grantType, $clientIdentifier, $clientSecret = null, $redirectUri = null); + public function getClientEntity($clientIdentifier, $grantType); } diff --git a/tests/Grant/AbstractGrantTest.php b/tests/Grant/AbstractGrantTest.php index 30e0079c..d64f6edb 100644 --- a/tests/Grant/AbstractGrantTest.php +++ b/tests/Grant/AbstractGrantTest.php @@ -10,33 +10,56 @@ use League\OAuth2\Server\Entities\Interfaces\AuthCodeEntityInterface; use League\OAuth2\Server\Entities\Interfaces\RefreshTokenEntityInterface; use League\OAuth2\Server\Entities\ScopeEntity; use League\OAuth2\Server\Grant\AbstractGrant; -use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; +use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; +use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; +use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; use Zend\Diactoros\ServerRequest; class AbstractGrantTest extends \PHPUnit_Framework_TestCase { public function testGetSet() { - $clientRepositoryMock = $this->getMock(ClientRepositoryInterface::class); - $accessTokenRepositoryMock = $this->getMock(AccessTokenRepositoryInterface::class); - $scopeRepositoryMock = $this->getMock(ScopeRepositoryInterface::class); + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setPathToPrivateKey('./private.key'); + $grantMock->setPathToPublicKey('./public.key'); + $grantMock->setEmitter(new Emitter()); + } + + public function testValidateClientPublic() + { + $client = new ClientEntity(); + + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); /** @var AbstractGrant $grantMock */ $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); $grantMock->setClientRepository($clientRepositoryMock); - $grantMock->setAccessTokenRepository($accessTokenRepositoryMock); - $grantMock->setScopeRepository($scopeRepositoryMock); - $grantMock->setPathToPrivateKey('./private.key'); - $grantMock->setPathToPublicKey('./public.key'); - $grantMock->setEmitter(new Emitter()); - $grantMock->setRefreshTokenTTL(new \DateInterval('PT1H')); + + $abstractGrantReflection = new \ReflectionClass($grantMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + ] + ); + $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); + $validateClientMethod->setAccessible(true); + + $result = $validateClientMethod->invoke($grantMock, $serverRequest, true, true); + $this->assertEquals($client, $result); } - public function testValidateClient() + public function testValidateClientConfidential() { $client = new ClientEntity(); + $client->setSecret('bar'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -89,6 +112,7 @@ class AbstractGrantTest extends \PHPUnit_Framework_TestCase public function testValidateClientMissingClientSecret() { $client = new ClientEntity(); + $client->setSecret('bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -112,9 +136,10 @@ class AbstractGrantTest extends \PHPUnit_Framework_TestCase /** * @expectedException \League\OAuth2\Server\Exception\OAuthServerException */ - public function testValidateClientMissingRedirectUri() + public function testValidateClientInvalidClientSecret() { $client = new ClientEntity(); + $client->setSecret('bar'); $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); $clientRepositoryMock->method('getClientEntity')->willReturn($client); @@ -124,6 +149,60 @@ class AbstractGrantTest extends \PHPUnit_Framework_TestCase $abstractGrantReflection = new \ReflectionClass($grantMock); + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody([ + 'client_id' => 'foo', + 'client_secret' => 'foo', + ]); + + $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); + $validateClientMethod->setAccessible(true); + + $validateClientMethod->invoke($grantMock, $serverRequest, true, true); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + */ + public function testValidateClientInvalidRedirectUri() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setClientRepository($clientRepositoryMock); + + $abstractGrantReflection = new \ReflectionClass($grantMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody([ + 'client_id' => 'foo', + 'redirect_uri' => 'http://bar/foo' + ]); + + $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); + $validateClientMethod->setAccessible(true); + + $validateClientMethod->invoke($grantMock, $serverRequest, true, true); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + */ + public function testValidateClientBadClient() + { + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn(null); + + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setClientRepository($clientRepositoryMock); + + $abstractGrantReflection = new \ReflectionClass($grantMock); + $serverRequest = new ServerRequest(); $serverRequest = $serverRequest->withParsedBody([ 'client_id' => 'foo', @@ -133,7 +212,7 @@ class AbstractGrantTest extends \PHPUnit_Framework_TestCase $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); $validateClientMethod->setAccessible(true); - $validateClientMethod->invoke($grantMock, $serverRequest, true, true); + $validateClientMethod->invoke($grantMock, $serverRequest, true); } public function testCanRespondToRequest() @@ -151,9 +230,12 @@ class AbstractGrantTest extends \PHPUnit_Framework_TestCase public function testIssueRefreshToken() { + $refreshTokenRepoMock = $this->getMock(RefreshTokenRepositoryInterface::class); + /** @var AbstractGrant $grantMock */ $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); $grantMock->setRefreshTokenTTL(new \DateInterval('PT1M')); + $grantMock->setRefreshTokenRepository($refreshTokenRepoMock); $abstractGrantReflection = new \ReflectionClass($grantMock); $issueRefreshTokenMethod = $abstractGrantReflection->getMethod('issueRefreshToken'); @@ -169,8 +251,11 @@ class AbstractGrantTest extends \PHPUnit_Framework_TestCase public function testIssueAccessToken() { + $accessTokenRepoMock = $this->getMock(AccessTokenRepositoryInterface::class); + /** @var AbstractGrant $grantMock */ $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setAccessTokenRepository($accessTokenRepoMock); $abstractGrantReflection = new \ReflectionClass($grantMock); $issueAccessTokenMethod = $abstractGrantReflection->getMethod('issueAccessToken'); @@ -190,8 +275,11 @@ class AbstractGrantTest extends \PHPUnit_Framework_TestCase public function testIssueAuthCode() { + $authCodeRepoMock = $this->getMock(AuthCodeRepositoryInterface::class); + /** @var AbstractGrant $grantMock */ $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setAuthCodeRepository($authCodeRepoMock); $abstractGrantReflection = new \ReflectionClass($grantMock); $issueAuthCodeMethod = $abstractGrantReflection->getMethod('issueAuthCode'); @@ -286,4 +374,15 @@ class AbstractGrantTest extends \PHPUnit_Framework_TestCase $grantMock->validateScopes($serverRequest, new ClientEntity()); } + + public function testGenerateUniqueIdentifier() + { + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + + $abstractGrantReflection = new \ReflectionClass($grantMock); + $method = $abstractGrantReflection->getMethod('generateUniqueIdentifier'); + $method->setAccessible(true); + + $this->assertTrue(is_string($method->invoke($grantMock))); + } } diff --git a/tests/Grant/ClientCredentialsGrantTest.php b/tests/Grant/ClientCredentialsGrantTest.php new file mode 100644 index 00000000..52e51c9a --- /dev/null +++ b/tests/Grant/ClientCredentialsGrantTest.php @@ -0,0 +1,48 @@ +assertEquals('client_credentials', $grant->getIdentifier()); + } + + public function testRespondToRequest() + { + $client = new ClientEntity(); + $client->setSecret('bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $grant = new ClientCredentialsGrant(); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + + $this->assertTrue($responseType->getAccessToken() instanceof AccessTokenEntityInterface); + } +} diff --git a/tests/Grant/PasswordGrantTest.php b/tests/Grant/PasswordGrantTest.php new file mode 100644 index 00000000..c81e4c61 --- /dev/null +++ b/tests/Grant/PasswordGrantTest.php @@ -0,0 +1,65 @@ +getMock(UserRepositoryInterface::class); + $refreshTokenRepositoryMock = $this->getMock(RefreshTokenRepositoryInterface::class); + + $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); + $this->assertEquals('password', $grant->getIdentifier()); + } + + public function testRespondToRequest() + { + $client = new ClientEntity(); + $client->setSecret('bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); + $userEntity = new UserEntity(); + $userRepositoryMock->method('getUserEntityByUserCredentials')->willReturn($userEntity); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + + $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'username' => 'foo', + 'password' => 'bar', + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + + $this->assertTrue($responseType->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($responseType->getRefreshToken() instanceof RefreshTokenEntityInterface); + } +} diff --git a/tests/Grant/RefreshTokenGrantTest.php b/tests/Grant/RefreshTokenGrantTest.php new file mode 100644 index 00000000..8bfe71c9 --- /dev/null +++ b/tests/Grant/RefreshTokenGrantTest.php @@ -0,0 +1,83 @@ +getMock(RefreshTokenRepositoryInterface::class); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $this->assertEquals('refresh_token', $grant->getIdentifier()); + } + + public function testRespondToRequest() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setSecret('bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); + $userEntity = new UserEntity(); + $userRepositoryMock->method('getUserEntityByUserCredentials')->willReturn($userEntity); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setPathToPublicKey('file://'.__DIR__.'/../Utils/public.key'); + $grant->setPathToPrivateKey('file://'.__DIR__.'/../Utils/private.key'); + + $oldRefreshToken = KeyCrypt::encrypt( + json_encode( + [ + 'client_id' => 'foo', + 'refresh_token_id' => 'zyxwvu', + 'access_token_id' => 'abcdef', + 'scopes' => ['foo'], + 'user_id' => 123, + 'expire_time' => time() + 3600, + ] + ), + 'file://'.__DIR__.'/../Utils/private.key' + ); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'refresh_token' => $oldRefreshToken, + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + + $this->assertTrue($responseType->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($responseType->getRefreshToken() instanceof RefreshTokenEntityInterface); + } +} diff --git a/tests/ServerTest.php b/tests/ServerTest.php new file mode 100644 index 00000000..9a85c351 --- /dev/null +++ b/tests/ServerTest.php @@ -0,0 +1,56 @@ +getMock(ClientRepositoryInterface::class), + $this->getMock(AccessTokenRepositoryInterface::class), + $this->getMock(ScopeRepositoryInterface::class), + '', + '', + new StubResponseType() + ); + + $server->enableGrantType(new ClientCredentialsGrant(), new \DateInterval('PT1M')); + + $response = $server->respondToRequest(); + $this->assertTrue($response instanceof ResponseInterface); + $this->assertEquals(400, $response->getStatusCode()); + } + + public function testRespondToRequest() + { + $clientRepository = $this->getMock(ClientRepositoryInterface::class); + $clientRepository->method('getClientEntity')->willReturn(new ClientEntity()); + + $server = new Server( + $clientRepository, + $this->getMock(AccessTokenRepositoryInterface::class), + $this->getMock(ScopeRepositoryInterface::class), + '', + '', + new StubResponseType() + ); + + $server->enableGrantType(new ClientCredentialsGrant(), new \DateInterval('PT1M')); + + $_POST['grant_type'] = 'client_credentials'; + $_POST['client_id'] = 'foo'; + $_POST['client_secret'] = 'bar'; + $response = $server->respondToRequest(); + $this->assertEquals(200, $response->getStatusCode()); + } +} diff --git a/tests/Stubs/StubResponseType.php b/tests/Stubs/StubResponseType.php new file mode 100644 index 00000000..d62daf9b --- /dev/null +++ b/tests/Stubs/StubResponseType.php @@ -0,0 +1,61 @@ +accessToken; + } + + public function getRefreshToken() + { + return $this->refreshToken; + } + + /** + * @param \League\OAuth2\Server\Entities\Interfaces\AccessTokenEntityInterface $accessToken + */ + public function setAccessToken(AccessTokenEntityInterface $accessToken) + { + $this->accessToken = $accessToken; + } + + /** + * @param \League\OAuth2\Server\Entities\Interfaces\RefreshTokenEntityInterface $refreshToken + */ + public function setRefreshToken(RefreshTokenEntityInterface $refreshToken) + { + $this->refreshToken = $refreshToken; + } + + /** + * @param ServerRequestInterface $request + * + * @return ServerRequestInterface + */ + public function determineAccessTokenInHeader(ServerRequestInterface $request) + { + // TODO: Implement determineAccessTokenInHeader() method. + } + + /** + * @param ResponseInterface $response + * + * @return ResponseInterface + */ + public function generateHttpResponse(ResponseInterface $response) + { + return new Response(); + } +} \ No newline at end of file diff --git a/tests/Stubs/UserEntity.php b/tests/Stubs/UserEntity.php new file mode 100644 index 00000000..4c157df5 --- /dev/null +++ b/tests/Stubs/UserEntity.php @@ -0,0 +1,13 @@ +