From 425f6d1e2e1b2c2e78fdbf6d6b0044af38960b5e Mon Sep 17 00:00:00 2001 From: fengyuexingzi Date: Mon, 30 Oct 2017 10:07:51 +0800 Subject: [PATCH] first commit --- .gitignore | 5 + LICENSE | 20 + README.md | 1 + composer.json | 27 + examples/README.md | 53 + examples/private.key | 27 + examples/public.key | Bin 0 -> 922 bytes examples/public/api.php | 72 + examples/public/auth_code.php | 107 ++ examples/public/client_credentials.php | 78 + examples/public/implicit.php | 81 + examples/public/index.php | 8 + examples/public/middleware_use.php | 109 ++ examples/public/password.php | 72 + examples/public/refresh_token.php | 73 + examples/public/tree.php | 89 + examples/src/Entities/AccessTokenEntity.php | 20 + examples/src/Entities/AuthCodeEntity.php | 20 + examples/src/Entities/ClientEntity.php | 29 + examples/src/Entities/RefreshTokenEntity.php | 19 + examples/src/Entities/ScopeEntity.php | 23 + examples/src/Entities/UserEntity.php | 25 + .../Repositories/AccessTokenRepository.php | 57 + .../src/Repositories/AuthCodeRepository.php | 49 + .../src/Repositories/ClientRepository.php | 51 + .../Repositories/RefreshTokenRepository.php | 49 + examples/src/Repositories/ScopeRepository.php | 60 + examples/src/Repositories/UserRepository.php | 33 + phpunit.xml.dist | 24 + src/AuthorizationServer.php | 207 +++ .../AuthorizationValidatorInterface.php | 25 + .../BearerTokenValidator.php | 99 + src/CryptKey.php | 116 ++ src/CryptTrait.php | 64 + src/Entities/AccessTokenEntityInterface.php | 24 + src/Entities/AuthCodeEntityInterface.php | 23 + src/Entities/ClientEntityInterface.php | 36 + src/Entities/RefreshTokenEntityInterface.php | 55 + src/Entities/ScopeEntityInterface.php | 20 + src/Entities/TokenInterface.php | 83 + src/Entities/Traits/AccessTokenTrait.php | 61 + src/Entities/Traits/AuthCodeTrait.php | 34 + src/Entities/Traits/ClientTrait.php | 46 + src/Entities/Traits/EntityTrait.php | 34 + src/Entities/Traits/RefreshTokenTrait.php | 61 + src/Entities/Traits/TokenEntityTrait.php | 116 ++ src/Entities/UserEntityInterface.php | 20 + src/Exception/OAuthServerException.php | 296 +++ ...IdentifierConstraintViolationException.php | 20 + src/Grant/AbstractAuthorizeGrant.php | 29 + src/Grant/AbstractGrant.php | 512 ++++++ src/Grant/AuthCodeGrant.php | 354 ++++ src/Grant/ClientCredentialsGrant.php | 53 + src/Grant/GrantTypeInterface.php | 135 ++ src/Grant/ImplicitGrant.php | 225 +++ src/Grant/PasswordGrant.php | 111 ++ src/Grant/RefreshTokenGrant.php | 133 ++ .../AuthorizationServerMiddleware.php | 55 + src/Middleware/ResourceServerMiddleware.php | 55 + .../AccessTokenRepositoryInterface.php | 57 + .../AuthCodeRepositoryInterface.php | 51 + .../ClientRepositoryInterface.php | 31 + .../RefreshTokenRepositoryInterface.php | 51 + src/Repositories/RepositoryInterface.php | 17 + src/Repositories/ScopeRepositoryInterface.php | 46 + src/Repositories/UserRepositoryInterface.php | 33 + src/RequestEvent.php | 46 + src/RequestTypes/AuthorizationRequest.php | 224 +++ src/ResourceServer.php | 84 + src/ResponseTypes/AbstractResponseType.php | 64 + src/ResponseTypes/BearerTokenResponse.php | 78 + src/ResponseTypes/RedirectResponse.php | 40 + src/ResponseTypes/ResponseTypeInterface.php | 43 + tests/AuthorizationServerTest.php | 208 +++ tests/Bootstrap.php | 11 + tests/CryptKeyTest.php | 36 + tests/CryptTraitTest.php | 29 + tests/Grant/AbstractGrantTest.php | 496 +++++ tests/Grant/AuthCodeGrantTest.php | 1637 +++++++++++++++++ tests/Grant/ClientCredentialsGrantTest.php | 54 + tests/Grant/ImplicitGrantTest.php | 410 +++++ tests/Grant/PasswordGrantTest.php | 170 ++ tests/Grant/RefreshTokenGrantTest.php | 421 +++++ .../AuthorizationServerMiddlewareTest.php | 113 ++ .../ResourceServerMiddlewareTest.php | 107 ++ tests/ResourceServerTest.php | 26 + .../ResponseTypes/BearerResponseTypeTest.php | 298 +++ .../BearerTokenResponseWithParams.php | 14 + tests/Stubs/AccessTokenEntity.php | 13 + tests/Stubs/AuthCodeEntity.php | 13 + tests/Stubs/ClientEntity.php | 22 + tests/Stubs/CryptTraitStub.php | 31 + tests/Stubs/RefreshTokenEntity.php | 12 + tests/Stubs/ScopeEntity.php | 16 + tests/Stubs/StubResponseType.php | 70 + tests/Stubs/UserEntity.php | 16 + tests/Stubs/private.key | 15 + tests/Stubs/public.key | 6 + 98 files changed, 9492 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 examples/README.md create mode 100644 examples/private.key create mode 100644 examples/public.key create mode 100644 examples/public/api.php create mode 100644 examples/public/auth_code.php create mode 100644 examples/public/client_credentials.php create mode 100644 examples/public/implicit.php create mode 100644 examples/public/index.php create mode 100644 examples/public/middleware_use.php create mode 100644 examples/public/password.php create mode 100644 examples/public/refresh_token.php create mode 100644 examples/public/tree.php create mode 100644 examples/src/Entities/AccessTokenEntity.php create mode 100644 examples/src/Entities/AuthCodeEntity.php create mode 100644 examples/src/Entities/ClientEntity.php create mode 100644 examples/src/Entities/RefreshTokenEntity.php create mode 100644 examples/src/Entities/ScopeEntity.php create mode 100644 examples/src/Entities/UserEntity.php create mode 100644 examples/src/Repositories/AccessTokenRepository.php create mode 100644 examples/src/Repositories/AuthCodeRepository.php create mode 100644 examples/src/Repositories/ClientRepository.php create mode 100644 examples/src/Repositories/RefreshTokenRepository.php create mode 100644 examples/src/Repositories/ScopeRepository.php create mode 100644 examples/src/Repositories/UserRepository.php create mode 100644 phpunit.xml.dist create mode 100644 src/AuthorizationServer.php create mode 100644 src/AuthorizationValidators/AuthorizationValidatorInterface.php create mode 100644 src/AuthorizationValidators/BearerTokenValidator.php create mode 100644 src/CryptKey.php create mode 100644 src/CryptTrait.php create mode 100644 src/Entities/AccessTokenEntityInterface.php create mode 100644 src/Entities/AuthCodeEntityInterface.php create mode 100644 src/Entities/ClientEntityInterface.php create mode 100644 src/Entities/RefreshTokenEntityInterface.php create mode 100644 src/Entities/ScopeEntityInterface.php create mode 100644 src/Entities/TokenInterface.php create mode 100644 src/Entities/Traits/AccessTokenTrait.php create mode 100644 src/Entities/Traits/AuthCodeTrait.php create mode 100644 src/Entities/Traits/ClientTrait.php create mode 100644 src/Entities/Traits/EntityTrait.php create mode 100644 src/Entities/Traits/RefreshTokenTrait.php create mode 100644 src/Entities/Traits/TokenEntityTrait.php create mode 100644 src/Entities/UserEntityInterface.php create mode 100644 src/Exception/OAuthServerException.php create mode 100644 src/Exception/UniqueTokenIdentifierConstraintViolationException.php create mode 100644 src/Grant/AbstractAuthorizeGrant.php create mode 100644 src/Grant/AbstractGrant.php create mode 100644 src/Grant/AuthCodeGrant.php create mode 100644 src/Grant/ClientCredentialsGrant.php create mode 100644 src/Grant/GrantTypeInterface.php create mode 100644 src/Grant/ImplicitGrant.php create mode 100644 src/Grant/PasswordGrant.php create mode 100644 src/Grant/RefreshTokenGrant.php create mode 100644 src/Middleware/AuthorizationServerMiddleware.php create mode 100644 src/Middleware/ResourceServerMiddleware.php create mode 100644 src/Repositories/AccessTokenRepositoryInterface.php create mode 100644 src/Repositories/AuthCodeRepositoryInterface.php create mode 100644 src/Repositories/ClientRepositoryInterface.php create mode 100644 src/Repositories/RefreshTokenRepositoryInterface.php create mode 100644 src/Repositories/RepositoryInterface.php create mode 100644 src/Repositories/ScopeRepositoryInterface.php create mode 100644 src/Repositories/UserRepositoryInterface.php create mode 100644 src/RequestEvent.php create mode 100644 src/RequestTypes/AuthorizationRequest.php create mode 100644 src/ResourceServer.php create mode 100644 src/ResponseTypes/AbstractResponseType.php create mode 100644 src/ResponseTypes/BearerTokenResponse.php create mode 100644 src/ResponseTypes/RedirectResponse.php create mode 100644 src/ResponseTypes/ResponseTypeInterface.php create mode 100644 tests/AuthorizationServerTest.php create mode 100644 tests/Bootstrap.php create mode 100644 tests/CryptKeyTest.php create mode 100644 tests/CryptTraitTest.php create mode 100644 tests/Grant/AbstractGrantTest.php create mode 100644 tests/Grant/AuthCodeGrantTest.php create mode 100644 tests/Grant/ClientCredentialsGrantTest.php create mode 100644 tests/Grant/ImplicitGrantTest.php create mode 100644 tests/Grant/PasswordGrantTest.php create mode 100644 tests/Grant/RefreshTokenGrantTest.php create mode 100644 tests/Middleware/AuthorizationServerMiddlewareTest.php create mode 100644 tests/Middleware/ResourceServerMiddlewareTest.php create mode 100644 tests/ResourceServerTest.php create mode 100644 tests/ResponseTypes/BearerResponseTypeTest.php create mode 100644 tests/ResponseTypes/BearerTokenResponseWithParams.php create mode 100644 tests/Stubs/AccessTokenEntity.php create mode 100644 tests/Stubs/AuthCodeEntity.php create mode 100644 tests/Stubs/ClientEntity.php create mode 100644 tests/Stubs/CryptTraitStub.php create mode 100644 tests/Stubs/RefreshTokenEntity.php create mode 100644 tests/Stubs/ScopeEntity.php create mode 100644 tests/Stubs/StubResponseType.php create mode 100644 tests/Stubs/UserEntity.php create mode 100644 tests/Stubs/private.key create mode 100644 tests/Stubs/public.key diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3eb7e17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor +/composer.lock +phpunit.xml +.idea +build \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a4f444b --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (C) Alex Bilbie + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..72db455 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +oauth2-server test \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..49e88df --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "require": { + "php": ">=5.6.0", + "league/oauth2-server":"*", + "ext-openssl": "*", + "league/event": "^2.1", + "lcobucci/jwt": "^3.1", + "paragonie/random_compat": "^2.0", + "psr/http-message": "^1.0", + "defuse/php-encryption": "^2.1", + "slim/slim": "3.0.*" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.0", + "zendframework/zend-diactoros": "^1.0" + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Server\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "LeagueTests\\": "tests/" + } + } +} \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..d8898b4 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,53 @@ +# Example implementations + +## Installation + +0. Run `composer install` in this directory to install dependencies +0. Create a private key `openssl genrsa -out private.key 2048` +0. Create a public key `openssl rsa -in private.key -pubout > public.key` +0. `cd` into the public directory +0. Start a PHP server `php -S localhost:4444` + +## Testing the client credentials grant example + +Send the following cURL request: + +``` +curl -X "POST" "http://localhost:4444/client_credentials.php/access_token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -H "Accept: 1.0" \ + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=myawesomeapp" \ + --data-urlencode "client_secret=abc123" \ + --data-urlencode "scope=basic email" +``` + +## Testing the password grant example + +Send the following cURL request: + +``` +curl -X "POST" "http://localhost:4444/password.php/access_token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -H "Accept: 1.0" \ + --data-urlencode "grant_type=password" \ + --data-urlencode "client_id=myawesomeapp" \ + --data-urlencode "client_secret=abc123" \ + --data-urlencode "username=alex" \ + --data-urlencode "password=whisky" \ + --data-urlencode "scope=basic email" +``` + +## Testing the refresh token grant example + +Send the following cURL request. Replace `{{REFRESH_TOKEN}}` with a refresh token from another grant above: + +``` +curl -X "POST" "http://localhost:4444/refresh_token.php/access_token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -H "Accept: 1.0" \ + --data-urlencode "grant_type=refresh_token" \ + --data-urlencode "client_id=myawesomeapp" \ + --data-urlencode "client_secret=abc123" \ + --data-urlencode "refresh_token={{REFRESH_TOKEN}}" +``` diff --git a/examples/private.key b/examples/private.key new file mode 100644 index 0000000..142fd6c --- /dev/null +++ b/examples/private.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAxHSx2v6sdLIBYIGyDo68YXh2okin6R6Abt8PVMNAr/8+wCLX +JibPFR930d9wmu7NxKR2uXC7HmsFe5mgU8Y+EeziuW+1a5pNY77cB/z8j02dxZam +KW/U+mI4t/j4iGveM4fWsxx6fAYIhF60Wtj/fcNPrncCPcfKHW7fJhuth6ExwLgl +sN/LwVGHstP/KUY/F+nUX3l7Tj25FNaV1+I5bB5Rfi1Kp1udvVYSIhFIvgZdND9Y +JCT4u5HfbS7JohL4qkFVLuaILW+T7QWx68fZlJ115sDFzr0eno5OwzwRw6aqmB0C +Ukr2BmW/8EgUA5HU9MMfSPnbBBxsjCu3gWL+FwIDAQABAoIBAQCe/EVXEhUkl03P ++QXNDCxdwSxY5u2kFgAgObphgWYfSDRTL57oDC44TPAd03KAn9O8WNCQADAeTMPy +JACA64Ud4pQVN9cdDpGksDfG6zDFFC4jUmiETjGQs95s3DrdxFXdZAgXiWuCZSuN +XEse3k/dSit46GzS7JWfvjMu1uVXRWxQiRmke18tHqNJ+KA8hhF05olMGEA6oLwG +bu1pUL01iS+6UfVa1Oi7z8zGqTXpGKY6hHRjXQEZXH1oA6CaogTlUvtGUzRSYKeP +Ya1xVuF6gQDRrMKeZe9Vzyk2/uXgIP3aJS0NjPwXPnj3Ni6FtyjX+MV4Jomvb5FR +RgBOOIURAoGBAOBMFsWq4rvwDDZ8F/+qifYkzOOrXmM10Xq0XqULokEOGlZjRGhE +nHl23atGvHfXWWQayP705ZsW4MDxzLhtkCeCxgRWWsNDaEBY+Eq6QLZNPfoQ8KqU +40BZYxMmxereweQ2e5GPa6FQe0AcBnC8jOgQcMjdV5OvLhZ5g9LMb6YvAoGBAOA5 +M1jN8vc6Lr2AJBtPsRxobc2Q6moB0aqc0/+a+N+ZeahO6RkoGragMsRq57U1SFUR +oUEiJoUZzpVFul21fYFEym3m395agNZNioFks0P1lwFMXc+JLpXEjRKGSCD7K4lD +dn7Mhp2eXP2PRBYWsJkfLoU8c6NMGf2871Ai4xSZAoGADQ9hJBFXMmo/y8xd+V0M +u3BZHciUrmIr2GE4QZPz1qXjkrQk/40/LzNkpNxxjOjVI6cLnQzVcbpbS+DIctSu +WB6dORNuJYB/SX/ktTUzH2pP+YeS+9u/f7e0tSDE1XAzCf7CIy+erL3q70iyd04D +Rl/usUNyHf9NR7G7o0KUHbECgYEAjjUZEMCF0TDQhhVoUP4JCUheI5s+YP9IV9nh +RuKl50Jye0GY6wCZeKw/pn1a+X2So4lr6WBcZ9xHPjscCOTbdYoIjQ15fI+P2NsS +9h/E5lwzanphoQeTSR+AjXgm9Ov337WuyJq04fCXb6VWfF/lnye8nHoLqUzRyzDx +4rRNqzkCgYARGIA5qZMfDX1ogTGQZ1ghF3vRiPpRkEdNgoAOEfV9kAC8f55a8t6i +8sKM/vUVAj8bvV1W/xUaVMKEh06wI5z4uBUj5R+8wuWNKSrCv6TG/Znq62A/AGwe +hUlY+Em75lKV7Yn2ZbW6VT05l3zV/sIQhm5Xld98JFCqxCmkx08WXw== +-----END RSA PRIVATE KEY----- diff --git a/examples/public.key b/examples/public.key new file mode 100644 index 0000000000000000000000000000000000000000..e854384ea3981a06edd0089489838cdf4b9c9828 GIT binary patch literal 922 zcma)*OH;x?5QO_|mH)vzMTJ)+9+N;MD2PJ9cmpxuBR;?op5n4Zt% zulTu|>79y!#&qcuuVfLIP~^jQtuECw&I9UCR7dB$(3>h#6T}upSMX-k`eZsvQ4;7* zHoFDhIOr)A8ro!c@MW5iscK5iD;zPnrfzY-;@rWd3XF5OZ>UM>FySNulQLNY#TGS< z!n+NeJ@yGTYYo8S-y@ z9#M12&N$^K&D{U_zP function () { + $server = new ResourceServer( + new AccessTokenRepository(), // instance of AccessTokenRepositoryInterface + 'file://' . __DIR__ . '/../public.key' // the authorization server's public key + ); + + return $server; + }, +]); + +// Add the resource server middleware which will intercept and validate requests +$app->add( + new \League\OAuth2\Server\Middleware\ResourceServerMiddleware( + $app->getContainer()->get(ResourceServer::class) + ) +); + +// An example endpoint secured with OAuth 2.0 +$app->get( + '/users', + function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { + $users = [ + [ + 'id' => 123, + 'name' => 'Alex', + 'email' => 'alex@thephpleague.com', + ], + [ + 'id' => 124, + 'name' => 'Frank', + 'email' => 'frank@thephpleague.com', + ], + [ + 'id' => 125, + 'name' => 'Phil', + 'email' => 'phil@thephpleague.com', + ], + ]; + + // If the access token doesn't have the `basic` scope hide users' names + if (in_array('basic', $request->getAttribute('oauth_scopes')) === false) { + for ($i = 0; $i < count($users); $i++) { + unset($users[$i]['name']); + } + } + + // If the access token doesn't have the `email` scope hide users' email addresses + if (in_array('email', $request->getAttribute('oauth_scopes')) === false) { + for ($i = 0; $i < count($users); $i++) { + unset($users[$i]['email']); + } + } + + $response->getBody()->write(json_encode($users)); + + return $response->withStatus(200); + } +); + +$app->run(); diff --git a/examples/public/auth_code.php b/examples/public/auth_code.php new file mode 100644 index 0000000..3c4ca68 --- /dev/null +++ b/examples/public/auth_code.php @@ -0,0 +1,107 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\Grant\AuthCodeGrant; +use OAuth2ServerExamples\Entities\UserEntity; +use OAuth2ServerExamples\Repositories\AccessTokenRepository; +use OAuth2ServerExamples\Repositories\AuthCodeRepository; +use OAuth2ServerExamples\Repositories\ClientRepository; +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; + +include __DIR__ . '/../vendor/autoload.php'; + +$app = new App([ + 'settings' => [ + 'displayErrorDetails' => true, + ], + AuthorizationServer::class => function () { + // Init our repositories + $clientRepository = new ClientRepository(); + $scopeRepository = new ScopeRepository(); + $accessTokenRepository = new AccessTokenRepository(); + $authCodeRepository = new AuthCodeRepository(); + $refreshTokenRepository = new RefreshTokenRepository(); + + $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; + + // Setup the authorization server + $server = new AuthorizationServer( + $clientRepository, + $accessTokenRepository, + $scopeRepository, + $privateKeyPath, + 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' + ); + + // Enable the authentication code grant on the server with a token TTL of 1 hour + $server->enableGrantType( + new AuthCodeGrant( + $authCodeRepository, + $refreshTokenRepository, + new \DateInterval('PT10M') + ), + new \DateInterval('PT1H') + ); + + return $server; + }, +]); + +$app->get('/authorize', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { + /* @var \League\OAuth2\Server\AuthorizationServer $server */ + $server = $app->getContainer()->get(AuthorizationServer::class); + + try { + // Validate the HTTP request and return an AuthorizationRequest object. + // The auth request object can be serialized into a user's session + $authRequest = $server->validateAuthorizationRequest($request); + + // Once the user has logged in set the user on the AuthorizationRequest + $authRequest->setUser(new UserEntity()); + + // Once the user has approved or denied the client update the status + // (true = approved, false = denied) + $authRequest->setAuthorizationApproved(true); + + // Return the HTTP redirect response + return $server->completeAuthorizationRequest($authRequest, $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(); diff --git a/examples/public/client_credentials.php b/examples/public/client_credentials.php new file mode 100644 index 0000000..b036f80 --- /dev/null +++ b/examples/public/client_credentials.php @@ -0,0 +1,78 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\Exception\OAuthServerException; +use OAuth2ServerExamples\Repositories\AccessTokenRepository; +use OAuth2ServerExamples\Repositories\ClientRepository; +use OAuth2ServerExamples\Repositories\ScopeRepository; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Slim\App; +use Zend\Diactoros\Stream; + +include __DIR__ . '/../vendor/autoload.php'; + +$app = new App([ + 'settings' => [ + 'displayErrorDetails' => true, + ], + AuthorizationServer::class => function () { + // Init our repositories + $clientRepository = new ClientRepository(); // instance of ClientRepositoryInterface + $scopeRepository = new ScopeRepository(); // instance of ScopeRepositoryInterface + $accessTokenRepository = new AccessTokenRepository(); // instance of AccessTokenRepositoryInterface + + // Path to public and private keys + $privateKey = 'file://' . __DIR__ . '/../private.key'; + //$privateKey = new CryptKey('file://path/to/private.key', 'passphrase'); // if private key has a pass phrase + + // Setup the authorization server + $server = new AuthorizationServer( + $clientRepository, + $accessTokenRepository, + $scopeRepository, + $privateKey, + 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' + ); + + // Enable the client credentials grant on the server + $server->enableGrantType( + new \League\OAuth2\Server\Grant\ClientCredentialsGrant(), + new \DateInterval('PT1H') // access tokens will expire after 1 hour + ); + + return $server; + }, +]); + +$app->post('/access_token', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { + + /* @var \League\OAuth2\Server\AuthorizationServer $server */ + $server = $app->getContainer()->get(AuthorizationServer::class); + + try { + + // Try to respond to the request + return $server->respondToAccessTokenRequest($request, $response); + } catch (OAuthServerException $exception) { + + // All instances of OAuthServerException can be formatted into a HTTP response + return $exception->generateHttpResponse($response); + } catch (\Exception $exception) { + + // Unknown exception + $body = new Stream('php://temp', 'r+'); + $body->write($exception->getMessage()); + + return $response->withStatus(500)->withBody($body); + } +}); + +$app->run(); \ No newline at end of file diff --git a/examples/public/implicit.php b/examples/public/implicit.php new file mode 100644 index 0000000..73de09e --- /dev/null +++ b/examples/public/implicit.php @@ -0,0 +1,81 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\Grant\ImplicitGrant; +use OAuth2ServerExamples\Entities\UserEntity; +use OAuth2ServerExamples\Repositories\AccessTokenRepository; +use OAuth2ServerExamples\Repositories\ClientRepository; +use OAuth2ServerExamples\Repositories\ScopeRepository; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Slim\App; +use Zend\Diactoros\Stream; + +include __DIR__ . '/../vendor/autoload.php'; + +$app = new App([ + 'settings' => [ + 'displayErrorDetails' => true, + ], + AuthorizationServer::class => function () { + // Init our repositories + $clientRepository = new ClientRepository(); + $scopeRepository = new ScopeRepository(); + $accessTokenRepository = new AccessTokenRepository(); + + $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; + + // Setup the authorization server + $server = new AuthorizationServer( + $clientRepository, + $accessTokenRepository, + $scopeRepository, + $privateKeyPath, + 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' + ); + $server->setEncryptionKey('lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen'); + + // Enable the implicit grant on the server with a token TTL of 1 hour + $server->enableGrantType(new ImplicitGrant(new \DateInterval('PT1H'))); + + return $server; + }, +]); + +$app->get('/authorize', function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { + /* @var \League\OAuth2\Server\AuthorizationServer $server */ + $server = $app->getContainer()->get(AuthorizationServer::class); + + try { + // Validate the HTTP request and return an AuthorizationRequest object. + // The auth request object can be serialized into a user's session + $authRequest = $server->validateAuthorizationRequest($request); + + // Once the user has logged in set the user on the AuthorizationRequest + $authRequest->setUser(new UserEntity()); + + // Once the user has approved or denied the client update the status + // (true = approved, false = denied) + $authRequest->setAuthorizationApproved(true); + + // Return the HTTP redirect response + return $server->completeAuthorizationRequest($authRequest, $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(); diff --git a/examples/public/index.php b/examples/public/index.php new file mode 100644 index 0000000..71853e4 --- /dev/null +++ b/examples/public/index.php @@ -0,0 +1,8 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\Grant\AuthCodeGrant; +use League\OAuth2\Server\Grant\RefreshTokenGrant; +use League\OAuth2\Server\Middleware\AuthorizationServerMiddleware; +use League\OAuth2\Server\Middleware\ResourceServerMiddleware; +use League\OAuth2\Server\ResourceServer; +use OAuth2ServerExamples\Repositories\AccessTokenRepository; +use OAuth2ServerExamples\Repositories\AuthCodeRepository; +use OAuth2ServerExamples\Repositories\ClientRepository; +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; + +include __DIR__ . '/../vendor/autoload.php'; + +$app = new App([ + 'settings' => [ + 'displayErrorDetails' => true, + ], + AuthorizationServer::class => function () { + // Init our repositories + $clientRepository = new ClientRepository(); + $accessTokenRepository = new AccessTokenRepository(); + $scopeRepository = new ScopeRepository(); + $authCodeRepository = new AuthCodeRepository(); + $refreshTokenRepository = new RefreshTokenRepository(); + + $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; + + // Setup the authorization server + $server = new AuthorizationServer( + $clientRepository, + $accessTokenRepository, + $scopeRepository, + $privateKeyPath, + 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' + ); + + // Enable the authentication code grant on the server with a token TTL of 1 hour + $server->enableGrantType( + new AuthCodeGrant( + $authCodeRepository, + $refreshTokenRepository, + new \DateInterval('PT10M') + ), + new \DateInterval('PT1H') + ); + + // Enable the refresh token grant on the server with a token TTL of 1 month + $server->enableGrantType( + new RefreshTokenGrant($refreshTokenRepository), + new \DateInterval('P1M') + ); + + return $server; + }, + ResourceServer::class => function () { + $publicKeyPath = 'file://' . __DIR__ . '/../public.key'; + + $server = new ResourceServer( + new AccessTokenRepository(), + $publicKeyPath + ); + + return $server; + }, +]); + +// Access token issuer +$app->post('/access_token', function () { +})->add(new AuthorizationServerMiddleware($app->getContainer()->get(AuthorizationServer::class))); + +// Secured API +$app->group('/api', function () { + $this->get('/user', function (ServerRequestInterface $request, ResponseInterface $response) { + $params = []; + + if (in_array('basic', $request->getAttribute('oauth_scopes', []))) { + $params = [ + 'id' => 1, + 'name' => 'Alex', + 'city' => 'London', + ]; + } + + if (in_array('email', $request->getAttribute('oauth_scopes', []))) { + $params['email'] = 'alex@example.com'; + } + + $body = new Stream('php://temp', 'r+'); + $body->write(json_encode($params)); + + return $response->withBody($body); + }); +})->add(new ResourceServerMiddleware($app->getContainer()->get(ResourceServer::class))); + +$app->run(); diff --git a/examples/public/password.php b/examples/public/password.php new file mode 100644 index 0000000..6857e98 --- /dev/null +++ b/examples/public/password.php @@ -0,0 +1,72 @@ + function () { + + // Setup the authorization server + $server = new AuthorizationServer( + new ClientRepository(), // instance of ClientRepositoryInterface + new AccessTokenRepository(), // instance of AccessTokenRepositoryInterface + new ScopeRepository(), // instance of ScopeRepositoryInterface + 'file://' . __DIR__ . '/../private.key', // path to private key + 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' // encryption key + ); + + $grant = new PasswordGrant( + new UserRepository(), // instance of UserRepositoryInterface + new RefreshTokenRepository() // instance of RefreshTokenRepositoryInterface + ); + $grant->setRefreshTokenTTL(new \DateInterval('P1M')); // refresh tokens will expire after 1 month + + // Enable the password grant on the server with a token TTL of 1 hour + $server->enableGrantType( + $grant, + new \DateInterval('PT1H') // access tokens will expire after 1 hour + ); + + return $server; + }, +]); + +$app->post( + '/access_token', + function (ServerRequestInterface $request, ResponseInterface $response) use ($app) { + + /* @var \League\OAuth2\Server\AuthorizationServer $server */ + $server = $app->getContainer()->get(AuthorizationServer::class); + + try { + + // Try to respond to the access token request + return $server->respondToAccessTokenRequest($request, $response); + } catch (OAuthServerException $exception) { + + // All instances of OAuthServerException can be converted to a PSR-7 response + return $exception->generateHttpResponse($response); + } catch (\Exception $exception) { + + // Catch unexpected exceptions + $body = $response->getBody(); + $body->write($exception->getMessage()); + + return $response->withStatus(500)->withBody($body); + } + } +); + +$app->run(); diff --git a/examples/public/refresh_token.php b/examples/public/refresh_token.php new file mode 100644 index 0000000..39be082 --- /dev/null +++ b/examples/public/refresh_token.php @@ -0,0 +1,73 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\Grant\RefreshTokenGrant; +use OAuth2ServerExamples\Repositories\AccessTokenRepository; +use OAuth2ServerExamples\Repositories\ClientRepository; +use OAuth2ServerExamples\Repositories\RefreshTokenRepository; +use OAuth2ServerExamples\Repositories\ScopeRepository; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Slim\App; + +include __DIR__ . '/../vendor/autoload.php'; + +$app = new App([ + 'settings' => [ + 'displayErrorDetails' => true, + ], + AuthorizationServer::class => function () { + // Init our repositories + $clientRepository = new ClientRepository(); + $accessTokenRepository = new AccessTokenRepository(); + $scopeRepository = new ScopeRepository(); + $refreshTokenRepository = new RefreshTokenRepository(); + + $privateKeyPath = 'file://' . __DIR__ . '/../private.key'; + + // Setup the authorization server + $server = new AuthorizationServer( + $clientRepository, + $accessTokenRepository, + $scopeRepository, + $privateKeyPath, + 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen' + ); + + // Enable the refresh token grant on the server + $grant = new RefreshTokenGrant($refreshTokenRepository); + $grant->setRefreshTokenTTL(new \DateInterval('P1M')); // The refresh token will expire in 1 month + + $server->enableGrantType( + $grant, + new \DateInterval('PT1H') // The new access token will expire after 1 hour + ); + + return $server; + }, +]); + +$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) { + $response->getBody()->write($exception->getMessage()); + + return $response->withStatus(500); + } +}); + +$app->run(); diff --git a/examples/public/tree.php b/examples/public/tree.php new file mode 100644 index 0000000..3c5e2e0 --- /dev/null +++ b/examples/public/tree.php @@ -0,0 +1,89 @@ + 0, 'name' => 'A'], + ['id' => 1, 'name' => 'a', 'pid' => 0], + ['id' => 2, 'name' => 'b', 'pid' => 1], + ['id' => 3, 'name' => 'c', 'pid' => 1], + ['id' => 4, 'name' => 'd', 'pid' => 5], + ['id' => 5, 'name' => 'e', 'pid' => 0], + ['id' => 6, 'name' => 'e', 'pid' => 4], +]; +function m_tree($arr, $pid = 0) +{ + $tree = []; + foreach ($arr as $t) { + if ($t['id'] == $pid) { + $tree = $t; + } + } + foreach ($arr as $child) { + if (isset($child['pid']) && $child['pid'] == $tree['id']) { + $tree['child'][] = $child; + } + } + foreach ($tree as $k1 => $v1) { + if (is_array($v1)) { + foreach ($v1 as $k2 => $v2) { + $tree['child'][$k2] = m_tree($arr, $v2['id']); + } + } + } + return $tree; +} + +echo '
';
+print_r(m_tree($test_data));
+echo '
'; +die; + +function tree($directory) +{ + $mydir = dir($directory); + echo "
    \n"; + while ($file = $mydir->read()) { + if ($directory == '/') { + $child = $directory . $file; + } else { + $child = "$directory/$file"; + } + if ((is_dir($child)) AND ($file != ".") AND ($file != "..") AND ($file != '$RECYCLE.BIN') AND ($file != 'System Volume Information')) { + var_dump("directory: " . $child); + echo "
  • $file
  • \n"; + tree($child); + } else + echo "
  • $file
  • \n"; + } + echo "
\n"; + $mydir->close(); +} + +function getChmod($filepath) +{ + return substr(base_convert(@fileperms($filepath), 10, 8), -4); +} + +$dirs = tree('/'); + +var_dump($dirs); + +var_dump(is_writable('/test')); + +$perms = getChmod('/test/'); +var_dump(fileperms('/test')); + +var_dump(scandir('/')); +var_dump($_SERVER['DOCUMENT_ROOT']); +chdir('d:/www'); +var_dump(scandir('/')); +echo getcwd(); + + + diff --git a/examples/src/Entities/AccessTokenEntity.php b/examples/src/Entities/AccessTokenEntity.php new file mode 100644 index 0000000..f55246b --- /dev/null +++ b/examples/src/Entities/AccessTokenEntity.php @@ -0,0 +1,20 @@ + + * @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\AccessTokenEntityInterface; +use League\OAuth2\Server\Entities\Traits\AccessTokenTrait; +use League\OAuth2\Server\Entities\Traits\EntityTrait; +use League\OAuth2\Server\Entities\Traits\TokenEntityTrait; + +class AccessTokenEntity implements AccessTokenEntityInterface +{ + use AccessTokenTrait, TokenEntityTrait, EntityTrait; +} diff --git a/examples/src/Entities/AuthCodeEntity.php b/examples/src/Entities/AuthCodeEntity.php new file mode 100644 index 0000000..acfbc3b --- /dev/null +++ b/examples/src/Entities/AuthCodeEntity.php @@ -0,0 +1,20 @@ + + * @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\AuthCodeEntityInterface; +use League\OAuth2\Server\Entities\Traits\AuthCodeTrait; +use League\OAuth2\Server\Entities\Traits\EntityTrait; +use League\OAuth2\Server\Entities\Traits\TokenEntityTrait; + +class AuthCodeEntity implements AuthCodeEntityInterface +{ + use EntityTrait, TokenEntityTrait, AuthCodeTrait; +} diff --git a/examples/src/Entities/ClientEntity.php b/examples/src/Entities/ClientEntity.php new file mode 100644 index 0000000..9d682a4 --- /dev/null +++ b/examples/src/Entities/ClientEntity.php @@ -0,0 +1,29 @@ + + * @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\ClientEntityInterface; +use League\OAuth2\Server\Entities\Traits\ClientTrait; +use League\OAuth2\Server\Entities\Traits\EntityTrait; + +class ClientEntity implements ClientEntityInterface +{ + use EntityTrait, ClientTrait; + + public function setName($name) + { + $this->name = $name; + } + + public function setRedirectUri($uri) + { + $this->redirectUri = $uri; + } +} diff --git a/examples/src/Entities/RefreshTokenEntity.php b/examples/src/Entities/RefreshTokenEntity.php new file mode 100644 index 0000000..60109c0 --- /dev/null +++ b/examples/src/Entities/RefreshTokenEntity.php @@ -0,0 +1,19 @@ + + * @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\RefreshTokenEntityInterface; +use League\OAuth2\Server\Entities\Traits\EntityTrait; +use League\OAuth2\Server\Entities\Traits\RefreshTokenTrait; + +class RefreshTokenEntity implements RefreshTokenEntityInterface +{ + use RefreshTokenTrait, EntityTrait; +} diff --git a/examples/src/Entities/ScopeEntity.php b/examples/src/Entities/ScopeEntity.php new file mode 100644 index 0000000..ec83cf5 --- /dev/null +++ b/examples/src/Entities/ScopeEntity.php @@ -0,0 +1,23 @@ + + * @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\ScopeEntityInterface; +use League\OAuth2\Server\Entities\Traits\EntityTrait; + +class ScopeEntity implements ScopeEntityInterface +{ + use EntityTrait; + + public function jsonSerialize() + { + return $this->getIdentifier(); + } +} diff --git a/examples/src/Entities/UserEntity.php b/examples/src/Entities/UserEntity.php new file mode 100644 index 0000000..22c1b4e --- /dev/null +++ b/examples/src/Entities/UserEntity.php @@ -0,0 +1,25 @@ + + * @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\UserEntityInterface; + +class UserEntity implements UserEntityInterface +{ + /** + * Return the user's identifier. + * + * @return mixed + */ + public function getIdentifier() + { + return 1; + } +} diff --git a/examples/src/Repositories/AccessTokenRepository.php b/examples/src/Repositories/AccessTokenRepository.php new file mode 100644 index 0000000..d7736c7 --- /dev/null +++ b/examples/src/Repositories/AccessTokenRepository.php @@ -0,0 +1,57 @@ + + * @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\AccessTokenEntityInterface; +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; +use OAuth2ServerExamples\Entities\AccessTokenEntity; + +class AccessTokenRepository implements AccessTokenRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity) + { + // Some logic here to save the access token to a database + } + + /** + * {@inheritdoc} + */ + public function revokeAccessToken($tokenId) + { + // Some logic here to revoke the access token + } + + /** + * {@inheritdoc} + */ + public function isAccessTokenRevoked($tokenId) + { + return false; // Access token hasn't been revoked + } + + /** + * {@inheritdoc} + */ + public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null) + { + $accessToken = new AccessTokenEntity(); + $accessToken->setClient($clientEntity); + foreach ($scopes as $scope) { + $accessToken->addScope($scope); + } + $accessToken->setUserIdentifier($userIdentifier); + + return $accessToken; + } +} diff --git a/examples/src/Repositories/AuthCodeRepository.php b/examples/src/Repositories/AuthCodeRepository.php new file mode 100644 index 0000000..d3ca482 --- /dev/null +++ b/examples/src/Repositories/AuthCodeRepository.php @@ -0,0 +1,49 @@ + + * @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\AuthCodeEntityInterface; +use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; +use OAuth2ServerExamples\Entities\AuthCodeEntity; + +class AuthCodeRepository implements AuthCodeRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity) + { + // Some logic to persist the auth code to a database + } + + /** + * {@inheritdoc} + */ + public function revokeAuthCode($codeId) + { + // Some logic to revoke the auth code in a database + } + + /** + * {@inheritdoc} + */ + public function isAuthCodeRevoked($codeId) + { + return false; // The auth code has not been revoked + } + + /** + * {@inheritdoc} + */ + public function getNewAuthCode() + { + return new AuthCodeEntity(); + } +} diff --git a/examples/src/Repositories/ClientRepository.php b/examples/src/Repositories/ClientRepository.php new file mode 100644 index 0000000..8d4b521 --- /dev/null +++ b/examples/src/Repositories/ClientRepository.php @@ -0,0 +1,51 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace OAuth2ServerExamples\Repositories; + +use League\OAuth2\Server\Repositories\ClientRepositoryInterface; +use OAuth2ServerExamples\Entities\ClientEntity; + +class ClientRepository implements ClientRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function getClientEntity($clientIdentifier, $grantType, $clientSecret = null, $mustValidateSecret = true) + { + $clients = [ + 'myawesomeapp' => [ + 'secret' => password_hash('abc123', PASSWORD_BCRYPT), + 'name' => 'My Awesome App', + 'redirect_uri' => 'http://foo/bar', + 'is_confidential' => true, + ], + ]; + + // Check if client is registered + if (array_key_exists($clientIdentifier, $clients) === false) { + return; + } + + if ( + $mustValidateSecret === true + && $clients[$clientIdentifier]['is_confidential'] === true + && password_verify($clientSecret, $clients[$clientIdentifier]['secret']) === false + ) { + return; + } + + $client = new ClientEntity(); + $client->setIdentifier($clientIdentifier); + $client->setName($clients[$clientIdentifier]['name']); + $client->setRedirectUri($clients[$clientIdentifier]['redirect_uri']); + + return $client; + } +} diff --git a/examples/src/Repositories/RefreshTokenRepository.php b/examples/src/Repositories/RefreshTokenRepository.php new file mode 100644 index 0000000..39a0b8c --- /dev/null +++ b/examples/src/Repositories/RefreshTokenRepository.php @@ -0,0 +1,49 @@ + + * @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\RefreshTokenEntityInterface; +use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; +use OAuth2ServerExamples\Entities\RefreshTokenEntity; + +class RefreshTokenRepository implements RefreshTokenRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntityInterface) + { + // Some logic to persist the refresh token in a database + } + + /** + * {@inheritdoc} + */ + public function revokeRefreshToken($tokenId) + { + // Some logic to revoke the refresh token in a database + } + + /** + * {@inheritdoc} + */ + public function isRefreshTokenRevoked($tokenId) + { + return false; // The refresh token has not been revoked + } + + /** + * {@inheritdoc} + */ + public function getNewRefreshToken() + { + return new RefreshTokenEntity(); + } +} diff --git a/examples/src/Repositories/ScopeRepository.php b/examples/src/Repositories/ScopeRepository.php new file mode 100644 index 0000000..d050d55 --- /dev/null +++ b/examples/src/Repositories/ScopeRepository.php @@ -0,0 +1,60 @@ + + * @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\Repositories\ScopeRepositoryInterface; +use OAuth2ServerExamples\Entities\ScopeEntity; + +class ScopeRepository implements ScopeRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function getScopeEntityByIdentifier($scopeIdentifier) + { + $scopes = [ + 'basic' => [ + 'description' => 'Basic details about you', + ], + 'email' => [ + 'description' => 'Your email address', + ], + ]; + + if (array_key_exists($scopeIdentifier, $scopes) === false) { + return; + } + + $scope = new ScopeEntity(); + $scope->setIdentifier($scopeIdentifier); + + return $scope; + } + + /** + * {@inheritdoc} + */ + public function finalizeScopes( + array $scopes, + $grantType, + ClientEntityInterface $clientEntity, + $userIdentifier = null + ) { + // Example of programatically modifying the final scope of the access token + if ((int) $userIdentifier === 1) { + $scope = new ScopeEntity(); + $scope->setIdentifier('email'); + $scopes[] = $scope; + } + + return $scopes; + } +} diff --git a/examples/src/Repositories/UserRepository.php b/examples/src/Repositories/UserRepository.php new file mode 100644 index 0000000..88836cd --- /dev/null +++ b/examples/src/Repositories/UserRepository.php @@ -0,0 +1,33 @@ + + * @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\Repositories\UserRepositoryInterface; +use OAuth2ServerExamples\Entities\UserEntity; + +class UserRepository implements UserRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function getUserEntityByUserCredentials( + $username, + $password, + $grantType, + ClientEntityInterface $clientEntity + ) { + if ($username === 'alex' && $password === 'whisky') { + return new UserEntity(); + } + + return; + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..9256408 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + + ./tests/ + + + + + src + + src/ResponseTypes/DefaultTemplates + src/TemplateRenderer + + + + + + + + diff --git a/src/AuthorizationServer.php b/src/AuthorizationServer.php new file mode 100644 index 0000000..46a9b27 --- /dev/null +++ b/src/AuthorizationServer.php @@ -0,0 +1,207 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server; + +use League\Event\EmitterAwareInterface; +use League\Event\EmitterAwareTrait; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\Grant\GrantTypeInterface; +use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; +use League\OAuth2\Server\Repositories\ClientRepositoryInterface; +use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; +use League\OAuth2\Server\RequestTypes\AuthorizationRequest; +use League\OAuth2\Server\ResponseTypes\BearerTokenResponse; +use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +class AuthorizationServer implements EmitterAwareInterface +{ + use EmitterAwareTrait; + + /** + * @var GrantTypeInterface[] + */ + protected $enabledGrantTypes = []; + + /** + * @var \DateInterval[] + */ + protected $grantTypeAccessTokenTTL = []; + + /** + * @var CryptKey + */ + protected $privateKey; + + /** + * @var CryptKey + */ + protected $publicKey; + + /** + * @var null|ResponseTypeInterface + */ + protected $responseType; + + /** + * @var ClientRepositoryInterface + */ + private $clientRepository; + + /** + * @var AccessTokenRepositoryInterface + */ + private $accessTokenRepository; + + /** + * @var ScopeRepositoryInterface + */ + private $scopeRepository; + + /** + * @var string + */ + private $encryptionKey; + + /** + * New server instance. + * + * @param ClientRepositoryInterface $clientRepository + * @param AccessTokenRepositoryInterface $accessTokenRepository + * @param ScopeRepositoryInterface $scopeRepository + * @param CryptKey|string $privateKey + * @param string $encryptionKey + * @param null|ResponseTypeInterface $responseType + */ + public function __construct( + ClientRepositoryInterface $clientRepository, + AccessTokenRepositoryInterface $accessTokenRepository, + ScopeRepositoryInterface $scopeRepository, + $privateKey, + $encryptionKey, + ResponseTypeInterface $responseType = null + ) { + $this->clientRepository = $clientRepository; + $this->accessTokenRepository = $accessTokenRepository; + $this->scopeRepository = $scopeRepository; + + if ($privateKey instanceof CryptKey === false) { + $privateKey = new CryptKey($privateKey); + } + $this->privateKey = $privateKey; + + $this->encryptionKey = $encryptionKey; + $this->responseType = $responseType; + } + + /** + * Enable a grant type on the server. + * + * @param GrantTypeInterface $grantType + * @param null|\DateInterval $accessTokenTTL + */ + public function enableGrantType(GrantTypeInterface $grantType, \DateInterval $accessTokenTTL = null) + { + if ($accessTokenTTL instanceof \DateInterval === false) { + $accessTokenTTL = new \DateInterval('PT1H'); + } + + $grantType->setAccessTokenRepository($this->accessTokenRepository); + $grantType->setClientRepository($this->clientRepository); + $grantType->setScopeRepository($this->scopeRepository); + $grantType->setPrivateKey($this->privateKey); + $grantType->setEmitter($this->getEmitter()); + $grantType->setEncryptionKey($this->encryptionKey); + + $this->enabledGrantTypes[$grantType->getIdentifier()] = $grantType; + $this->grantTypeAccessTokenTTL[$grantType->getIdentifier()] = $accessTokenTTL; + } + + /** + * Validate an authorization request + * + * @param ServerRequestInterface $request + * + * @throws OAuthServerException + * + * @return AuthorizationRequest + */ + public function validateAuthorizationRequest(ServerRequestInterface $request) + { + foreach ($this->enabledGrantTypes as $grantType) { + if ($grantType->canRespondToAuthorizationRequest($request)) { + return $grantType->validateAuthorizationRequest($request); + } + } + + throw OAuthServerException::unsupportedGrantType(); + } + + /** + * Complete an authorization request + * + * @param AuthorizationRequest $authRequest + * @param ResponseInterface $response + * + * @return ResponseInterface + */ + public function completeAuthorizationRequest(AuthorizationRequest $authRequest, ResponseInterface $response) + { + return $this->enabledGrantTypes[$authRequest->getGrantTypeId()] + ->completeAuthorizationRequest($authRequest) + ->generateHttpResponse($response); + } + + /** + * Return an access token response. + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * + * @throws OAuthServerException + * + * @return ResponseInterface + */ + public function respondToAccessTokenRequest(ServerRequestInterface $request, ResponseInterface $response) + { + foreach ($this->enabledGrantTypes as $grantType) { + if ($grantType->canRespondToAccessTokenRequest($request)) { + $tokenResponse = $grantType->respondToAccessTokenRequest( + $request, + $this->getResponseType(), + $this->grantTypeAccessTokenTTL[$grantType->getIdentifier()] + ); + + if ($tokenResponse instanceof ResponseTypeInterface) { + return $tokenResponse->generateHttpResponse($response); + } + } + } + + throw OAuthServerException::unsupportedGrantType(); + } + + /** + * Get the token type that grants will return in the HTTP response. + * + * @return ResponseTypeInterface + */ + protected function getResponseType() + { + if ($this->responseType instanceof ResponseTypeInterface === false) { + $this->responseType = new BearerTokenResponse(); + } + + $this->responseType->setPrivateKey($this->privateKey); + $this->responseType->setEncryptionKey($this->encryptionKey); + + return $this->responseType; + } +} diff --git a/src/AuthorizationValidators/AuthorizationValidatorInterface.php b/src/AuthorizationValidators/AuthorizationValidatorInterface.php new file mode 100644 index 0000000..7e49f84 --- /dev/null +++ b/src/AuthorizationValidators/AuthorizationValidatorInterface.php @@ -0,0 +1,25 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\AuthorizationValidators; + +use Psr\Http\Message\ServerRequestInterface; + +interface AuthorizationValidatorInterface +{ + /** + * Determine the access token in the authorization header and append OAUth properties to the request + * as attributes. + * + * @param ServerRequestInterface $request + * + * @return ServerRequestInterface + */ + public function validateAuthorization(ServerRequestInterface $request); +} diff --git a/src/AuthorizationValidators/BearerTokenValidator.php b/src/AuthorizationValidators/BearerTokenValidator.php new file mode 100644 index 0000000..6f299ce --- /dev/null +++ b/src/AuthorizationValidators/BearerTokenValidator.php @@ -0,0 +1,99 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\AuthorizationValidators; + +use Lcobucci\JWT\Parser; +use Lcobucci\JWT\Signer\Rsa\Sha256; +use Lcobucci\JWT\ValidationData; +use League\OAuth2\Server\CryptKey; +use League\OAuth2\Server\CryptTrait; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; +use Psr\Http\Message\ServerRequestInterface; + +class BearerTokenValidator implements AuthorizationValidatorInterface +{ + use CryptTrait; + + /** + * @var AccessTokenRepositoryInterface + */ + private $accessTokenRepository; + + /** + * @var \League\OAuth2\Server\CryptKey + */ + protected $publicKey; + + /** + * @param AccessTokenRepositoryInterface $accessTokenRepository + */ + public function __construct(AccessTokenRepositoryInterface $accessTokenRepository) + { + $this->accessTokenRepository = $accessTokenRepository; + } + + /** + * Set the public key + * + * @param \League\OAuth2\Server\CryptKey $key + */ + public function setPublicKey(CryptKey $key) + { + $this->publicKey = $key; + } + + /** + * {@inheritdoc} + */ + public function validateAuthorization(ServerRequestInterface $request) + { + if ($request->hasHeader('authorization') === false) { + throw OAuthServerException::accessDenied('Missing "Authorization" header'); + } + + $header = $request->getHeader('authorization'); + $jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0])); + + try { + // Attempt to parse and validate the JWT + $token = (new Parser())->parse($jwt); + if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) { + throw OAuthServerException::accessDenied('Access token could not be verified'); + } + + // Ensure access token hasn't expired + $data = new ValidationData(); + $data->setCurrentTime(time()); + + if ($token->validate($data) === false) { + throw OAuthServerException::accessDenied('Access token is invalid'); + } + + // Check if token has been revoked + if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) { + throw OAuthServerException::accessDenied('Access token has been revoked'); + } + + // Return the request with additional attributes + return $request + ->withAttribute('oauth_access_token_id', $token->getClaim('jti')) + ->withAttribute('oauth_client_id', $token->getClaim('aud')) + ->withAttribute('oauth_user_id', $token->getClaim('sub')) + ->withAttribute('oauth_scopes', $token->getClaim('scopes')); + } catch (\InvalidArgumentException $exception) { + // JWT couldn't be parsed so return the request as is + throw OAuthServerException::accessDenied($exception->getMessage()); + } catch (\RuntimeException $exception) { + //JWR couldn't be parsed so return the request as is + throw OAuthServerException::accessDenied('Error while decoding to JSON'); + } + } +} diff --git a/src/CryptKey.php b/src/CryptKey.php new file mode 100644 index 0000000..2ede9e3 --- /dev/null +++ b/src/CryptKey.php @@ -0,0 +1,116 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server; + +class CryptKey +{ + const RSA_KEY_PATTERN = + '/^(-----BEGIN (RSA )?(PUBLIC|PRIVATE) KEY-----\n)(.|\n)+(-----END (RSA )?(PUBLIC|PRIVATE) KEY-----)$/'; + + /** + * @var string + */ + protected $keyPath; + + /** + * @var null|string + */ + protected $passPhrase; + + /** + * @param string $keyPath + * @param null|string $passPhrase + * @param bool $keyPermissionsCheck + */ + public function __construct($keyPath, $passPhrase = null, $keyPermissionsCheck = true) + { + if (preg_match(self::RSA_KEY_PATTERN, $keyPath)) { + $keyPath = $this->saveKeyToFile($keyPath); + } + + if (strpos($keyPath, 'file://') !== 0) { + $keyPath = 'file://' . $keyPath; + } + + if (!file_exists($keyPath) || !is_readable($keyPath)) { + throw new \LogicException(sprintf('Key path "%s" does not exist or is not readable', $keyPath)); + } + + if ($keyPermissionsCheck === true) { + // Verify the permissions of the key + $keyPathPerms = decoct(fileperms($keyPath) & 0777); + if (in_array($keyPathPerms, ['600', '660'], true) === false) { + trigger_error(sprintf( + 'Key file "%s" permissions are not correct, should be 600 or 660 instead of %s', + $keyPath, + $keyPathPerms + ), E_USER_NOTICE); + } + } + + $this->keyPath = $keyPath; + $this->passPhrase = $passPhrase; + } + + /** + * @param string $key + * + * @throws \RuntimeException + * + * @return string + */ + private function saveKeyToFile($key) + { + $tmpDir = sys_get_temp_dir(); + $keyPath = $tmpDir . '/' . sha1($key) . '.key'; + + if (!file_exists($keyPath) && !touch($keyPath)) { + // @codeCoverageIgnoreStart + throw new \RuntimeException(sprintf('"%s" key file could not be created', $keyPath)); + // @codeCoverageIgnoreEnd + } + + if (file_put_contents($keyPath, $key) === false) { + // @codeCoverageIgnoreStart + throw new \RuntimeException(sprintf('Unable to write key file to temporary directory "%s"', $tmpDir)); + // @codeCoverageIgnoreEnd + } + + if (chmod($keyPath, 0600) === false) { + // @codeCoverageIgnoreStart + throw new \RuntimeException(sprintf('The key file "%s" file mode could not be changed with chmod to 600', $keyPath)); + // @codeCoverageIgnoreEnd + } + + return 'file://' . $keyPath; + } + + /** + * Retrieve key path. + * + * @return string + */ + public function getKeyPath() + { + return $this->keyPath; + } + + /** + * Retrieve key pass phrase. + * + * @return null|string + */ + public function getPassPhrase() + { + return $this->passPhrase; + } +} diff --git a/src/CryptTrait.php b/src/CryptTrait.php new file mode 100644 index 0000000..805969b --- /dev/null +++ b/src/CryptTrait.php @@ -0,0 +1,64 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server; + +use Defuse\Crypto\Crypto; + +trait CryptTrait +{ + /** + * @var string + */ + protected $encryptionKey; + + /** + * Encrypt data with a private key. + * + * @param string $unencryptedData + * + * @throws \LogicException + * @return string + */ + protected function encrypt($unencryptedData) + { + try { + return Crypto::encryptWithPassword($unencryptedData, $this->encryptionKey); + } catch (\Exception $e) { + throw new \LogicException($e->getMessage()); + } + } + + /** + * Decrypt data with a public key. + * + * @param string $encryptedData + * + * @throws \LogicException + * @return string + */ + protected function decrypt($encryptedData) + { + try { + return Crypto::decryptWithPassword($encryptedData, $this->encryptionKey); + } catch (\Exception $e) { + throw new \LogicException($e->getMessage()); + } + } + + /** + * Set the encryption key + * + * @param string $key + */ + public function setEncryptionKey($key = null) + { + $this->encryptionKey = $key; + } +} diff --git a/src/Entities/AccessTokenEntityInterface.php b/src/Entities/AccessTokenEntityInterface.php new file mode 100644 index 0000000..c297e26 --- /dev/null +++ b/src/Entities/AccessTokenEntityInterface.php @@ -0,0 +1,24 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities; + +use League\OAuth2\Server\CryptKey; + +interface AccessTokenEntityInterface extends TokenInterface +{ + /** + * Generate a JWT from the access token + * + * @param CryptKey $privateKey + * + * @return string + */ + public function convertToJWT(CryptKey $privateKey); +} diff --git a/src/Entities/AuthCodeEntityInterface.php b/src/Entities/AuthCodeEntityInterface.php new file mode 100644 index 0000000..e71aa2c --- /dev/null +++ b/src/Entities/AuthCodeEntityInterface.php @@ -0,0 +1,23 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities; + +interface AuthCodeEntityInterface extends TokenInterface +{ + /** + * @return string + */ + public function getRedirectUri(); + + /** + * @param string $uri + */ + public function setRedirectUri($uri); +} diff --git a/src/Entities/ClientEntityInterface.php b/src/Entities/ClientEntityInterface.php new file mode 100644 index 0000000..80cc70c --- /dev/null +++ b/src/Entities/ClientEntityInterface.php @@ -0,0 +1,36 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities; + +interface ClientEntityInterface +{ + /** + * Get the client's identifier. + * + * @return string + */ + public function getIdentifier(); + + /** + * Get the client's name. + * + * @return string + */ + public function getName(); + + /** + * Returns the registered redirect URI (as a string). + * + * Alternatively return an indexed array of redirect URIs. + * + * @return string|string[] + */ + public function getRedirectUri(); +} diff --git a/src/Entities/RefreshTokenEntityInterface.php b/src/Entities/RefreshTokenEntityInterface.php new file mode 100644 index 0000000..05e86e0 --- /dev/null +++ b/src/Entities/RefreshTokenEntityInterface.php @@ -0,0 +1,55 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities; + +interface RefreshTokenEntityInterface +{ + /** + * Get the token's identifier. + * + * @return string + */ + public function getIdentifier(); + + /** + * Set the token's identifier. + * + * @param $identifier + */ + public function setIdentifier($identifier); + + /** + * Get the token's expiry date time. + * + * @return \DateTime + */ + public function getExpiryDateTime(); + + /** + * Set the date time when the token expires. + * + * @param \DateTime $dateTime + */ + public function setExpiryDateTime(\DateTime $dateTime); + + /** + * Set the access token that the refresh token was associated with. + * + * @param AccessTokenEntityInterface $accessToken + */ + public function setAccessToken(AccessTokenEntityInterface $accessToken); + + /** + * Get the access token that the refresh token was originally associated with. + * + * @return AccessTokenEntityInterface + */ + public function getAccessToken(); +} diff --git a/src/Entities/ScopeEntityInterface.php b/src/Entities/ScopeEntityInterface.php new file mode 100644 index 0000000..34ef75f --- /dev/null +++ b/src/Entities/ScopeEntityInterface.php @@ -0,0 +1,20 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities; + +interface ScopeEntityInterface extends \JsonSerializable +{ + /** + * Get the scope's identifier. + * + * @return string + */ + public function getIdentifier(); +} diff --git a/src/Entities/TokenInterface.php b/src/Entities/TokenInterface.php new file mode 100644 index 0000000..c842b09 --- /dev/null +++ b/src/Entities/TokenInterface.php @@ -0,0 +1,83 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities; + +interface TokenInterface +{ + /** + * Get the token's identifier. + * + * @return string + */ + public function getIdentifier(); + + /** + * Set the token's identifier. + * + * @param $identifier + */ + public function setIdentifier($identifier); + + /** + * Get the token's expiry date time. + * + * @return \DateTime + */ + public function getExpiryDateTime(); + + /** + * Set the date time when the token expires. + * + * @param \DateTime $dateTime + */ + public function setExpiryDateTime(\DateTime $dateTime); + + /** + * Set the identifier of the user associated with the token. + * + * @param string|int $identifier The identifier of the user + */ + public function setUserIdentifier($identifier); + + /** + * Get the token user's identifier. + * + * @return string|int + */ + public function getUserIdentifier(); + + /** + * Get the client that the token was issued to. + * + * @return ClientEntityInterface + */ + public function getClient(); + + /** + * Set the client that the token was issued to. + * + * @param ClientEntityInterface $client + */ + public function setClient(ClientEntityInterface $client); + + /** + * Associate a scope with the token. + * + * @param ScopeEntityInterface $scope + */ + public function addScope(ScopeEntityInterface $scope); + + /** + * Return an array of scopes associated with the token. + * + * @return ScopeEntityInterface[] + */ + public function getScopes(); +} diff --git a/src/Entities/Traits/AccessTokenTrait.php b/src/Entities/Traits/AccessTokenTrait.php new file mode 100644 index 0000000..741d6c1 --- /dev/null +++ b/src/Entities/Traits/AccessTokenTrait.php @@ -0,0 +1,61 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities\Traits; + +use Lcobucci\JWT\Builder; +use Lcobucci\JWT\Signer\Key; +use Lcobucci\JWT\Signer\Rsa\Sha256; +use League\OAuth2\Server\CryptKey; +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\ScopeEntityInterface; + +trait AccessTokenTrait +{ + /** + * Generate a JWT from the access token + * + * @param CryptKey $privateKey + * + * @return string + */ + public function convertToJWT(CryptKey $privateKey) + { + return (new Builder()) + ->setAudience($this->getClient()->getIdentifier()) + ->setId($this->getIdentifier(), true) + ->setIssuedAt(time()) + ->setNotBefore(time()) + ->setExpiration($this->getExpiryDateTime()->getTimestamp()) + ->setSubject($this->getUserIdentifier()) + ->set('scopes', $this->getScopes()) + ->sign(new Sha256(), new Key($privateKey->getKeyPath(), $privateKey->getPassPhrase())) + ->getToken(); + } + + /** + * @return ClientEntityInterface + */ + abstract public function getClient(); + + /** + * @return \DateTime + */ + abstract public function getExpiryDateTime(); + + /** + * @return string|int + */ + abstract public function getUserIdentifier(); + + /** + * @return ScopeEntityInterface[] + */ + abstract public function getScopes(); +} diff --git a/src/Entities/Traits/AuthCodeTrait.php b/src/Entities/Traits/AuthCodeTrait.php new file mode 100644 index 0000000..5bb9e30 --- /dev/null +++ b/src/Entities/Traits/AuthCodeTrait.php @@ -0,0 +1,34 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities\Traits; + +trait AuthCodeTrait +{ + /** + * @var null|string + */ + protected $redirectUri; + + /** + * @return string + */ + public function getRedirectUri() + { + return $this->redirectUri; + } + + /** + * @param string $uri + */ + public function setRedirectUri($uri) + { + $this->redirectUri = $uri; + } +} diff --git a/src/Entities/Traits/ClientTrait.php b/src/Entities/Traits/ClientTrait.php new file mode 100644 index 0000000..fec76a6 --- /dev/null +++ b/src/Entities/Traits/ClientTrait.php @@ -0,0 +1,46 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities\Traits; + +trait ClientTrait +{ + /** + * @var string + */ + protected $name; + + /** + * @var string|string[] + */ + protected $redirectUri; + + /** + * Get the client's name. + * + * @return string + * @codeCoverageIgnore + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the registered redirect URI (as a string). + * + * Alternatively return an indexed array of redirect URIs. + * + * @return string|string[] + */ + public function getRedirectUri() + { + return $this->redirectUri; + } +} diff --git a/src/Entities/Traits/EntityTrait.php b/src/Entities/Traits/EntityTrait.php new file mode 100644 index 0000000..20c8659 --- /dev/null +++ b/src/Entities/Traits/EntityTrait.php @@ -0,0 +1,34 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities\Traits; + +trait EntityTrait +{ + /* + * @var string + */ + protected $identifier; + + /** + * @return mixed + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * @param mixed $identifier + */ + public function setIdentifier($identifier) + { + $this->identifier = $identifier; + } +} diff --git a/src/Entities/Traits/RefreshTokenTrait.php b/src/Entities/Traits/RefreshTokenTrait.php new file mode 100644 index 0000000..fb9dbc6 --- /dev/null +++ b/src/Entities/Traits/RefreshTokenTrait.php @@ -0,0 +1,61 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities\Traits; + +use League\OAuth2\Server\Entities\AccessTokenEntityInterface; + +trait RefreshTokenTrait +{ + /** + * @var AccessTokenEntityInterface + */ + protected $accessToken; + + /** + * @var \DateTime + */ + protected $expiryDateTime; + + /** + * {@inheritdoc} + */ + public function setAccessToken(AccessTokenEntityInterface $accessToken) + { + $this->accessToken = $accessToken; + } + + /** + * {@inheritdoc} + */ + public function getAccessToken() + { + return $this->accessToken; + } + + /** + * Get the token's expiry date time. + * + * @return \DateTime + */ + public function getExpiryDateTime() + { + return $this->expiryDateTime; + } + + /** + * Set the date time when the token expires. + * + * @param \DateTime $dateTime + */ + public function setExpiryDateTime(\DateTime $dateTime) + { + $this->expiryDateTime = $dateTime; + } +} diff --git a/src/Entities/Traits/TokenEntityTrait.php b/src/Entities/Traits/TokenEntityTrait.php new file mode 100644 index 0000000..0b5608c --- /dev/null +++ b/src/Entities/Traits/TokenEntityTrait.php @@ -0,0 +1,116 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities\Traits; + +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\ScopeEntityInterface; + +trait TokenEntityTrait +{ + /** + * @var ScopeEntityInterface[] + */ + protected $scopes = []; + + /** + * @var \DateTime + */ + protected $expiryDateTime; + + /** + * @var string|int + */ + protected $userIdentifier; + + /** + * @var ClientEntityInterface + */ + protected $client; + + /** + * Associate a scope with the token. + * + * @param ScopeEntityInterface $scope + */ + public function addScope(ScopeEntityInterface $scope) + { + $this->scopes[$scope->getIdentifier()] = $scope; + } + + /** + * Return an array of scopes associated with the token. + * + * @return ScopeEntityInterface[] + */ + public function getScopes() + { + return array_values($this->scopes); + } + + /** + * Get the token's expiry date time. + * + * @return \DateTime + */ + public function getExpiryDateTime() + { + return $this->expiryDateTime; + } + + /** + * Set the date time when the token expires. + * + * @param \DateTime $dateTime + */ + public function setExpiryDateTime(\DateTime $dateTime) + { + $this->expiryDateTime = $dateTime; + } + + /** + * Set the identifier of the user associated with the token. + * + * @param string|int $identifier The identifier of the user + */ + public function setUserIdentifier($identifier) + { + $this->userIdentifier = $identifier; + } + + /** + * Get the token user's identifier. + * + * @return string|int + */ + public function getUserIdentifier() + { + return $this->userIdentifier; + } + + /** + * Get the client that the token was issued to. + * + * @return ClientEntityInterface + */ + public function getClient() + { + return $this->client; + } + + /** + * Set the client that the token was issued to. + * + * @param ClientEntityInterface $client + */ + public function setClient(ClientEntityInterface $client) + { + $this->client = $client; + } +} diff --git a/src/Entities/UserEntityInterface.php b/src/Entities/UserEntityInterface.php new file mode 100644 index 0000000..c71cb9c --- /dev/null +++ b/src/Entities/UserEntityInterface.php @@ -0,0 +1,20 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Entities; + +interface UserEntityInterface +{ + /** + * Return the user's identifier. + * + * @return mixed + */ + public function getIdentifier(); +} diff --git a/src/Exception/OAuthServerException.php b/src/Exception/OAuthServerException.php new file mode 100644 index 0000000..45e03c0 --- /dev/null +++ b/src/Exception/OAuthServerException.php @@ -0,0 +1,296 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Exception; + +use Psr\Http\Message\ResponseInterface; + +class OAuthServerException extends \Exception +{ + /** + * @var int + */ + private $httpStatusCode; + + /** + * @var string + */ + private $errorType; + + /** + * @var null|string + */ + private $hint; + + /** + * @var null|string + */ + private $redirectUri; + + /** + * Throw a new exception. + * + * @param string $message Error message + * @param int $code Error code + * @param string $errorType Error type + * @param int $httpStatusCode HTTP status code to send (default = 400) + * @param null|string $hint A helper hint + * @param null|string $redirectUri A HTTP URI to redirect the user back to + */ + public function __construct($message, $code, $errorType, $httpStatusCode = 400, $hint = null, $redirectUri = null) + { + parent::__construct($message, $code); + $this->httpStatusCode = $httpStatusCode; + $this->errorType = $errorType; + $this->hint = $hint; + $this->redirectUri = $redirectUri; + } + + /** + * Unsupported grant type error. + * + * @return static + */ + public static function unsupportedGrantType() + { + $errorMessage = 'The authorization grant type is not supported by the authorization server.'; + $hint = 'Check the `grant_type` parameter'; + + return new static($errorMessage, 2, 'unsupported_grant_type', 400, $hint); + } + + /** + * Invalid request error. + * + * @param string $parameter The invalid parameter + * @param null|string $hint + * + * @return static + */ + public static function invalidRequest($parameter, $hint = null) + { + $errorMessage = 'The request is missing a required parameter, includes an invalid parameter value, ' . + 'includes a parameter more than once, or is otherwise malformed.'; + $hint = ($hint === null) ? sprintf('Check the `%s` parameter', $parameter) : $hint; + + return new static($errorMessage, 3, 'invalid_request', 400, $hint); + } + + /** + * Invalid client error. + * + * @return static + */ + public static function invalidClient() + { + $errorMessage = 'Client authentication failed'; + + return new static($errorMessage, 4, 'invalid_client', 401); + } + + /** + * Invalid scope error. + * + * @param string $scope The bad scope + * @param null|string $redirectUri A HTTP URI to redirect the user back to + * + * @return static + */ + public static function invalidScope($scope, $redirectUri = null) + { + $errorMessage = 'The requested scope is invalid, unknown, or malformed'; + $hint = sprintf( + 'Check the `%s` scope', + htmlspecialchars($scope, ENT_QUOTES, 'UTF-8', false) + ); + + return new static($errorMessage, 5, 'invalid_scope', 400, $hint, $redirectUri); + } + + /** + * Invalid credentials error. + * + * @return static + */ + public static function invalidCredentials() + { + return new static('The user credentials were incorrect.', 6, 'invalid_credentials', 401); + } + + /** + * Server error. + * + * @param $hint + * + * @return static + * + * @codeCoverageIgnore + */ + public static function serverError($hint) + { + return new static( + 'The authorization server encountered an unexpected condition which prevented it from fulfilling' + . ' the request: ' . $hint, + 7, + 'server_error', + 500 + ); + } + + /** + * Invalid refresh token. + * + * @param null|string $hint + * + * @return static + */ + public static function invalidRefreshToken($hint = null) + { + return new static('The refresh token is invalid.', 8, 'invalid_request', 401, $hint); + } + + /** + * Access denied. + * + * @param null|string $hint + * @param null|string $redirectUri + * + * @return static + */ + public static function accessDenied($hint = null, $redirectUri = null) + { + return new static( + 'The resource owner or authorization server denied the request.', + 9, + 'access_denied', + 401, + $hint, + $redirectUri + ); + } + + /** + * Invalid grant. + * + * @param string $hint + * + * @return static + */ + public static function invalidGrant($hint = '') + { + return new static( + 'The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token ' + . 'is invalid, expired, revoked, does not match the redirection URI used in the authorization request, ' + . 'or was issued to another client.', + 10, + 'invalid_grant', + 400, + $hint + ); + } + + /** + * @return string + */ + public function getErrorType() + { + return $this->errorType; + } + + /** + * Generate a HTTP response. + * + * @param ResponseInterface $response + * @param bool $useFragment True if errors should be in the URI fragment instead of query string + * + * @return ResponseInterface + */ + public function generateHttpResponse(ResponseInterface $response, $useFragment = false) + { + $headers = $this->getHttpHeaders(); + + $payload = [ + 'error' => $this->getErrorType(), + 'message' => $this->getMessage(), + ]; + + if ($this->hint !== null) { + $payload['hint'] = $this->hint; + } + + if ($this->redirectUri !== null) { + if ($useFragment === true) { + $this->redirectUri .= (strstr($this->redirectUri, '#') === false) ? '#' : '&'; + } else { + $this->redirectUri .= (strstr($this->redirectUri, '?') === false) ? '?' : '&'; + } + + return $response->withStatus(302)->withHeader('Location', $this->redirectUri . http_build_query($payload)); + } + + foreach ($headers as $header => $content) { + $response = $response->withHeader($header, $content); + } + + $response->getBody()->write(json_encode($payload)); + + return $response->withStatus($this->getHttpStatusCode()); + } + + /** + * Get all headers that have to be send with the error response. + * + * @return array Array with header values + */ + public function getHttpHeaders() + { + $headers = [ + 'Content-type' => 'application/json', + ]; + + // Add "WWW-Authenticate" header + // + // RFC 6749, section 5.2.: + // "If the client attempted to authenticate via the 'Authorization' + // request header field, the authorization server MUST + // respond with an HTTP 401 (Unauthorized) status code and + // include the "WWW-Authenticate" response header field + // matching the authentication scheme used by the client. + // @codeCoverageIgnoreStart + if ($this->errorType === 'invalid_client') { + $authScheme = 'Basic'; + if (array_key_exists('HTTP_AUTHORIZATION', $_SERVER) !== false + && strpos($_SERVER['HTTP_AUTHORIZATION'], 'Bearer') === 0 + ) { + $authScheme = 'Bearer'; + } + $headers['WWW-Authenticate'] = $authScheme . ' realm="OAuth"'; + } + // @codeCoverageIgnoreEnd + return $headers; + } + + /** + * Returns the HTTP status code to send when the exceptions is output. + * + * @return int + */ + public function getHttpStatusCode() + { + return $this->httpStatusCode; + } + + /** + * @return null|string + */ + public function getHint() + { + return $this->hint; + } +} diff --git a/src/Exception/UniqueTokenIdentifierConstraintViolationException.php b/src/Exception/UniqueTokenIdentifierConstraintViolationException.php new file mode 100644 index 0000000..a67855b --- /dev/null +++ b/src/Exception/UniqueTokenIdentifierConstraintViolationException.php @@ -0,0 +1,20 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Exception; + +class UniqueTokenIdentifierConstraintViolationException extends OAuthServerException +{ + public static function create() + { + $errorMessage = 'Could not create unique access token identifier'; + + return new static($errorMessage, 100, 'access_token_duplicate', 500); + } +} diff --git a/src/Grant/AbstractAuthorizeGrant.php b/src/Grant/AbstractAuthorizeGrant.php new file mode 100644 index 0000000..7f05100 --- /dev/null +++ b/src/Grant/AbstractAuthorizeGrant.php @@ -0,0 +1,29 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Grant; + +abstract class AbstractAuthorizeGrant extends AbstractGrant +{ + /** + * @param string $uri + * @param array $params + * @param string $queryDelimiter + * + * @return string + */ + public function makeRedirectUri($uri, $params = [], $queryDelimiter = '?') + { + $uri .= (strstr($uri, $queryDelimiter) === false) ? $queryDelimiter : '&'; + + return $uri . http_build_query($params); + } +} diff --git a/src/Grant/AbstractGrant.php b/src/Grant/AbstractGrant.php new file mode 100644 index 0000000..3ac98cf --- /dev/null +++ b/src/Grant/AbstractGrant.php @@ -0,0 +1,512 @@ + + * @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\EmitterAwareTrait; +use League\OAuth2\Server\CryptKey; +use League\OAuth2\Server\CryptTrait; +use League\OAuth2\Server\Entities\AccessTokenEntityInterface; +use League\OAuth2\Server\Entities\AuthCodeEntityInterface; +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; +use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; +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\Repositories\UserRepositoryInterface; +use League\OAuth2\Server\RequestEvent; +use League\OAuth2\Server\RequestTypes\AuthorizationRequest; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Abstract grant class. + */ +abstract class AbstractGrant implements GrantTypeInterface +{ + use EmitterAwareTrait, CryptTrait; + + const SCOPE_DELIMITER_STRING = ' '; + + const MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS = 10; + + /** + * @var ClientRepositoryInterface + */ + protected $clientRepository; + + /** + * @var AccessTokenRepositoryInterface + */ + protected $accessTokenRepository; + + /** + * @var ScopeRepositoryInterface + */ + protected $scopeRepository; + + /** + * @var AuthCodeRepositoryInterface + */ + protected $authCodeRepository; + + /** + * @var RefreshTokenRepositoryInterface + */ + protected $refreshTokenRepository; + + /** + * @var UserRepositoryInterface + */ + protected $userRepository; + + /** + * @var \DateInterval + */ + protected $refreshTokenTTL; + + /** + * @var \League\OAuth2\Server\CryptKey + */ + protected $privateKey; + + /** + * @param ClientRepositoryInterface $clientRepository + */ + public function setClientRepository(ClientRepositoryInterface $clientRepository) + { + $this->clientRepository = $clientRepository; + } + + /** + * @param AccessTokenRepositoryInterface $accessTokenRepository + */ + public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository) + { + $this->accessTokenRepository = $accessTokenRepository; + } + + /** + * @param ScopeRepositoryInterface $scopeRepository + */ + public function setScopeRepository(ScopeRepositoryInterface $scopeRepository) + { + $this->scopeRepository = $scopeRepository; + } + + /** + * @param RefreshTokenRepositoryInterface $refreshTokenRepository + */ + public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository) + { + $this->refreshTokenRepository = $refreshTokenRepository; + } + + /** + * @param AuthCodeRepositoryInterface $authCodeRepository + */ + public function setAuthCodeRepository(AuthCodeRepositoryInterface $authCodeRepository) + { + $this->authCodeRepository = $authCodeRepository; + } + + /** + * @param UserRepositoryInterface $userRepository + */ + public function setUserRepository(UserRepositoryInterface $userRepository) + { + $this->userRepository = $userRepository; + } + + /** + * {@inheritdoc} + */ + public function setRefreshTokenTTL(\DateInterval $refreshTokenTTL) + { + $this->refreshTokenTTL = $refreshTokenTTL; + } + + /** + * Set the private key + * + * @param \League\OAuth2\Server\CryptKey $key + */ + public function setPrivateKey(CryptKey $key) + { + $this->privateKey = $key; + } + + /** + * Validate the client. + * + * @param ServerRequestInterface $request + * + * @throws OAuthServerException + * + * @return ClientEntityInterface + */ + protected function validateClient(ServerRequestInterface $request) + { + list($basicAuthUser, $basicAuthPassword) = $this->getBasicAuthCredentials($request); + + $clientId = $this->getRequestParameter('client_id', $request, $basicAuthUser); + if (is_null($clientId)) { + throw OAuthServerException::invalidRequest('client_id'); + } + + // If the client is confidential require the client secret + $clientSecret = $this->getRequestParameter('client_secret', $request, $basicAuthPassword); + + $client = $this->clientRepository->getClientEntity( + $clientId, + $this->getIdentifier(), + $clientSecret, + true + ); + + if ($client instanceof ClientEntityInterface === false) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); + throw OAuthServerException::invalidClient(); + } + + // If a redirect URI is provided ensure it matches what is pre-registered + $redirectUri = $this->getRequestParameter('redirect_uri', $request, null); + if ($redirectUri !== null) { + if ( + is_string($client->getRedirectUri()) + && (strcmp($client->getRedirectUri(), $redirectUri) !== 0) + ) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); + throw OAuthServerException::invalidClient(); + } elseif ( + is_array($client->getRedirectUri()) + && in_array($redirectUri, $client->getRedirectUri()) === false + ) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); + throw OAuthServerException::invalidClient(); + } + } + + return $client; + } + + /** + * Validate scopes in the request. + * + * @param string $scopes + * @param string $redirectUri + * + * @throws OAuthServerException + * + * @return ScopeEntityInterface[] + */ + public function validateScopes( + $scopes, + $redirectUri = null + ) { + $scopesList = array_filter( + explode(self::SCOPE_DELIMITER_STRING, trim($scopes)), + function ($scope) { + return !empty($scope); + } + ); + + $scopes = []; + foreach ($scopesList as $scopeItem) { + $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeItem); + + if ($scope instanceof ScopeEntityInterface === false) { + throw OAuthServerException::invalidScope($scopeItem, $redirectUri); + } + + $scopes[] = $scope; + } + + return $scopes; + } + + /** + * Retrieve request parameter. + * + * @param string $parameter + * @param ServerRequestInterface $request + * @param mixed $default + * + * @return null|string + */ + protected function getRequestParameter($parameter, ServerRequestInterface $request, $default = null) + { + $requestParameters = (array) $request->getParsedBody(); + + return isset($requestParameters[$parameter]) ? $requestParameters[$parameter] : $default; + } + + /** + * Retrieve HTTP Basic Auth credentials with the Authorization header + * of a request. First index of the returned array is the username, + * second is the password (so list() will work). If the header does + * not exist, or is otherwise an invalid HTTP Basic header, return + * [null, null]. + * + * @param ServerRequestInterface $request + * + * @return string[]|null[] + */ + protected function getBasicAuthCredentials(ServerRequestInterface $request) + { + if (!$request->hasHeader('Authorization')) { + return [null, null]; + } + + $header = $request->getHeader('Authorization')[0]; + if (strpos($header, 'Basic ') !== 0) { + return [null, null]; + } + + if (!($decoded = base64_decode(substr($header, 6)))) { + return [null, null]; + } + + if (strpos($decoded, ':') === false) { + return [null, null]; // HTTP Basic header without colon isn't valid + } + + return explode(':', $decoded, 2); + } + + /** + * Retrieve query string parameter. + * + * @param string $parameter + * @param 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 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; + } + + /** + * Retrieve server parameter. + * + * @param string $parameter + * @param ServerRequestInterface $request + * @param mixed $default + * + * @return null|string + */ + protected function getServerParameter($parameter, ServerRequestInterface $request, $default = null) + { + return isset($request->getServerParams()[$parameter]) ? $request->getServerParams()[$parameter] : $default; + } + + /** + * Issue an access token. + * + * @param \DateInterval $accessTokenTTL + * @param ClientEntityInterface $client + * @param string $userIdentifier + * @param ScopeEntityInterface[] $scopes + * + * @throws OAuthServerException + * @throws UniqueTokenIdentifierConstraintViolationException + * + * @return AccessTokenEntityInterface + */ + protected function issueAccessToken( + \DateInterval $accessTokenTTL, + ClientEntityInterface $client, + $userIdentifier, + array $scopes = [] + ) { + $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; + + $accessToken = $this->accessTokenRepository->getNewToken($client, $scopes, $userIdentifier); + $accessToken->setClient($client); + $accessToken->setUserIdentifier($userIdentifier); + $accessToken->setExpiryDateTime((new \DateTime())->add($accessTokenTTL)); + + foreach ($scopes as $scope) { + $accessToken->addScope($scope); + } + + while ($maxGenerationAttempts-- > 0) { + $accessToken->setIdentifier($this->generateUniqueIdentifier()); + try { + $this->accessTokenRepository->persistNewAccessToken($accessToken); + + return $accessToken; + } catch (UniqueTokenIdentifierConstraintViolationException $e) { + if ($maxGenerationAttempts === 0) { + throw $e; + } + } + } + } + + /** + * Issue an auth code. + * + * @param \DateInterval $authCodeTTL + * @param ClientEntityInterface $client + * @param string $userIdentifier + * @param string $redirectUri + * @param ScopeEntityInterface[] $scopes + * + * @throws OAuthServerException + * @throws UniqueTokenIdentifierConstraintViolationException + * + * @return AuthCodeEntityInterface + */ + protected function issueAuthCode( + \DateInterval $authCodeTTL, + ClientEntityInterface $client, + $userIdentifier, + $redirectUri, + array $scopes = [] + ) { + $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; + + $authCode = $this->authCodeRepository->getNewAuthCode(); + $authCode->setExpiryDateTime((new \DateTime())->add($authCodeTTL)); + $authCode->setClient($client); + $authCode->setUserIdentifier($userIdentifier); + $authCode->setRedirectUri($redirectUri); + + foreach ($scopes as $scope) { + $authCode->addScope($scope); + } + + while ($maxGenerationAttempts-- > 0) { + $authCode->setIdentifier($this->generateUniqueIdentifier()); + try { + $this->authCodeRepository->persistNewAuthCode($authCode); + + return $authCode; + } catch (UniqueTokenIdentifierConstraintViolationException $e) { + if ($maxGenerationAttempts === 0) { + throw $e; + } + } + } + } + + /** + * @param AccessTokenEntityInterface $accessToken + * + * @throws OAuthServerException + * @throws UniqueTokenIdentifierConstraintViolationException + * + * @return RefreshTokenEntityInterface + */ + protected function issueRefreshToken(AccessTokenEntityInterface $accessToken) + { + $maxGenerationAttempts = self::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS; + + $refreshToken = $this->refreshTokenRepository->getNewRefreshToken(); + $refreshToken->setExpiryDateTime((new \DateTime())->add($this->refreshTokenTTL)); + $refreshToken->setAccessToken($accessToken); + + while ($maxGenerationAttempts-- > 0) { + $refreshToken->setIdentifier($this->generateUniqueIdentifier()); + try { + $this->refreshTokenRepository->persistNewRefreshToken($refreshToken); + + return $refreshToken; + } catch (UniqueTokenIdentifierConstraintViolationException $e) { + if ($maxGenerationAttempts === 0) { + throw $e; + } + } + } + } + + /** + * Generate a new unique identifier. + * + * @param int $length + * + * @throws OAuthServerException + * + * @return string + */ + protected function generateUniqueIdentifier($length = 40) + { + try { + return bin2hex(random_bytes($length)); + // @codeCoverageIgnoreStart + } catch (\TypeError $e) { + throw OAuthServerException::serverError('An unexpected error has occurred'); + } catch (\Error $e) { + throw OAuthServerException::serverError('An unexpected error has occurred'); + } catch (\Exception $e) { + // If you get this message, the CSPRNG failed hard. + throw OAuthServerException::serverError('Could not generate a random string'); + } + // @codeCoverageIgnoreEnd + } + + /** + * {@inheritdoc} + */ + public function canRespondToAccessTokenRequest(ServerRequestInterface $request) + { + $requestParameters = (array) $request->getParsedBody(); + + return ( + array_key_exists('grant_type', $requestParameters) + && $requestParameters['grant_type'] === $this->getIdentifier() + ); + } + + /** + * {@inheritdoc} + */ + public function canRespondToAuthorizationRequest(ServerRequestInterface $request) + { + return false; + } + + /** + * {@inheritdoc} + */ + public function validateAuthorizationRequest(ServerRequestInterface $request) + { + throw new \LogicException('This grant cannot validate an authorization request'); + } + + /** + * {@inheritdoc} + */ + public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest) + { + throw new \LogicException('This grant cannot complete an authorization request'); + } +} diff --git a/src/Grant/AuthCodeGrant.php b/src/Grant/AuthCodeGrant.php new file mode 100644 index 0000000..a138366 --- /dev/null +++ b/src/Grant/AuthCodeGrant.php @@ -0,0 +1,354 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Grant; + +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; +use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; +use League\OAuth2\Server\RequestEvent; +use League\OAuth2\Server\RequestTypes\AuthorizationRequest; +use League\OAuth2\Server\ResponseTypes\RedirectResponse; +use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; +use Psr\Http\Message\ServerRequestInterface; + +class AuthCodeGrant extends AbstractAuthorizeGrant +{ + /** + * @var \DateInterval + */ + private $authCodeTTL; + + /** + * @var bool + */ + private $enableCodeExchangeProof = false; + + /** + * @param AuthCodeRepositoryInterface $authCodeRepository + * @param RefreshTokenRepositoryInterface $refreshTokenRepository + * @param \DateInterval $authCodeTTL + */ + public function __construct( + AuthCodeRepositoryInterface $authCodeRepository, + RefreshTokenRepositoryInterface $refreshTokenRepository, + \DateInterval $authCodeTTL + ) { + $this->setAuthCodeRepository($authCodeRepository); + $this->setRefreshTokenRepository($refreshTokenRepository); + $this->authCodeTTL = $authCodeTTL; + $this->refreshTokenTTL = new \DateInterval('P1M'); + } + + public function enableCodeExchangeProof() + { + $this->enableCodeExchangeProof = true; + } + + /** + * Respond to an access token request. + * + * @param ServerRequestInterface $request + * @param ResponseTypeInterface $responseType + * @param \DateInterval $accessTokenTTL + * + * @throws OAuthServerException + * + * @return ResponseTypeInterface + */ + public function respondToAccessTokenRequest( + ServerRequestInterface $request, + ResponseTypeInterface $responseType, + \DateInterval $accessTokenTTL + ) { + // Validate request + $client = $this->validateClient($request); + $encryptedAuthCode = $this->getRequestParameter('code', $request, null); + + if ($encryptedAuthCode === null) { + throw OAuthServerException::invalidRequest('code'); + } + + // Validate the authorization code + try { + $authCodePayload = json_decode($this->decrypt($encryptedAuthCode)); + 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'); + } + + // The redirect URI is required in this request + $redirectUri = $this->getRequestParameter('redirect_uri', $request, null); + if (empty($authCodePayload->redirect_uri) === false && $redirectUri === null) { + throw OAuthServerException::invalidRequest('redirect_uri'); + } + + if ($authCodePayload->redirect_uri !== $redirectUri) { + throw OAuthServerException::invalidRequest('redirect_uri', 'Invalid redirect URI'); + } + + $scopes = []; + foreach ($authCodePayload->scopes as $scopeId) { + $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeId); + + if ($scope instanceof ScopeEntityInterface === false) { + // @codeCoverageIgnoreStart + throw OAuthServerException::invalidScope($scopeId); + // @codeCoverageIgnoreEnd + } + + $scopes[] = $scope; + } + + // Finalize the requested scopes + $scopes = $this->scopeRepository->finalizeScopes( + $scopes, + $this->getIdentifier(), + $client, + $authCodePayload->user_id + ); + } catch (\LogicException $e) { + throw OAuthServerException::invalidRequest('code', 'Cannot decrypt the authorization code'); + } + + // Validate code challenge + if ($this->enableCodeExchangeProof === true) { + $codeVerifier = $this->getRequestParameter('code_verifier', $request, null); + if ($codeVerifier === null) { + throw OAuthServerException::invalidRequest('code_verifier'); + } + + switch ($authCodePayload->code_challenge_method) { + case 'plain': + if (hash_equals($codeVerifier, $authCodePayload->code_challenge) === false) { + throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.'); + } + + break; + case 'S256': + if ( + hash_equals( + urlencode(base64_encode(hash('sha256', $codeVerifier))), + $authCodePayload->code_challenge + ) === false + ) { + throw OAuthServerException::invalidGrant('Failed to verify `code_verifier`.'); + } + // @codeCoverageIgnoreStart + break; + default: + throw OAuthServerException::serverError( + sprintf( + 'Unsupported code challenge method `%s`', + $authCodePayload->code_challenge_method + ) + ); + // @codeCoverageIgnoreEnd + } + } + + // Issue and persist access + refresh tokens + $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $authCodePayload->user_id, $scopes); + $refreshToken = $this->issueRefreshToken($accessToken); + + // Inject tokens into response type + $responseType->setAccessToken($accessToken); + $responseType->setRefreshToken($refreshToken); + + // Revoke used auth code + $this->authCodeRepository->revokeAuthCode($authCodePayload->auth_code_id); + + return $responseType; + } + + /** + * Return the grant identifier that can be used in matching up requests. + * + * @return string + */ + public function getIdentifier() + { + return 'authorization_code'; + } + + /** + * {@inheritdoc} + */ + public function canRespondToAuthorizationRequest(ServerRequestInterface $request) + { + return ( + array_key_exists('response_type', $request->getQueryParams()) + && $request->getQueryParams()['response_type'] === 'code' + && isset($request->getQueryParams()['client_id']) + ); + } + + /** + * {@inheritdoc} + */ + public function validateAuthorizationRequest(ServerRequestInterface $request) + { + $clientId = $this->getQueryStringParameter( + 'client_id', + $request, + $this->getServerParameter('PHP_AUTH_USER', $request) + ); + if (is_null($clientId)) { + throw OAuthServerException::invalidRequest('client_id'); + } + + $client = $this->clientRepository->getClientEntity( + $clientId, + $this->getIdentifier(), + null, + false + ); + + if ($client instanceof ClientEntityInterface === false) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); + throw OAuthServerException::invalidClient(); + } + + $redirectUri = $this->getQueryStringParameter('redirect_uri', $request); + if ($redirectUri !== null) { + if ( + is_string($client->getRedirectUri()) + && (strcmp($client->getRedirectUri(), $redirectUri) !== 0) + ) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); + throw OAuthServerException::invalidClient(); + } elseif ( + is_array($client->getRedirectUri()) + && in_array($redirectUri, $client->getRedirectUri()) === false + ) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); + throw OAuthServerException::invalidClient(); + } + } + + $scopes = $this->validateScopes( + $this->getQueryStringParameter('scope', $request), + is_array($client->getRedirectUri()) + ? $client->getRedirectUri()[0] + : $client->getRedirectUri() + ); + + $stateParameter = $this->getQueryStringParameter('state', $request); + + $authorizationRequest = new AuthorizationRequest(); + $authorizationRequest->setGrantTypeId($this->getIdentifier()); + $authorizationRequest->setClient($client); + $authorizationRequest->setRedirectUri($redirectUri); + $authorizationRequest->setState($stateParameter); + $authorizationRequest->setScopes($scopes); + + if ($this->enableCodeExchangeProof === true) { + $codeChallenge = $this->getQueryStringParameter('code_challenge', $request); + if ($codeChallenge === null) { + throw OAuthServerException::invalidRequest('code_challenge'); + } + + if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $codeChallenge) !== 1) { + throw OAuthServerException::invalidRequest( + 'code_challenge', + 'The code_challenge must be between 43 and 128 characters' + ); + } + + $codeChallengeMethod = $this->getQueryStringParameter('code_challenge_method', $request, 'plain'); + if (in_array($codeChallengeMethod, ['plain', 'S256']) === false) { + throw OAuthServerException::invalidRequest( + 'code_challenge_method', + 'Code challenge method must be `plain` or `S256`' + ); + } + + $authorizationRequest->setCodeChallenge($codeChallenge); + $authorizationRequest->setCodeChallengeMethod($codeChallengeMethod); + } + + return $authorizationRequest; + } + + /** + * {@inheritdoc} + */ + public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest) + { + if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) { + throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest'); + } + + $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null) + ? is_array($authorizationRequest->getClient()->getRedirectUri()) + ? $authorizationRequest->getClient()->getRedirectUri()[0] + : $authorizationRequest->getClient()->getRedirectUri() + : $authorizationRequest->getRedirectUri(); + + // The user approved the client, redirect them back with an auth code + if ($authorizationRequest->isAuthorizationApproved() === true) { + $authCode = $this->issueAuthCode( + $this->authCodeTTL, + $authorizationRequest->getClient(), + $authorizationRequest->getUser()->getIdentifier(), + $authorizationRequest->getRedirectUri(), + $authorizationRequest->getScopes() + ); + + $payload = [ + 'client_id' => $authCode->getClient()->getIdentifier(), + 'redirect_uri' => $authCode->getRedirectUri(), + 'auth_code_id' => $authCode->getIdentifier(), + 'scopes' => $authCode->getScopes(), + 'user_id' => $authCode->getUserIdentifier(), + 'expire_time' => (new \DateTime())->add($this->authCodeTTL)->format('U'), + 'code_challenge' => $authorizationRequest->getCodeChallenge(), + 'code_challenge_method' => $authorizationRequest->getCodeChallengeMethod(), + ]; + + $response = new RedirectResponse(); + $response->setRedirectUri( + $this->makeRedirectUri( + $finalRedirectUri, + [ + 'code' => $this->encrypt( + json_encode( + $payload + ) + ), + 'state' => $authorizationRequest->getState(), + ] + ) + ); + + return $response; + } + + // The user denied the client, redirect them back with an error + throw OAuthServerException::accessDenied( + 'The user denied the request', + $this->makeRedirectUri( + $finalRedirectUri, + [ + 'state' => $authorizationRequest->getState(), + ] + ) + ); + } +} diff --git a/src/Grant/ClientCredentialsGrant.php b/src/Grant/ClientCredentialsGrant.php new file mode 100644 index 0000000..b5b968d --- /dev/null +++ b/src/Grant/ClientCredentialsGrant.php @@ -0,0 +1,53 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Grant; + +use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Client credentials grant class. + */ +class ClientCredentialsGrant extends AbstractGrant +{ + /** + * {@inheritdoc} + */ + public function respondToAccessTokenRequest( + ServerRequestInterface $request, + ResponseTypeInterface $responseType, + \DateInterval $accessTokenTTL + ) { + // Validate request + $client = $this->validateClient($request); + $scopes = $this->validateScopes($this->getRequestParameter('scope', $request)); + + // Finalize the requested scopes + $scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client); + + // Issue and persist access token + $accessToken = $this->issueAccessToken($accessTokenTTL, $client, null, $scopes); + + // Inject access token into response type + $responseType->setAccessToken($accessToken); + + return $responseType; + } + + /** + * {@inheritdoc} + */ + public function getIdentifier() + { + return 'client_credentials'; + } +} diff --git a/src/Grant/GrantTypeInterface.php b/src/Grant/GrantTypeInterface.php new file mode 100644 index 0000000..7aa9824 --- /dev/null +++ b/src/Grant/GrantTypeInterface.php @@ -0,0 +1,135 @@ + + * @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\EmitterAwareInterface; +use League\OAuth2\Server\CryptKey; +use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; +use League\OAuth2\Server\Repositories\ClientRepositoryInterface; +use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; +use League\OAuth2\Server\RequestTypes\AuthorizationRequest; +use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Grant type interface. + */ +interface GrantTypeInterface extends EmitterAwareInterface +{ + /** + * Set refresh token TTL. + * + * @param \DateInterval $refreshTokenTTL + */ + public function setRefreshTokenTTL(\DateInterval $refreshTokenTTL); + + /** + * Return the grant identifier that can be used in matching up requests. + * + * @return string + */ + public function getIdentifier(); + + /** + * Respond to an incoming request. + * + * @param ServerRequestInterface $request + * @param ResponseTypeInterface $responseType + * @param \DateInterval $accessTokenTTL + * + * @return ResponseTypeInterface + */ + public function respondToAccessTokenRequest( + ServerRequestInterface $request, + ResponseTypeInterface $responseType, + \DateInterval $accessTokenTTL + ); + + /** + * The grant type should return true if it is able to response to an authorization request + * + * @param ServerRequestInterface $request + * + * @return bool + */ + public function canRespondToAuthorizationRequest(ServerRequestInterface $request); + + /** + * If the grant can respond to an authorization request this method should be called to validate the parameters of + * the request. + * + * If the validation is successful an AuthorizationRequest object will be returned. This object can be safely + * serialized in a user's session, and can be used during user authentication and authorization. + * + * @param ServerRequestInterface $request + * + * @return AuthorizationRequest + */ + public function validateAuthorizationRequest(ServerRequestInterface $request); + + /** + * Once a user has authenticated and authorized the client the grant can complete the authorization request. + * The AuthorizationRequest object's $userId property must be set to the authenticated user and the + * $authorizationApproved property must reflect their desire to authorize or deny the client. + * + * @param AuthorizationRequest $authorizationRequest + * + * @return ResponseTypeInterface + */ + public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest); + + /** + * The grant type should return true if it is able to respond to this request. + * + * For example most grant types will check that the $_POST['grant_type'] property matches it's identifier property. + * + * @param ServerRequestInterface $request + * + * @return bool + */ + public function canRespondToAccessTokenRequest(ServerRequestInterface $request); + + /** + * Set the client repository. + * + * @param ClientRepositoryInterface $clientRepository + */ + public function setClientRepository(ClientRepositoryInterface $clientRepository); + + /** + * Set the access token repository. + * + * @param AccessTokenRepositoryInterface $accessTokenRepository + */ + public function setAccessTokenRepository(AccessTokenRepositoryInterface $accessTokenRepository); + + /** + * Set the scope repository. + * + * @param ScopeRepositoryInterface $scopeRepository + */ + public function setScopeRepository(ScopeRepositoryInterface $scopeRepository); + + /** + * Set the path to the private key. + * + * @param CryptKey $privateKey + */ + public function setPrivateKey(CryptKey $privateKey); + + /** + * Set the encryption key + * + * @param string|null $key + */ + public function setEncryptionKey($key = null); +} diff --git a/src/Grant/ImplicitGrant.php b/src/Grant/ImplicitGrant.php new file mode 100644 index 0000000..2f7ea51 --- /dev/null +++ b/src/Grant/ImplicitGrant.php @@ -0,0 +1,225 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Grant; + +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; +use League\OAuth2\Server\RequestEvent; +use League\OAuth2\Server\RequestTypes\AuthorizationRequest; +use League\OAuth2\Server\ResponseTypes\RedirectResponse; +use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; +use Psr\Http\Message\ServerRequestInterface; + +class ImplicitGrant extends AbstractAuthorizeGrant +{ + /** + * @var \DateInterval + */ + private $accessTokenTTL; + + /** + * @param \DateInterval $accessTokenTTL + */ + public function __construct(\DateInterval $accessTokenTTL) + { + $this->accessTokenTTL = $accessTokenTTL; + } + + /** + * @param \DateInterval $refreshTokenTTL + * + * @throw \LogicException + */ + public function setRefreshTokenTTL(\DateInterval $refreshTokenTTL) + { + throw new \LogicException('The Implicit Grant does not return refresh tokens'); + } + + /** + * @param RefreshTokenRepositoryInterface $refreshTokenRepository + * + * @throw \LogicException + */ + public function setRefreshTokenRepository(RefreshTokenRepositoryInterface $refreshTokenRepository) + { + throw new \LogicException('The Implicit Grant does not return refresh tokens'); + } + + /** + * {@inheritdoc} + */ + public function canRespondToAccessTokenRequest(ServerRequestInterface $request) + { + return false; + } + + /** + * Return the grant identifier that can be used in matching up requests. + * + * @return string + */ + public function getIdentifier() + { + return 'implicit'; + } + + /** + * Respond to an incoming request. + * + * @param ServerRequestInterface $request + * @param ResponseTypeInterface $responseType + * @param \DateInterval $accessTokenTTL + * + * @return ResponseTypeInterface + */ + public function respondToAccessTokenRequest( + ServerRequestInterface $request, + ResponseTypeInterface $responseType, + \DateInterval $accessTokenTTL + ) { + throw new \LogicException('This grant does not used this method'); + } + + /** + * {@inheritdoc} + */ + public function canRespondToAuthorizationRequest(ServerRequestInterface $request) + { + return ( + isset($request->getQueryParams()['response_type']) + && $request->getQueryParams()['response_type'] === 'token' + && isset($request->getQueryParams()['client_id']) + ); + } + + /** + * {@inheritdoc} + */ + public function validateAuthorizationRequest(ServerRequestInterface $request) + { + $clientId = $this->getQueryStringParameter( + 'client_id', + $request, + $this->getServerParameter('PHP_AUTH_USER', $request) + ); + if (is_null($clientId)) { + throw OAuthServerException::invalidRequest('client_id'); + } + + $client = $this->clientRepository->getClientEntity( + $clientId, + $this->getIdentifier(), + null, + false + ); + + if ($client instanceof ClientEntityInterface === false) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); + throw OAuthServerException::invalidClient(); + } + + $redirectUri = $this->getQueryStringParameter('redirect_uri', $request); + if ($redirectUri !== null) { + if ( + is_string($client->getRedirectUri()) + && (strcmp($client->getRedirectUri(), $redirectUri) !== 0) + ) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); + throw OAuthServerException::invalidClient(); + } elseif ( + is_array($client->getRedirectUri()) + && in_array($redirectUri, $client->getRedirectUri()) === false + ) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request)); + throw OAuthServerException::invalidClient(); + } + } + + $scopes = $this->validateScopes( + $this->getQueryStringParameter('scope', $request), + is_array($client->getRedirectUri()) + ? $client->getRedirectUri()[0] + : $client->getRedirectUri() + ); + + // Finalize the requested scopes + $scopes = $this->scopeRepository->finalizeScopes( + $scopes, + $this->getIdentifier(), + $client + ); + + $stateParameter = $this->getQueryStringParameter('state', $request); + + $authorizationRequest = new AuthorizationRequest(); + $authorizationRequest->setGrantTypeId($this->getIdentifier()); + $authorizationRequest->setClient($client); + $authorizationRequest->setRedirectUri($redirectUri); + $authorizationRequest->setState($stateParameter); + $authorizationRequest->setScopes($scopes); + + return $authorizationRequest; + } + + /** + * {@inheritdoc} + */ + public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest) + { + if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) { + throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest'); + } + + $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null) + ? is_array($authorizationRequest->getClient()->getRedirectUri()) + ? $authorizationRequest->getClient()->getRedirectUri()[0] + : $authorizationRequest->getClient()->getRedirectUri() + : $authorizationRequest->getRedirectUri(); + + // The user approved the client, redirect them back with an access token + if ($authorizationRequest->isAuthorizationApproved() === true) { + $accessToken = $this->issueAccessToken( + $this->accessTokenTTL, + $authorizationRequest->getClient(), + $authorizationRequest->getUser()->getIdentifier(), + $authorizationRequest->getScopes() + ); + + $response = new RedirectResponse(); + $response->setRedirectUri( + $this->makeRedirectUri( + $finalRedirectUri, + [ + 'access_token' => (string) $accessToken->convertToJWT($this->privateKey), + 'token_type' => 'Bearer', + 'expires_in' => $accessToken->getExpiryDateTime()->getTimestamp() - (new \DateTime())->getTimestamp(), + 'state' => $authorizationRequest->getState(), + ], + '#' + ) + ); + + return $response; + } + + // The user denied the client, redirect them back with an error + throw OAuthServerException::accessDenied( + 'The user denied the request', + $this->makeRedirectUri( + $finalRedirectUri, + [ + 'state' => $authorizationRequest->getState(), + ] + ) + ); + } +} diff --git a/src/Grant/PasswordGrant.php b/src/Grant/PasswordGrant.php new file mode 100644 index 0000000..3175561 --- /dev/null +++ b/src/Grant/PasswordGrant.php @@ -0,0 +1,111 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Grant; + +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; +use League\OAuth2\Server\Repositories\UserRepositoryInterface; +use League\OAuth2\Server\RequestEvent; +use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Password grant class. + */ +class PasswordGrant extends AbstractGrant +{ + /** + * @param UserRepositoryInterface $userRepository + * @param RefreshTokenRepositoryInterface $refreshTokenRepository + */ + public function __construct( + UserRepositoryInterface $userRepository, + RefreshTokenRepositoryInterface $refreshTokenRepository + ) { + $this->setUserRepository($userRepository); + $this->setRefreshTokenRepository($refreshTokenRepository); + + $this->refreshTokenTTL = new \DateInterval('P1M'); + } + + /** + * {@inheritdoc} + */ + public function respondToAccessTokenRequest( + ServerRequestInterface $request, + ResponseTypeInterface $responseType, + \DateInterval $accessTokenTTL + ) { + // Validate request + $client = $this->validateClient($request); + $scopes = $this->validateScopes($this->getRequestParameter('scope', $request)); + $user = $this->validateUser($request, $client); + + // Finalize the requested scopes + $scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier()); + + // Issue and persist new tokens + $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $scopes); + $refreshToken = $this->issueRefreshToken($accessToken); + + // Inject tokens into response + $responseType->setAccessToken($accessToken); + $responseType->setRefreshToken($refreshToken); + + return $responseType; + } + + /** + * @param ServerRequestInterface $request + * @param ClientEntityInterface $client + * + * @throws OAuthServerException + * + * @return UserEntityInterface + */ + protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client) + { + $username = $this->getRequestParameter('username', $request); + if (is_null($username)) { + throw OAuthServerException::invalidRequest('username'); + } + + $password = $this->getRequestParameter('password', $request); + if (is_null($password)) { + throw OAuthServerException::invalidRequest('password'); + } + + $user = $this->userRepository->getUserEntityByUserCredentials( + $username, + $password, + $this->getIdentifier(), + $client + ); + if ($user instanceof UserEntityInterface === false) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request)); + + throw OAuthServerException::invalidCredentials(); + } + + return $user; + } + + /** + * {@inheritdoc} + */ + public function getIdentifier() + { + return 'password'; + } +} diff --git a/src/Grant/RefreshTokenGrant.php b/src/Grant/RefreshTokenGrant.php new file mode 100644 index 0000000..53dfdf7 --- /dev/null +++ b/src/Grant/RefreshTokenGrant.php @@ -0,0 +1,133 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Grant; + +use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; +use League\OAuth2\Server\RequestEvent; +use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; +use Psr\Http\Message\ServerRequestInterface; + +/** + * Refresh token grant. + */ +class RefreshTokenGrant extends AbstractGrant +{ + /** + * @param RefreshTokenRepositoryInterface $refreshTokenRepository + */ + public function __construct(RefreshTokenRepositoryInterface $refreshTokenRepository) + { + $this->setRefreshTokenRepository($refreshTokenRepository); + + $this->refreshTokenTTL = new \DateInterval('P1M'); + } + + /** + * {@inheritdoc} + */ + public function respondToAccessTokenRequest( + ServerRequestInterface $request, + ResponseTypeInterface $responseType, + \DateInterval $accessTokenTTL + ) { + // Validate request + $client = $this->validateClient($request); + $oldRefreshToken = $this->validateOldRefreshToken($request, $client->getIdentifier()); + $scopes = $this->validateScopes($this->getRequestParameter('scope', $request)); + + // If no new scopes are requested then give the access token the original session scopes + if (count($scopes) === 0) { + $scopes = array_map(function ($scopeId) use ($client) { + $scope = $this->scopeRepository->getScopeEntityByIdentifier($scopeId); + + if ($scope instanceof ScopeEntityInterface === false) { + // @codeCoverageIgnoreStart + throw OAuthServerException::invalidScope($scopeId); + // @codeCoverageIgnoreEnd + } + + 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 + foreach ($scopes as $scope) { + if (in_array($scope->getIdentifier(), $oldRefreshToken['scopes']) === false) { + throw OAuthServerException::invalidScope($scope->getIdentifier()); + } + } + } + + // Expire old tokens + $this->accessTokenRepository->revokeAccessToken($oldRefreshToken['access_token_id']); + $this->refreshTokenRepository->revokeRefreshToken($oldRefreshToken['refresh_token_id']); + + // Issue and persist new tokens + $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $oldRefreshToken['user_id'], $scopes); + $refreshToken = $this->issueRefreshToken($accessToken); + + // Inject tokens into response + $responseType->setAccessToken($accessToken); + $responseType->setRefreshToken($refreshToken); + + return $responseType; + } + + /** + * @param ServerRequestInterface $request + * @param string $clientId + * + * @throws OAuthServerException + * + * @return array + */ + protected function validateOldRefreshToken(ServerRequestInterface $request, $clientId) + { + $encryptedRefreshToken = $this->getRequestParameter('refresh_token', $request); + if (is_null($encryptedRefreshToken)) { + throw OAuthServerException::invalidRequest('refresh_token'); + } + + // Validate refresh token + try { + $refreshToken = $this->decrypt($encryptedRefreshToken); + } catch (\Exception $e) { + throw OAuthServerException::invalidRefreshToken('Cannot decrypt the refresh token'); + } + + $refreshTokenData = json_decode($refreshToken, true); + if ($refreshTokenData['client_id'] !== $clientId) { + $this->getEmitter()->emit(new RequestEvent(RequestEvent::REFRESH_TOKEN_CLIENT_FAILED, $request)); + throw OAuthServerException::invalidRefreshToken('Token is not linked to client'); + } + + if ($refreshTokenData['expire_time'] < time()) { + throw OAuthServerException::invalidRefreshToken('Token has expired'); + } + + if ($this->refreshTokenRepository->isRefreshTokenRevoked($refreshTokenData['refresh_token_id']) === true) { + throw OAuthServerException::invalidRefreshToken('Token has been revoked'); + } + + return $refreshTokenData; + } + + /** + * {@inheritdoc} + */ + public function getIdentifier() + { + return 'refresh_token'; + } +} diff --git a/src/Middleware/AuthorizationServerMiddleware.php b/src/Middleware/AuthorizationServerMiddleware.php new file mode 100644 index 0000000..a5f102f --- /dev/null +++ b/src/Middleware/AuthorizationServerMiddleware.php @@ -0,0 +1,55 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Middleware; + +use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\Exception\OAuthServerException; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +class AuthorizationServerMiddleware +{ + /** + * @var AuthorizationServer + */ + private $server; + + /** + * @param AuthorizationServer $server + */ + public function __construct(AuthorizationServer $server) + { + $this->server = $server; + } + + /** + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param callable $next + * + * @return ResponseInterface + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next) + { + try { + $response = $this->server->respondToAccessTokenRequest($request, $response); + } catch (OAuthServerException $exception) { + return $exception->generateHttpResponse($response); + // @codeCoverageIgnoreStart + } catch (\Exception $exception) { + return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) + ->generateHttpResponse($response); + // @codeCoverageIgnoreEnd + } + + // Pass the request and response on to the next responder in the chain + return $next($request, $response); + } +} diff --git a/src/Middleware/ResourceServerMiddleware.php b/src/Middleware/ResourceServerMiddleware.php new file mode 100644 index 0000000..56d28ae --- /dev/null +++ b/src/Middleware/ResourceServerMiddleware.php @@ -0,0 +1,55 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Middleware; + +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\ResourceServer; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; + +class ResourceServerMiddleware +{ + /** + * @var ResourceServer + */ + private $server; + + /** + * @param ResourceServer $server + */ + public function __construct(ResourceServer $server) + { + $this->server = $server; + } + + /** + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @param callable $next + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next) + { + try { + $request = $this->server->validateAuthenticatedRequest($request); + } catch (OAuthServerException $exception) { + return $exception->generateHttpResponse($response); + // @codeCoverageIgnoreStart + } catch (\Exception $exception) { + return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) + ->generateHttpResponse($response); + // @codeCoverageIgnoreEnd + } + + // Pass the request and response on to the next responder in the chain + return $next($request, $response); + } +} diff --git a/src/Repositories/AccessTokenRepositoryInterface.php b/src/Repositories/AccessTokenRepositoryInterface.php new file mode 100644 index 0000000..72ddf1f --- /dev/null +++ b/src/Repositories/AccessTokenRepositoryInterface.php @@ -0,0 +1,57 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Repositories; + +use League\OAuth2\Server\Entities\AccessTokenEntityInterface; +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; + +/** + * Access token interface. + */ +interface AccessTokenRepositoryInterface extends RepositoryInterface +{ + /** + * Create a new access token + * + * @param ClientEntityInterface $clientEntity + * @param ScopeEntityInterface[] $scopes + * @param mixed $userIdentifier + * + * @return AccessTokenEntityInterface + */ + public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null); + + /** + * Persists a new access token to permanent storage. + * + * @param AccessTokenEntityInterface $accessTokenEntity + * + * @throws UniqueTokenIdentifierConstraintViolationException + */ + public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity); + + /** + * Revoke an access token. + * + * @param string $tokenId + */ + public function revokeAccessToken($tokenId); + + /** + * Check if the access token has been revoked. + * + * @param string $tokenId + * + * @return bool Return true if this token has been revoked + */ + public function isAccessTokenRevoked($tokenId); +} diff --git a/src/Repositories/AuthCodeRepositoryInterface.php b/src/Repositories/AuthCodeRepositoryInterface.php new file mode 100644 index 0000000..2dc285b --- /dev/null +++ b/src/Repositories/AuthCodeRepositoryInterface.php @@ -0,0 +1,51 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Repositories; + +use League\OAuth2\Server\Entities\AuthCodeEntityInterface; +use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; + +/** + * Auth code storage interface. + */ +interface AuthCodeRepositoryInterface extends RepositoryInterface +{ + /** + * Creates a new AuthCode + * + * @return AuthCodeEntityInterface + */ + public function getNewAuthCode(); + + /** + * Persists a new auth code to permanent storage. + * + * @param AuthCodeEntityInterface $authCodeEntity + * + * @throws UniqueTokenIdentifierConstraintViolationException + */ + public function persistNewAuthCode(AuthCodeEntityInterface $authCodeEntity); + + /** + * Revoke an auth code. + * + * @param string $codeId + */ + public function revokeAuthCode($codeId); + + /** + * Check if the auth code has been revoked. + * + * @param string $codeId + * + * @return bool Return true if this code has been revoked + */ + public function isAuthCodeRevoked($codeId); +} diff --git a/src/Repositories/ClientRepositoryInterface.php b/src/Repositories/ClientRepositoryInterface.php new file mode 100644 index 0000000..34adf56 --- /dev/null +++ b/src/Repositories/ClientRepositoryInterface.php @@ -0,0 +1,31 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Repositories; + +use League\OAuth2\Server\Entities\ClientEntityInterface; + +/** + * Client storage interface. + */ +interface ClientRepositoryInterface extends RepositoryInterface +{ + /** + * Get a client. + * + * @param string $clientIdentifier The client's identifier + * @param string $grantType The grant type used + * @param null|string $clientSecret The client's secret (if sent) + * @param bool $mustValidateSecret If true the client must attempt to validate the secret if the client + * is confidential + * + * @return ClientEntityInterface + */ + public function getClientEntity($clientIdentifier, $grantType, $clientSecret = null, $mustValidateSecret = true); +} diff --git a/src/Repositories/RefreshTokenRepositoryInterface.php b/src/Repositories/RefreshTokenRepositoryInterface.php new file mode 100644 index 0000000..0c0697b --- /dev/null +++ b/src/Repositories/RefreshTokenRepositoryInterface.php @@ -0,0 +1,51 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Repositories; + +use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; +use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; + +/** + * Refresh token interface. + */ +interface RefreshTokenRepositoryInterface extends RepositoryInterface +{ + /** + * Creates a new refresh token + * + * @return RefreshTokenEntityInterface + */ + public function getNewRefreshToken(); + + /** + * Create a new refresh token_name. + * + * @param RefreshTokenEntityInterface $refreshTokenEntity + * + * @throws UniqueTokenIdentifierConstraintViolationException + */ + public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity); + + /** + * Revoke the refresh token. + * + * @param string $tokenId + */ + public function revokeRefreshToken($tokenId); + + /** + * Check if the refresh token has been revoked. + * + * @param string $tokenId + * + * @return bool Return true if this token has been revoked + */ + public function isRefreshTokenRevoked($tokenId); +} diff --git a/src/Repositories/RepositoryInterface.php b/src/Repositories/RepositoryInterface.php new file mode 100644 index 0000000..9c27b4b --- /dev/null +++ b/src/Repositories/RepositoryInterface.php @@ -0,0 +1,17 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Repositories; + +/** + * Repository interface. + */ +interface RepositoryInterface +{ +} diff --git a/src/Repositories/ScopeRepositoryInterface.php b/src/Repositories/ScopeRepositoryInterface.php new file mode 100644 index 0000000..52db05d --- /dev/null +++ b/src/Repositories/ScopeRepositoryInterface.php @@ -0,0 +1,46 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Repositories; + +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\ScopeEntityInterface; + +/** + * Scope interface. + */ +interface ScopeRepositoryInterface extends RepositoryInterface +{ + /** + * Return information about a scope. + * + * @param string $identifier The scope identifier + * + * @return ScopeEntityInterface + */ + public function getScopeEntityByIdentifier($identifier); + + /** + * Given a client, grant type and optional user identifier validate the set of scopes requested are valid and optionally + * append additional scopes or remove requested scopes. + * + * @param ScopeEntityInterface[] $scopes + * @param string $grantType + * @param ClientEntityInterface $clientEntity + * @param null|string $userIdentifier + * + * @return ScopeEntityInterface[] + */ + public function finalizeScopes( + array $scopes, + $grantType, + ClientEntityInterface $clientEntity, + $userIdentifier = null + ); +} diff --git a/src/Repositories/UserRepositoryInterface.php b/src/Repositories/UserRepositoryInterface.php new file mode 100644 index 0000000..0a9efef --- /dev/null +++ b/src/Repositories/UserRepositoryInterface.php @@ -0,0 +1,33 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\Repositories; + +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; + +interface UserRepositoryInterface extends RepositoryInterface +{ + /** + * Get a user entity. + * + * @param string $username + * @param string $password + * @param string $grantType The grant type used + * @param ClientEntityInterface $clientEntity + * + * @return UserEntityInterface + */ + public function getUserEntityByUserCredentials( + $username, + $password, + $grantType, + ClientEntityInterface $clientEntity + ); +} diff --git a/src/RequestEvent.php b/src/RequestEvent.php new file mode 100644 index 0000000..1558e11 --- /dev/null +++ b/src/RequestEvent.php @@ -0,0 +1,46 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server; + +use League\Event\Event; +use Psr\Http\Message\ServerRequestInterface; + +class RequestEvent extends Event +{ + const CLIENT_AUTHENTICATION_FAILED = 'client.authentication.failed'; + const USER_AUTHENTICATION_FAILED = 'user.authentication.failed'; + const REFRESH_TOKEN_CLIENT_FAILED = 'refresh_token.client.failed'; + + /** + * @var ServerRequestInterface + */ + private $request; + + /** + * RequestEvent constructor. + * + * @param string $name + * @param ServerRequestInterface $request + */ + public function __construct($name, ServerRequestInterface $request) + { + parent::__construct($name); + $this->request = $request; + } + + /** + * @return ServerRequestInterface + * @codeCoverageIgnore + */ + public function getRequest() + { + return $this->request; + } +} diff --git a/src/RequestTypes/AuthorizationRequest.php b/src/RequestTypes/AuthorizationRequest.php new file mode 100644 index 0000000..41bfb50 --- /dev/null +++ b/src/RequestTypes/AuthorizationRequest.php @@ -0,0 +1,224 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\RequestTypes; + +use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Entities\UserEntityInterface; + +class AuthorizationRequest +{ + /** + * The grant type identifier + * + * @var string + */ + protected $grantTypeId; + + /** + * The client identifier + * + * @var ClientEntityInterface + */ + protected $client; + + /** + * The user identifier + * + * @var UserEntityInterface + */ + protected $user; + + /** + * An array of scope identifiers + * + * @var ScopeEntityInterface[] + */ + protected $scopes = []; + + /** + * Has the user authorized the authorization request + * + * @var bool + */ + protected $authorizationApproved = false; + + /** + * The redirect URI used in the request + * + * @var string + */ + protected $redirectUri; + + /** + * The state parameter on the authorization request + * + * @var string + */ + protected $state; + + /** + * The code challenge (if provided) + * + * @var string + */ + protected $codeChallenge; + + /** + * The code challenge method (if provided) + * + * @var string + */ + protected $codeChallengeMethod; + + /** + * @return string + */ + public function getGrantTypeId() + { + return $this->grantTypeId; + } + + /** + * @param string $grantTypeId + */ + public function setGrantTypeId($grantTypeId) + { + $this->grantTypeId = $grantTypeId; + } + + /** + * @return ClientEntityInterface + */ + public function getClient() + { + return $this->client; + } + + /** + * @param ClientEntityInterface $client + */ + public function setClient(ClientEntityInterface $client) + { + $this->client = $client; + } + + /** + * @return UserEntityInterface + */ + public function getUser() + { + return $this->user; + } + + /** + * @param UserEntityInterface $user + */ + public function setUser(UserEntityInterface $user) + { + $this->user = $user; + } + + /** + * @return ScopeEntityInterface[] + */ + public function getScopes() + { + return $this->scopes; + } + + /** + * @param ScopeEntityInterface[] $scopes + */ + public function setScopes(array $scopes) + { + $this->scopes = $scopes; + } + + /** + * @return bool + */ + public function isAuthorizationApproved() + { + return $this->authorizationApproved; + } + + /** + * @param bool $authorizationApproved + */ + public function setAuthorizationApproved($authorizationApproved) + { + $this->authorizationApproved = $authorizationApproved; + } + + /** + * @return string + */ + public function getRedirectUri() + { + return $this->redirectUri; + } + + /** + * @param string $redirectUri + */ + public function setRedirectUri($redirectUri) + { + $this->redirectUri = $redirectUri; + } + + /** + * @return string + */ + public function getState() + { + return $this->state; + } + + /** + * @param string $state + */ + public function setState($state) + { + $this->state = $state; + } + + /** + * @return string + */ + public function getCodeChallenge() + { + return $this->codeChallenge; + } + + /** + * @param string $codeChallenge + */ + public function setCodeChallenge($codeChallenge) + { + $this->codeChallenge = $codeChallenge; + } + + /** + * @return string + */ + public function getCodeChallengeMethod() + { + return $this->codeChallengeMethod; + } + + /** + * @param string $codeChallengeMethod + */ + public function setCodeChallengeMethod($codeChallengeMethod) + { + $this->codeChallengeMethod = $codeChallengeMethod; + } +} diff --git a/src/ResourceServer.php b/src/ResourceServer.php new file mode 100644 index 0000000..5e9c13f --- /dev/null +++ b/src/ResourceServer.php @@ -0,0 +1,84 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server; + +use League\OAuth2\Server\AuthorizationValidators\AuthorizationValidatorInterface; +use League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator; +use League\OAuth2\Server\Exception\OAuthServerException; +use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; +use Psr\Http\Message\ServerRequestInterface; + +class ResourceServer +{ + /** + * @var AccessTokenRepositoryInterface + */ + private $accessTokenRepository; + + /** + * @var CryptKey + */ + private $publicKey; + + /** + * @var null|AuthorizationValidatorInterface + */ + private $authorizationValidator; + + /** + * New server instance. + * + * @param AccessTokenRepositoryInterface $accessTokenRepository + * @param CryptKey|string $publicKey + * @param null|AuthorizationValidatorInterface $authorizationValidator + */ + public function __construct( + AccessTokenRepositoryInterface $accessTokenRepository, + $publicKey, + AuthorizationValidatorInterface $authorizationValidator = null + ) { + $this->accessTokenRepository = $accessTokenRepository; + + if ($publicKey instanceof CryptKey === false) { + $publicKey = new CryptKey($publicKey); + } + $this->publicKey = $publicKey; + + $this->authorizationValidator = $authorizationValidator; + } + + /** + * @return AuthorizationValidatorInterface + */ + protected function getAuthorizationValidator() + { + if ($this->authorizationValidator instanceof AuthorizationValidatorInterface === false) { + $this->authorizationValidator = new BearerTokenValidator($this->accessTokenRepository); + } + + $this->authorizationValidator->setPublicKey($this->publicKey); + + return $this->authorizationValidator; + } + + /** + * Determine the access token validity. + * + * @param ServerRequestInterface $request + * + * @throws OAuthServerException + * + * @return ServerRequestInterface + */ + public function validateAuthenticatedRequest(ServerRequestInterface $request) + { + return $this->getAuthorizationValidator()->validateAuthorization($request); + } +} diff --git a/src/ResponseTypes/AbstractResponseType.php b/src/ResponseTypes/AbstractResponseType.php new file mode 100644 index 0000000..0c256f1 --- /dev/null +++ b/src/ResponseTypes/AbstractResponseType.php @@ -0,0 +1,64 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\ResponseTypes; + +use League\OAuth2\Server\CryptKey; +use League\OAuth2\Server\CryptTrait; +use League\OAuth2\Server\Entities\AccessTokenEntityInterface; +use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; + +abstract class AbstractResponseType implements ResponseTypeInterface +{ + use CryptTrait; + + /** + * @var AccessTokenEntityInterface + */ + protected $accessToken; + + /** + * @var RefreshTokenEntityInterface + */ + protected $refreshToken; + + /** + * @var CryptKey + */ + protected $privateKey; + + /** + * {@inheritdoc} + */ + public function setAccessToken(AccessTokenEntityInterface $accessToken) + { + $this->accessToken = $accessToken; + } + + /** + * {@inheritdoc} + */ + public function setRefreshToken(RefreshTokenEntityInterface $refreshToken) + { + $this->refreshToken = $refreshToken; + } + + /** + * Set the private key + * + * @param \League\OAuth2\Server\CryptKey $key + */ + public function setPrivateKey(CryptKey $key) + { + $this->privateKey = $key; + } + +} diff --git a/src/ResponseTypes/BearerTokenResponse.php b/src/ResponseTypes/BearerTokenResponse.php new file mode 100644 index 0000000..a57573a --- /dev/null +++ b/src/ResponseTypes/BearerTokenResponse.php @@ -0,0 +1,78 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\ResponseTypes; + +use League\OAuth2\Server\Entities\AccessTokenEntityInterface; +use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; +use Psr\Http\Message\ResponseInterface; + +class BearerTokenResponse extends AbstractResponseType +{ + /** + * {@inheritdoc} + */ + public function generateHttpResponse(ResponseInterface $response) + { + $expireDateTime = $this->accessToken->getExpiryDateTime()->getTimestamp(); + + $jwtAccessToken = $this->accessToken->convertToJWT($this->privateKey); + + $responseParams = [ + 'token_type' => 'Bearer', + 'expires_in' => $expireDateTime - (new \DateTime())->getTimestamp(), + 'access_token' => (string) $jwtAccessToken, + ]; + + if ($this->refreshToken instanceof RefreshTokenEntityInterface) { + $refreshToken = $this->encrypt( + json_encode( + [ + 'client_id' => $this->accessToken->getClient()->getIdentifier(), + 'refresh_token_id' => $this->refreshToken->getIdentifier(), + 'access_token_id' => $this->accessToken->getIdentifier(), + 'scopes' => $this->accessToken->getScopes(), + 'user_id' => $this->accessToken->getUserIdentifier(), + 'expire_time' => $this->refreshToken->getExpiryDateTime()->getTimestamp(), + ] + ) + ); + + $responseParams['refresh_token'] = $refreshToken; + } + + $responseParams = array_merge($this->getExtraParams($this->accessToken), $responseParams); + + $response = $response + ->withStatus(200) + ->withHeader('pragma', 'no-cache') + ->withHeader('cache-control', 'no-store') + ->withHeader('content-type', 'application/json; charset=UTF-8'); + + $response->getBody()->write(json_encode($responseParams)); + + return $response; + } + + /** + * Add custom fields to your Bearer Token response here, then override + * AuthorizationServer::getResponseType() to pull in your version of + * this class rather than the default. + * + * @param AccessTokenEntityInterface $accessToken + * + * @return array + */ + protected function getExtraParams(AccessTokenEntityInterface $accessToken) + { + return []; + } +} diff --git a/src/ResponseTypes/RedirectResponse.php b/src/ResponseTypes/RedirectResponse.php new file mode 100644 index 0000000..e463914 --- /dev/null +++ b/src/ResponseTypes/RedirectResponse.php @@ -0,0 +1,40 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\ResponseTypes; + +use Psr\Http\Message\ResponseInterface; + +class RedirectResponse extends AbstractResponseType +{ + /** + * @var string + */ + private $redirectUri; + + /** + * @param string $redirectUri + */ + public function setRedirectUri($redirectUri) + { + $this->redirectUri = $redirectUri; + } + + /** + * @param ResponseInterface $response + * + * @return ResponseInterface + */ + public function generateHttpResponse(ResponseInterface $response) + { + return $response->withStatus(302)->withHeader('Location', $this->redirectUri); + } +} diff --git a/src/ResponseTypes/ResponseTypeInterface.php b/src/ResponseTypes/ResponseTypeInterface.php new file mode 100644 index 0000000..8ac20b8 --- /dev/null +++ b/src/ResponseTypes/ResponseTypeInterface.php @@ -0,0 +1,43 @@ + + * @copyright Copyright (c) Alex Bilbie + * @license http://mit-license.org/ + * + * @link https://github.com/thephpleague/oauth2-server + */ + +namespace League\OAuth2\Server\ResponseTypes; + +use League\OAuth2\Server\Entities\AccessTokenEntityInterface; +use League\OAuth2\Server\Entities\RefreshTokenEntityInterface; +use Psr\Http\Message\ResponseInterface; + +interface ResponseTypeInterface +{ + /** + * @param AccessTokenEntityInterface $accessToken + */ + public function setAccessToken(AccessTokenEntityInterface $accessToken); + + /** + * @param RefreshTokenEntityInterface $refreshToken + */ + public function setRefreshToken(RefreshTokenEntityInterface $refreshToken); + + /** + * @param ResponseInterface $response + * + * @return ResponseInterface + */ + public function generateHttpResponse(ResponseInterface $response); + + /** + * Set the encryption key + * + * @param string|null $key + */ + public function setEncryptionKey($key = null); +} diff --git a/tests/AuthorizationServerTest.php b/tests/AuthorizationServerTest.php new file mode 100644 index 0000000..91ca9e4 --- /dev/null +++ b/tests/AuthorizationServerTest.php @@ -0,0 +1,208 @@ +getMockBuilder(ClientRepositoryInterface::class)->getMock(), + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/Stubs/private.key', + base64_encode(random_bytes(36)), + new StubResponseType() + ); + + $server->enableGrantType(new ClientCredentialsGrant(), new \DateInterval('PT1M')); + + try { + $server->respondToAccessTokenRequest(ServerRequestFactory::fromGlobals(), new Response); + } catch (OAuthServerException $e) { + $this->assertEquals('unsupported_grant_type', $e->getErrorType()); + $this->assertEquals(400, $e->getHttpStatusCode()); + } + } + + public function testRespondToRequest() + { + $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepository->method('getClientEntity')->willReturn(new ClientEntity()); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + + $server = new AuthorizationServer( + $clientRepository, + $accessTokenRepositoryMock, + $scopeRepositoryMock, + 'file://' . __DIR__ . '/Stubs/private.key', + base64_encode(random_bytes(36)), + new StubResponseType() + ); + + $server->enableGrantType(new ClientCredentialsGrant(), new \DateInterval('PT1M')); + + $_POST['grant_type'] = 'client_credentials'; + $_POST['client_id'] = 'foo'; + $_POST['client_secret'] = 'bar'; + $response = $server->respondToAccessTokenRequest(ServerRequestFactory::fromGlobals(), new Response); + $this->assertEquals(200, $response->getStatusCode()); + } + + public function testGetResponseType() + { + $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + + $server = new AuthorizationServer( + $clientRepository, + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/Stubs/private.key', + 'file://' . __DIR__ . '/Stubs/public.key' + ); + + $abstractGrantReflection = new \ReflectionClass($server); + $method = $abstractGrantReflection->getMethod('getResponseType'); + $method->setAccessible(true); + + $this->assertTrue($method->invoke($server) instanceof BearerTokenResponse); + } + + public function testCompleteAuthorizationRequest() + { + $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + + $server = new AuthorizationServer( + $clientRepository, + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/Stubs/private.key', + 'file://' . __DIR__ . '/Stubs/public.key' + ); + + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $grant = new AuthCodeGrant( + $authCodeRepository, + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + + $server->enableGrantType($grant); + + $authRequest = new AuthorizationRequest(); + $authRequest->setAuthorizationApproved(true); + $authRequest->setClient(new ClientEntity()); + $authRequest->setGrantTypeId('authorization_code'); + $authRequest->setUser(new UserEntity()); + + $this->assertTrue( + $server->completeAuthorizationRequest($authRequest, new Response) instanceof ResponseInterface + ); + } + + public function testValidateAuthorizationRequest() + { + $client = new ClientEntity(); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + + $server = new AuthorizationServer( + $clientRepositoryMock, + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/Stubs/private.key', + 'file://' . __DIR__ . '/Stubs/public.key' + ); + $server->enableGrantType($grant); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + 'client_id' => 'foo', + ] + ); + + $this->assertTrue($server->validateAuthorizationRequest($request) instanceof AuthorizationRequest); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 2 + */ + public function testValidateAuthorizationRequestUnregistered() + { + $server = new AuthorizationServer( + $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(), + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/Stubs/private.key', + 'file://' . __DIR__ . '/Stubs/public.key' + ); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + 'client_id' => 'foo', + ] + ); + + $server->validateAuthorizationRequest($request); + } +} diff --git a/tests/Bootstrap.php b/tests/Bootstrap.php new file mode 100644 index 0000000..b02cb7b --- /dev/null +++ b/tests/Bootstrap.php @@ -0,0 +1,11 @@ + wget http://getcomposer.org/composer.phar +> php composer.phar install +MSG; + + exit($message); +} diff --git a/tests/CryptKeyTest.php b/tests/CryptKeyTest.php new file mode 100644 index 0000000..c7f7f4a --- /dev/null +++ b/tests/CryptKeyTest.php @@ -0,0 +1,36 @@ +assertEquals('file://' . $keyFile, $key->getKeyPath()); + $this->assertEquals('secret', $key->getPassPhrase()); + } + + public function testKeyFileCreation() + { + $keyContent = file_get_contents(__DIR__ . '/Stubs/public.key'); + $key = new CryptKey($keyContent); + + $this->assertEquals( + 'file://' . sys_get_temp_dir() . '/' . sha1($keyContent) . '.key', + $key->getKeyPath() + ); + } +} diff --git a/tests/CryptTraitTest.php b/tests/CryptTraitTest.php new file mode 100644 index 0000000..8c7d264 --- /dev/null +++ b/tests/CryptTraitTest.php @@ -0,0 +1,29 @@ +cryptStub = new CryptTraitStub; + } + + public function testEncryptDecrypt() + { + $payload = 'alex loves whisky'; + $encrypted = $this->cryptStub->doEncrypt($payload); + $plainText = $this->cryptStub->doDecrypt($encrypted); + + $this->assertNotEquals($payload, $encrypted); + $this->assertEquals($payload, $plainText); + } +} diff --git a/tests/Grant/AbstractGrantTest.php b/tests/Grant/AbstractGrantTest.php new file mode 100644 index 0000000..542c78d --- /dev/null +++ b/tests/Grant/AbstractGrantTest.php @@ -0,0 +1,496 @@ +getMockForAbstractClass(AbstractGrant::class); + $grantMock->setEmitter(new Emitter()); + } + + public function testHttpBasicWithPassword() + { + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $abstractGrantReflection = new \ReflectionClass($grantMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withHeader('Authorization', 'Basic ' . base64_encode('Open:Sesame')); + $basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials'); + $basicAuthMethod->setAccessible(true); + + $this->assertSame(['Open', 'Sesame'], $basicAuthMethod->invoke($grantMock, $serverRequest)); + } + + public function testHttpBasicNoPassword() + { + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $abstractGrantReflection = new \ReflectionClass($grantMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withHeader('Authorization', 'Basic ' . base64_encode('Open:')); + $basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials'); + $basicAuthMethod->setAccessible(true); + + $this->assertSame(['Open', ''], $basicAuthMethod->invoke($grantMock, $serverRequest)); + } + + public function testHttpBasicNotBasic() + { + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $abstractGrantReflection = new \ReflectionClass($grantMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withHeader('Authorization', 'Foo ' . base64_encode('Open:Sesame')); + $basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials'); + $basicAuthMethod->setAccessible(true); + + $this->assertSame([null, null], $basicAuthMethod->invoke($grantMock, $serverRequest)); + } + + public function testHttpBasicNotBase64() + { + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $abstractGrantReflection = new \ReflectionClass($grantMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withHeader('Authorization', 'Basic ||'); + $basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials'); + $basicAuthMethod->setAccessible(true); + + $this->assertSame([null, null], $basicAuthMethod->invoke($grantMock, $serverRequest)); + } + + public function testHttpBasicNoColon() + { + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $abstractGrantReflection = new \ReflectionClass($grantMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withHeader('Authorization', 'Basic ' . base64_encode('OpenSesame')); + $basicAuthMethod = $abstractGrantReflection->getMethod('getBasicAuthCredentials'); + $basicAuthMethod->setAccessible(true); + + $this->assertSame([null, null], $basicAuthMethod->invoke($grantMock, $serverRequest)); + } + + 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); + + $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 testValidateClientConfidential() + { + $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); + + $abstractGrantReflection = new \ReflectionClass($grantMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'redirect_uri' => 'http://foo/bar', + ] + ); + $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); + $validateClientMethod->setAccessible(true); + + $result = $validateClientMethod->invoke($grantMock, $serverRequest, true, true); + $this->assertEquals($client, $result); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + */ + public function testValidateClientMissingClientId() + { + $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); + + $abstractGrantReflection = new \ReflectionClass($grantMock); + + $serverRequest = new ServerRequest(); + $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); + $validateClientMethod->setAccessible(true); + + $validateClientMethod->invoke($grantMock, $serverRequest, true, true); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + */ + public function testValidateClientMissingClientSecret() + { + $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', + ]); + + $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); + $validateClientMethod->setAccessible(true); + + $validateClientMethod->invoke($grantMock, $serverRequest, true, true); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + */ + public function testValidateClientInvalidClientSecret() + { + $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', + '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 testValidateClientInvalidRedirectUriArray() + { + $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', + 'client_secret' => 'bar', + ]); + + $validateClientMethod = $abstractGrantReflection->getMethod('validateClient'); + $validateClientMethod->setAccessible(true); + + $validateClientMethod->invoke($grantMock, $serverRequest, true); + } + + public function testCanRespondToRequest() + { + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->method('getIdentifier')->willReturn('foobar'); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody([ + 'grant_type' => 'foobar', + ]); + + $this->assertTrue($grantMock->canRespondToAccessTokenRequest($serverRequest)); + } + + public function testIssueRefreshToken() + { + $refreshTokenRepoMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepoMock + ->expects($this->once()) + ->method('getNewRefreshToken') + ->willReturn(new RefreshTokenEntity()); + + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setRefreshTokenTTL(new \DateInterval('PT1M')); + $grantMock->setRefreshTokenRepository($refreshTokenRepoMock); + + $abstractGrantReflection = new \ReflectionClass($grantMock); + $issueRefreshTokenMethod = $abstractGrantReflection->getMethod('issueRefreshToken'); + $issueRefreshTokenMethod->setAccessible(true); + + $accessToken = new AccessTokenEntity(); + /** @var RefreshTokenEntityInterface $refreshToken */ + $refreshToken = $issueRefreshTokenMethod->invoke($grantMock, $accessToken); + $this->assertTrue($refreshToken instanceof RefreshTokenEntityInterface); + $this->assertEquals($accessToken, $refreshToken->getAccessToken()); + } + + public function testIssueAccessToken() + { + $accessTokenRepoMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepoMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setAccessTokenRepository($accessTokenRepoMock); + + $abstractGrantReflection = new \ReflectionClass($grantMock); + $issueAccessTokenMethod = $abstractGrantReflection->getMethod('issueAccessToken'); + $issueAccessTokenMethod->setAccessible(true); + + /** @var AccessTokenEntityInterface $accessToken */ + $accessToken = $issueAccessTokenMethod->invoke( + $grantMock, + new \DateInterval('PT1H'), + new ClientEntity(), + 123, + [new ScopeEntity()] + ); + $this->assertTrue($accessToken instanceof AccessTokenEntityInterface); + } + + public function testIssueAuthCode() + { + $authCodeRepoMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepoMock->expects($this->once())->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setAuthCodeRepository($authCodeRepoMock); + + $abstractGrantReflection = new \ReflectionClass($grantMock); + $issueAuthCodeMethod = $abstractGrantReflection->getMethod('issueAuthCode'); + $issueAuthCodeMethod->setAccessible(true); + + $this->assertTrue( + $issueAuthCodeMethod->invoke( + $grantMock, + new \DateInterval('PT1H'), + new ClientEntity(), + 123, + 'http://foo/bar', + [new ScopeEntity()] + ) instanceof AuthCodeEntityInterface + ); + } + + public function testGetCookieParameter() + { + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->method('getIdentifier')->willReturn('foobar'); + + $abstractGrantReflection = new \ReflectionClass($grantMock); + $method = $abstractGrantReflection->getMethod('getCookieParameter'); + $method->setAccessible(true); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withCookieParams([ + 'foo' => 'bar', + ]); + + $this->assertEquals('bar', $method->invoke($grantMock, 'foo', $serverRequest)); + $this->assertEquals('foo', $method->invoke($grantMock, 'bar', $serverRequest, 'foo')); + } + + public function testGetQueryStringParameter() + { + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->method('getIdentifier')->willReturn('foobar'); + + $abstractGrantReflection = new \ReflectionClass($grantMock); + $method = $abstractGrantReflection->getMethod('getQueryStringParameter'); + $method->setAccessible(true); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withQueryParams([ + 'foo' => 'bar', + ]); + + $this->assertEquals('bar', $method->invoke($grantMock, 'foo', $serverRequest)); + $this->assertEquals('foo', $method->invoke($grantMock, 'bar', $serverRequest, 'foo')); + } + + public function testValidateScopes() + { + $scope = new ScopeEntity(); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); + + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setScopeRepository($scopeRepositoryMock); + + $this->assertEquals([$scope], $grantMock->validateScopes('basic ')); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + */ + public function testValidateScopesBadScope() + { + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(null); + + /** @var AbstractGrant $grantMock */ + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->setScopeRepository($scopeRepositoryMock); + + $grantMock->validateScopes('basic '); + } + + 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))); + } + + public function testCanRespondToAuthorizationRequest() + { + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $this->assertFalse($grantMock->canRespondToAuthorizationRequest(new ServerRequest())); + } + + /** + * @expectedException \LogicException + */ + public function testValidateAuthorizationRequest() + { + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->validateAuthorizationRequest(new ServerRequest()); + } + + /** + * @expectedException \LogicException + */ + public function testCompleteAuthorizationRequest() + { + $grantMock = $this->getMockForAbstractClass(AbstractGrant::class); + $grantMock->completeAuthorizationRequest(new AuthorizationRequest()); + } +} diff --git a/tests/Grant/AuthCodeGrantTest.php b/tests/Grant/AuthCodeGrantTest.php new file mode 100644 index 0000000..3bccba0 --- /dev/null +++ b/tests/Grant/AuthCodeGrantTest.php @@ -0,0 +1,1637 @@ +cryptStub = new CryptTraitStub; + } + + public function testGetIdentifier() + { + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + + $this->assertEquals('authorization_code', $grant->getIdentifier()); + } + + public function testCanRespondToAuthorizationRequest() + { + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + 'client_id' => 'foo', + ] + ); + + $this->assertTrue($grant->canRespondToAuthorizationRequest($request)); + } + + public function testValidateAuthorizationRequest() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + [], + [], + [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + ] + ); + + $this->assertTrue($grant->validateAuthorizationRequest($request) instanceof AuthorizationRequest); + } + + public function testValidateAuthorizationRequestRedirectUriArray() + { + $client = new ClientEntity(); + $client->setRedirectUri(['http://foo/bar']); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + [], + [], + [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + ] + ); + + $this->assertTrue($grant->validateAuthorizationRequest($request) instanceof AuthorizationRequest); + } + + public function testValidateAuthorizationRequestCodeChallenge() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + [], + [], + [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code_challenge' => str_repeat('A', 43), + ] + ); + + $this->assertTrue($grant->validateAuthorizationRequest($request) instanceof AuthorizationRequest); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + */ + public function testValidateAuthorizationRequestCodeChallengeInvalidLengthTooShort() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + [], + [], + [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code_challenge' => str_repeat('A', 42), + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + */ + public function testValidateAuthorizationRequestCodeChallengeInvalidLengthTooLong() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + [], + [], + [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code_challenge' => str_repeat('A', 129), + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + */ + public function testValidateAuthorizationRequestCodeChallengeInvalidCharacters() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + [], + [], + [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code_challenge' => str_repeat('A', 42) . '!', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 3 + */ + public function testValidateAuthorizationRequestMissingClientId() + { + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 4 + */ + public function testValidateAuthorizationRequestInvalidClientId() + { + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn(null); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + 'client_id' => 'foo', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 4 + */ + public function testValidateAuthorizationRequestBadRedirectUriString() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + [], + [], + [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://bar', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 4 + */ + public function testValidateAuthorizationRequestBadRedirectUriArray() + { + $client = new ClientEntity(); + $client->setRedirectUri(['http://foo/bar']); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + [], + [], + [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://bar', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 3 + */ + public function testValidateAuthorizationRequestMissingCodeChallenge() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + [], + [], + [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 3 + */ + public function testValidateAuthorizationRequestInvalidCodeChallengeMethod() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + [], + [], + [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code_challenge' => 'foobar', + 'code_challenge_method' => 'foo', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + public function testCompleteAuthorizationRequest() + { + $authRequest = new AuthorizationRequest(); + $authRequest->setAuthorizationApproved(true); + $authRequest->setClient(new ClientEntity()); + $authRequest->setGrantTypeId('authorization_code'); + $authRequest->setUser(new UserEntity()); + + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $grant = new AuthCodeGrant( + $authCodeRepository, + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $this->assertTrue($grant->completeAuthorizationRequest($authRequest) instanceof RedirectResponse); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 9 + */ + public function testCompleteAuthorizationRequestDenied() + { + $authRequest = new AuthorizationRequest(); + $authRequest->setAuthorizationApproved(false); + $authRequest->setClient(new ClientEntity()); + $authRequest->setGrantTypeId('authorization_code'); + $authRequest->setUser(new UserEntity()); + + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $grant = new AuthCodeGrant( + $authCodeRepository, + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $grant->completeAuthorizationRequest($authRequest); + } + + public function testRespondToAccessTokenRequest() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + ] + ) + ), + ] + ); + + /** @var StubResponseType $response */ + $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + + $this->assertTrue($response->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($response->getRefreshToken() instanceof RefreshTokenEntityInterface); + } + + public function testRespondToAccessTokenRequestCodeChallengePlain() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code_verifier' => 'foobar', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + 'code_challenge' => 'foobar', + 'code_challenge_method' => 'plain', + ] + ) + ), + ] + ); + + /** @var StubResponseType $response */ + $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + + $this->assertTrue($response->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($response->getRefreshToken() instanceof RefreshTokenEntityInterface); + } + + public function testRespondToAccessTokenRequestCodeChallengeS256() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code_verifier' => 'foobar', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + 'code_challenge' => urlencode(base64_encode(hash('sha256', 'foobar'))), + 'code_challenge_method' => 'S256', + ] + ) + ), + ] + ); + + /** @var StubResponseType $response */ + $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + + $this->assertTrue($response->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($response->getRefreshToken() instanceof RefreshTokenEntityInterface); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 3 + */ + public function testRespondToAccessTokenRequestMissingRedirectUri() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'client_id' => 'foo', + 'grant_type' => 'authorization_code', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + ] + ) + ), + ] + ); + + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 3 + */ + public function testRespondToAccessTokenRequestRedirectUriMismatch() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'client_id' => 'foo', + 'grant_type' => 'authorization_code', + 'redirect_uri' => 'http://bar/foo', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + ] + ) + ), + ] + ); + + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 3 + */ + public function testRespondToAccessTokenRequestMissingCode() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'redirect_uri' => 'http://foo/bar', + ] + ); + + /* @var StubResponseType $response */ + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } + + public function testRespondToAccessTokenRequestExpiredCode() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() - 3600, + 'client_id' => 'foo', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + ] + ) + ), + ] + ); + + try { + /* @var StubResponseType $response */ + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } catch (OAuthServerException $e) { + $this->assertEquals($e->getHint(), 'Authorization code has expired'); + } + } + + public function testRespondToAccessTokenRequestRevokedCode() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + + $authCodeRepositoryMock = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepositoryMock->method('isAuthCodeRevoked')->willReturn(true); + + $grant = new AuthCodeGrant( + $authCodeRepositoryMock, + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + ] + ) + ), + ] + ); + + try { + /* @var StubResponseType $response */ + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } catch (OAuthServerException $e) { + $this->assertEquals($e->getHint(), 'Authorization code has been revoked'); + } + } + + public function testRespondToAccessTokenRequestClientMismatch() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'bar', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + ] + ) + ), + ] + ); + + try { + /* @var StubResponseType $response */ + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } catch (OAuthServerException $e) { + $this->assertEquals($e->getHint(), 'Authorization code was not issued to this client'); + } + } + + public function testRespondToAccessTokenRequestBadCodeEncryption() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code' => 'sdfsfsd', + ] + ); + + try { + /* @var StubResponseType $response */ + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } catch (OAuthServerException $e) { + $this->assertEquals($e->getHint(), 'Cannot decrypt the authorization code'); + } + } + + public function testRespondToAccessTokenRequestBadCodeVerifierPlain() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code_verifier' => 'nope', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + 'code_challenge' => 'foobar', + 'code_challenge_method' => 'plain', + ] + ) + ), + ] + ); + + try { + /* @var StubResponseType $response */ + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } catch (OAuthServerException $e) { + $this->assertEquals($e->getHint(), 'Failed to verify `code_verifier`.'); + } + } + + public function testRespondToAccessTokenRequestBadCodeVerifierS256() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code_verifier' => 'nope', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + 'code_challenge' => 'foobar', + 'code_challenge_method' => 'S256', + ] + ) + ), + ] + ); + + try { + /* @var StubResponseType $response */ + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } catch (OAuthServerException $e) { + $this->assertEquals($e->getHint(), 'Failed to verify `code_verifier`.'); + } + } + + public function testRespondToAccessTokenRequestMissingCodeVerifier() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->enableCodeExchangeProof(); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + 'code_challenge' => 'foobar', + 'code_challenge_method' => 'plain', + ] + ) + ), + ] + ); + + try { + /* @var StubResponseType $response */ + $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + } catch (OAuthServerException $e) { + $this->assertEquals($e->getHint(), 'Check the `code_verifier` parameter'); + } + } + + public function testAuthCodeRepositoryUniqueConstraintCheck() + { + $authRequest = new AuthorizationRequest(); + $authRequest->setAuthorizationApproved(true); + $authRequest->setClient(new ClientEntity()); + $authRequest->setGrantTypeId('authorization_code'); + $authRequest->setUser(new UserEntity()); + + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + + $authCodeRepository->expects($this->at(0))->method('persistNewAuthCode')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); + $authCodeRepository->expects($this->at(1))->method('persistNewAuthCode'); + + $grant = new AuthCodeGrant( + $authCodeRepository, + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $this->assertTrue($grant->completeAuthorizationRequest($authRequest) instanceof RedirectResponse); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 7 + */ + public function testAuthCodeRepositoryFailToPersist() + { + $authRequest = new AuthorizationRequest(); + $authRequest->setAuthorizationApproved(true); + $authRequest->setClient(new ClientEntity()); + $authRequest->setGrantTypeId('authorization_code'); + $authRequest->setUser(new UserEntity()); + + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + $authCodeRepository->method('persistNewAuthCode')->willThrowException(OAuthServerException::serverError('something bad happened')); + + $grant = new AuthCodeGrant( + $authCodeRepository, + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $this->assertTrue($grant->completeAuthorizationRequest($authRequest) instanceof RedirectResponse); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException + * @expectedExceptionCode 100 + */ + public function testAuthCodeRepositoryFailToPersistUniqueNoInfiniteLoop() + { + $authRequest = new AuthorizationRequest(); + $authRequest->setAuthorizationApproved(true); + $authRequest->setClient(new ClientEntity()); + $authRequest->setGrantTypeId('authorization_code'); + $authRequest->setUser(new UserEntity()); + + $authCodeRepository = $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(); + $authCodeRepository->method('getNewAuthCode')->willReturn(new AuthCodeEntity()); + $authCodeRepository->method('persistNewAuthCode')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); + + $grant = new AuthCodeGrant( + $authCodeRepository, + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + + $this->assertTrue($grant->completeAuthorizationRequest($authRequest) instanceof RedirectResponse); + } + + public function testRefreshTokenRepositoryUniqueConstraintCheck() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $refreshTokenRepositoryMock->expects($this->at(0))->method('persistNewRefreshToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); + $refreshTokenRepositoryMock->expects($this->at(1))->method('persistNewRefreshToken'); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + ] + ) + ), + ] + ); + + /** @var StubResponseType $response */ + $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + + $this->assertTrue($response->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($response->getRefreshToken() instanceof RefreshTokenEntityInterface); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 7 + */ + public function testRefreshTokenRepositoryFailToPersist() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willThrowException(OAuthServerException::serverError('something bad happened')); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + ] + ) + ), + ] + ); + + /** @var StubResponseType $response */ + $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + + $this->assertTrue($response->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($response->getRefreshToken() instanceof RefreshTokenEntityInterface); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException + * @expectedExceptionCode 100 + */ + public function testRefreshTokenRepositoryFailToPersistUniqueNoInfiniteLoop() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); + + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + + $request = new ServerRequest( + [], + [], + null, + 'POST', + 'php://input', + [], + [], + [], + [ + 'grant_type' => 'authorization_code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + 'code' => $this->cryptStub->doEncrypt( + json_encode( + [ + 'auth_code_id' => uniqid(), + 'expire_time' => time() + 3600, + 'client_id' => 'foo', + 'user_id' => 123, + 'scopes' => ['foo'], + 'redirect_uri' => 'http://foo/bar', + ] + ) + ), + ] + ); + + /** @var StubResponseType $response */ + $response = $grant->respondToAccessTokenRequest($request, new StubResponseType(), new \DateInterval('PT10M')); + + $this->assertTrue($response->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($response->getRefreshToken() instanceof RefreshTokenEntityInterface); + } + + /** + * @expectedException \LogicException + */ + public function testCompleteAuthorizationRequestNoUser() + { + $grant = new AuthCodeGrant( + $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(), + $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(), + new \DateInterval('PT10M') + ); + + $grant->completeAuthorizationRequest(new AuthorizationRequest()); + } +} diff --git a/tests/Grant/ClientCredentialsGrantTest.php b/tests/Grant/ClientCredentialsGrantTest.php new file mode 100644 index 0000000..a166583 --- /dev/null +++ b/tests/Grant/ClientCredentialsGrantTest.php @@ -0,0 +1,54 @@ +assertEquals('client_credentials', $grant->getIdentifier()); + } + + public function testRespondToRequest() + { + $client = new ClientEntity(); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $grant = new ClientCredentialsGrant(); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + + $this->assertTrue($responseType->getAccessToken() instanceof AccessTokenEntityInterface); + } +} diff --git a/tests/Grant/ImplicitGrantTest.php b/tests/Grant/ImplicitGrantTest.php new file mode 100644 index 0000000..3bfe4b8 --- /dev/null +++ b/tests/Grant/ImplicitGrantTest.php @@ -0,0 +1,410 @@ +cryptStub = new CryptTraitStub(); + } + + public function testGetIdentifier() + { + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $this->assertEquals('implicit', $grant->getIdentifier()); + } + + public function testCanRespondToAccessTokenRequest() + { + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + + $this->assertFalse( + $grant->canRespondToAccessTokenRequest(new ServerRequest()) + ); + } + + /** + * @expectedException \LogicException + */ + public function testRespondToAccessTokenRequest() + { + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->respondToAccessTokenRequest( + new ServerRequest(), + new StubResponseType(), + new \DateInterval('PT10M') + ); + } + + public function testCanRespondToAuthorizationRequest() + { + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'token', + 'client_id' => 'foo', + ] + ); + + $this->assertTrue($grant->canRespondToAuthorizationRequest($request)); + } + + public function testValidateAuthorizationRequest() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + ] + ); + + $this->assertTrue($grant->validateAuthorizationRequest($request) instanceof AuthorizationRequest); + } + + public function testValidateAuthorizationRequestRedirectUriArray() + { + $client = new ClientEntity(); + $client->setRedirectUri(['http://foo/bar']); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://foo/bar', + ] + ); + + $this->assertTrue($grant->validateAuthorizationRequest($request) instanceof AuthorizationRequest); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 3 + */ + public function testValidateAuthorizationRequestMissingClientId() + { + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 4 + */ + public function testValidateAuthorizationRequestInvalidClientId() + { + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn(null); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + 'client_id' => 'foo', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 4 + */ + public function testValidateAuthorizationRequestBadRedirectUriString() + { + $client = new ClientEntity(); + $client->setRedirectUri('http://foo/bar'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://bar', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 4 + */ + public function testValidateAuthorizationRequestBadRedirectUriArray() + { + $client = new ClientEntity(); + $client->setRedirectUri(['http://foo/bar']); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->setClientRepository($clientRepositoryMock); + + $request = new ServerRequest( + [], + [], + null, + null, + 'php://input', + $headers = [], + $cookies = [], + $queryParams = [ + 'response_type' => 'code', + 'client_id' => 'foo', + 'redirect_uri' => 'http://bar', + ] + ); + + $grant->validateAuthorizationRequest($request); + } + + public function testCompleteAuthorizationRequest() + { + $authRequest = new AuthorizationRequest(); + $authRequest->setAuthorizationApproved(true); + $authRequest->setClient(new ClientEntity()); + $authRequest->setGrantTypeId('authorization_code'); + $authRequest->setUser(new UserEntity()); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + + $this->assertTrue($grant->completeAuthorizationRequest($authRequest) instanceof RedirectResponse); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 9 + */ + public function testCompleteAuthorizationRequestDenied() + { + $authRequest = new AuthorizationRequest(); + $authRequest->setAuthorizationApproved(false); + $authRequest->setClient(new ClientEntity()); + $authRequest->setGrantTypeId('authorization_code'); + $authRequest->setUser(new UserEntity()); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + + $grant->completeAuthorizationRequest($authRequest); + } + + public function testAccessTokenRepositoryUniqueConstraintCheck() + { + $authRequest = new AuthorizationRequest(); + $authRequest->setAuthorizationApproved(true); + $authRequest->setClient(new ClientEntity()); + $authRequest->setGrantTypeId('authorization_code'); + $authRequest->setUser(new UserEntity()); + + /** @var AccessTokenRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $accessTokenRepositoryMock */ + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->expects($this->at(0))->method('persistNewAccessToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); + $accessTokenRepositoryMock->expects($this->at(1))->method('persistNewAccessToken')->willReturnSelf(); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + + $this->assertTrue($grant->completeAuthorizationRequest($authRequest) instanceof RedirectResponse); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 7 + */ + public function testAccessTokenRepositoryFailToPersist() + { + $authRequest = new AuthorizationRequest(); + $authRequest->setAuthorizationApproved(true); + $authRequest->setClient(new ClientEntity()); + $authRequest->setGrantTypeId('authorization_code'); + $authRequest->setUser(new UserEntity()); + + /** @var AccessTokenRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $accessTokenRepositoryMock */ + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willThrowException(OAuthServerException::serverError('something bad happened')); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + + $grant->completeAuthorizationRequest($authRequest); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException + * @expectedExceptionCode 100 + */ + public function testAccessTokenRepositoryFailToPersistUniqueNoInfiniteLoop() + { + $authRequest = new AuthorizationRequest(); + $authRequest->setAuthorizationApproved(true); + $authRequest->setClient(new ClientEntity()); + $authRequest->setGrantTypeId('authorization_code'); + $authRequest->setUser(new UserEntity()); + + /** @var AccessTokenRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $accessTokenRepositoryMock */ + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willThrowException(UniqueTokenIdentifierConstraintViolationException::create()); + + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + + $grant->completeAuthorizationRequest($authRequest); + } + + /** + * @expectedException \LogicException + */ + public function testSetRefreshTokenTTL() + { + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->setRefreshTokenTTL(new \DateInterval('PT10M')); + } + + /** + * @expectedException \LogicException + */ + public function testSetRefreshTokenRepository() + { + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $grant->setRefreshTokenRepository($refreshTokenRepositoryMock); + } + + /** + * @expectedException \LogicException + */ + public function testCompleteAuthorizationRequestNoUser() + { + $grant = new ImplicitGrant(new \DateInterval('PT10M')); + $grant->completeAuthorizationRequest(new AuthorizationRequest()); + } +} diff --git a/tests/Grant/PasswordGrantTest.php b/tests/Grant/PasswordGrantTest.php new file mode 100644 index 0000000..b380bfb --- /dev/null +++ b/tests/Grant/PasswordGrantTest.php @@ -0,0 +1,170 @@ +getMockBuilder(UserRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + + $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); + $this->assertEquals('password', $grant->getIdentifier()); + } + + public function testRespondToRequest() + { + $client = new ClientEntity(); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $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(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'username' => 'foo', + 'password' => 'bar', + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + + $this->assertTrue($responseType->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($responseType->getRefreshToken() instanceof RefreshTokenEntityInterface); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + */ + public function testRespondToRequestMissingUsername() + { + $client = new ClientEntity(); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + + $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + */ + public function testRespondToRequestMissingPassword() + { + $client = new ClientEntity(); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + + $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'username' => 'alex', + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + */ + public function testRespondToRequestBadCredentials() + { + $client = new ClientEntity(); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock(); + $userRepositoryMock->method('getUserEntityByUserCredentials')->willReturn(null); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + + $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'username' => 'alex', + 'password' => 'whisky', + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + } +} diff --git a/tests/Grant/RefreshTokenGrantTest.php b/tests/Grant/RefreshTokenGrantTest.php new file mode 100644 index 0000000..47d7ad1 --- /dev/null +++ b/tests/Grant/RefreshTokenGrantTest.php @@ -0,0 +1,421 @@ +cryptStub = new CryptTraitStub(); + } + + public function testGetIdentifier() + { + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $this->assertEquals('refresh_token', $grant->getIdentifier()); + } + + public function testRespondToRequest() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeEntity = new ScopeEntity(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock + ->expects($this->once()) + ->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + $refreshTokenRepositoryMock + ->expects($this->once()) + ->method('persistNewRefreshToken')->willReturnSelf(); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $oldRefreshToken = $this->cryptStub->doEncrypt( + json_encode( + [ + 'client_id' => 'foo', + 'refresh_token_id' => 'zyxwvu', + 'access_token_id' => 'abcdef', + 'scopes' => ['foo'], + 'user_id' => 123, + 'expire_time' => time() + 3600, + ] + ) + ); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'refresh_token' => $oldRefreshToken, + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + + $this->assertTrue($responseType->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($responseType->getRefreshToken() instanceof RefreshTokenEntityInterface); + } + + public function testRespondToReducedScopes() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity()); + + $scope = new ScopeEntity(); + $scope->setIdentifier('foo'); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $oldRefreshToken = $this->cryptStub->doEncrypt( + json_encode( + [ + 'client_id' => 'foo', + 'refresh_token_id' => 'zyxwvu', + 'access_token_id' => 'abcdef', + 'scopes' => ['foo', 'bar'], + 'user_id' => 123, + 'expire_time' => time() + 3600, + ] + ) + ); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'refresh_token' => $oldRefreshToken, + 'scope' => 'foo', + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + + $this->assertTrue($responseType->getAccessToken() instanceof AccessTokenEntityInterface); + $this->assertTrue($responseType->getRefreshToken() instanceof RefreshTokenEntityInterface); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 5 + */ + public function testRespondToUnexpectedScope() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + + $scope = new ScopeEntity(); + $scope->setIdentifier('foobar'); + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scope); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setScopeRepository($scopeRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $oldRefreshToken = $this->cryptStub->doEncrypt( + json_encode( + [ + 'client_id' => 'foo', + 'refresh_token_id' => 'zyxwvu', + 'access_token_id' => 'abcdef', + 'scopes' => ['foo', 'bar'], + 'user_id' => 123, + 'expire_time' => time() + 3600, + ] + ) + ); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'refresh_token' => $oldRefreshToken, + 'scope' => 'foobar', + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 3 + */ + public function testRespondToRequestMissingOldToken() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 8 + */ + public function testRespondToRequestInvalidOldToken() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $oldRefreshToken = 'foobar'; + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'refresh_token' => $oldRefreshToken, + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 8 + */ + public function testRespondToRequestClientMismatch() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf(); + + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('persistNewRefreshToken')->willReturnSelf(); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $oldRefreshToken = $this->cryptStub->doEncrypt( + json_encode( + [ + 'client_id' => 'bar', + 'refresh_token_id' => 'zyxwvu', + 'access_token_id' => 'abcdef', + 'scopes' => ['foo'], + 'user_id' => 123, + 'expire_time' => time() + 3600, + ] + ) + ); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'refresh_token' => $oldRefreshToken, + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 8 + */ + public function testRespondToRequestExpiredToken() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $oldRefreshToken = $this->cryptStub->doEncrypt( + json_encode( + [ + 'client_id' => 'foo', + 'refresh_token_id' => 'zyxwvu', + 'access_token_id' => 'abcdef', + 'scopes' => ['foo'], + 'user_id' => 123, + 'expire_time' => time() - 3600, + ] + ) + ); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'refresh_token' => $oldRefreshToken, + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + } + + /** + * @expectedException \League\OAuth2\Server\Exception\OAuthServerException + * @expectedExceptionCode 8 + */ + public function testRespondToRequestRevokedToken() + { + $client = new ClientEntity(); + $client->setIdentifier('foo'); + $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepositoryMock->method('getClientEntity')->willReturn($client); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock(); + $refreshTokenRepositoryMock->method('isRefreshTokenRevoked')->willReturn(true); + + $grant = new RefreshTokenGrant($refreshTokenRepositoryMock); + $grant->setClientRepository($clientRepositoryMock); + $grant->setAccessTokenRepository($accessTokenRepositoryMock); + $grant->setEncryptionKey($this->cryptStub->getKey()); + $grant->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $oldRefreshToken = $this->cryptStub->doEncrypt( + json_encode( + [ + 'client_id' => 'foo', + 'refresh_token_id' => 'zyxwvu', + 'access_token_id' => 'abcdef', + 'scopes' => ['foo'], + 'user_id' => 123, + 'expire_time' => time() + 3600, + ] + ) + ); + + $serverRequest = new ServerRequest(); + $serverRequest = $serverRequest->withParsedBody( + [ + 'client_id' => 'foo', + 'client_secret' => 'bar', + 'refresh_token' => $oldRefreshToken, + ] + ); + + $responseType = new StubResponseType(); + $grant->respondToAccessTokenRequest($serverRequest, $responseType, new \DateInterval('PT5M')); + } +} diff --git a/tests/Middleware/AuthorizationServerMiddlewareTest.php b/tests/Middleware/AuthorizationServerMiddlewareTest.php new file mode 100644 index 0000000..74dffbf --- /dev/null +++ b/tests/Middleware/AuthorizationServerMiddlewareTest.php @@ -0,0 +1,113 @@ +getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepository->method('getClientEntity')->willReturn(new ClientEntity()); + + $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(); + $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0); + + $accessRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity()); + + $server = new AuthorizationServer( + $clientRepository, + $accessRepositoryMock, + $scopeRepositoryMock, + 'file://' . __DIR__ . '/../Stubs/private.key', + base64_encode(random_bytes(36)), + new StubResponseType() + ); + + $server->enableGrantType(new ClientCredentialsGrant()); + + $_POST['grant_type'] = 'client_credentials'; + $_POST['client_id'] = 'foo'; + $_POST['client_secret'] = 'bar'; + + $request = ServerRequestFactory::fromGlobals(); + + $middleware = new AuthorizationServerMiddleware($server); + $response = $middleware->__invoke( + $request, + new Response(), + function () { + return func_get_args()[1]; + } + ); + $this->assertEquals(200, $response->getStatusCode()); + } + + public function testOAuthErrorResponse() + { + $clientRepository = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock(); + $clientRepository->method('getClientEntity')->willReturn(null); + + $server = new AuthorizationServer( + $clientRepository, + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/../Stubs/private.key', + base64_encode(random_bytes(36)), + new StubResponseType() + ); + + $server->enableGrantType(new ClientCredentialsGrant(), new \DateInterval('PT1M')); + + $_POST['grant_type'] = 'client_credentials'; + $_POST['client_id'] = 'foo'; + $_POST['client_secret'] = 'bar'; + + $request = ServerRequestFactory::fromGlobals(); + + $middleware = new AuthorizationServerMiddleware($server); + + $response = $middleware->__invoke( + $request, + new Response(), + function () { + return func_get_args()[1]; + } + ); + + $this->assertEquals(401, $response->getStatusCode()); + } + + public function testOAuthErrorResponseRedirectUri() + { + $exception = OAuthServerException::invalidScope('test', 'http://foo/bar'); + $response = $exception->generateHttpResponse(new Response()); + + $this->assertEquals(302, $response->getStatusCode()); + $this->assertEquals('http://foo/bar?error=invalid_scope&message=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed&hint=Check+the+%60test%60+scope', + $response->getHeader('location')[0]); + } + + public function testOAuthErrorResponseRedirectUriFragment() + { + $exception = OAuthServerException::invalidScope('test', 'http://foo/bar'); + $response = $exception->generateHttpResponse(new Response(), true); + + $this->assertEquals(302, $response->getStatusCode()); + $this->assertEquals('http://foo/bar#error=invalid_scope&message=The+requested+scope+is+invalid%2C+unknown%2C+or+malformed&hint=Check+the+%60test%60+scope', + $response->getHeader('location')[0]); + } +} diff --git a/tests/Middleware/ResourceServerMiddlewareTest.php b/tests/Middleware/ResourceServerMiddlewareTest.php new file mode 100644 index 0000000..549c800 --- /dev/null +++ b/tests/Middleware/ResourceServerMiddlewareTest.php @@ -0,0 +1,107 @@ +getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/../Stubs/public.key' + ); + + $client = new ClientEntity(); + $client->setIdentifier('clientName'); + + $accessToken = new AccessTokenEntity(); + $accessToken->setIdentifier('test'); + $accessToken->setUserIdentifier(123); + $accessToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H'))); + $accessToken->setClient($client); + + $token = $accessToken->convertToJWT(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $request = new ServerRequest(); + $request = $request->withHeader('authorization', sprintf('Bearer %s', $token)); + + $middleware = new ResourceServerMiddleware($server); + $response = $middleware->__invoke( + $request, + new Response(), + function () { + $this->assertEquals('test', func_get_args()[0]->getAttribute('oauth_access_token_id')); + + return func_get_args()[1]; + } + ); + + $this->assertEquals(200, $response->getStatusCode()); + } + + public function testValidResponseExpiredToken() + { + $server = new ResourceServer( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/../Stubs/public.key' + ); + + $client = new ClientEntity(); + $client->setIdentifier('clientName'); + + $accessToken = new AccessTokenEntity(); + $accessToken->setIdentifier('test'); + $accessToken->setUserIdentifier(123); + $accessToken->setExpiryDateTime((new \DateTime())->sub(new \DateInterval('PT1H'))); + $accessToken->setClient($client); + + $token = $accessToken->convertToJWT(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + + $request = new ServerRequest(); + $request = $request->withHeader('authorization', sprintf('Bearer %s', $token)); + + $middleware = new ResourceServerMiddleware($server); + $response = $middleware->__invoke( + $request, + new Response(), + function () { + $this->assertEquals('test', func_get_args()[0]->getAttribute('oauth_access_token_id')); + + return func_get_args()[1]; + } + ); + + $this->assertEquals(401, $response->getStatusCode()); + } + + public function testErrorResponse() + { + $server = new ResourceServer( + $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/../Stubs/public.key' + ); + + $request = new ServerRequest(); + $request = $request->withHeader('authorization', ''); + + $middleware = new ResourceServerMiddleware($server); + $response = $middleware->__invoke( + $request, + new Response(), + function () { + return func_get_args()[1]; + } + ); + + $this->assertEquals(401, $response->getStatusCode()); + } +} diff --git a/tests/ResourceServerTest.php b/tests/ResourceServerTest.php new file mode 100644 index 0000000..8a3353c --- /dev/null +++ b/tests/ResourceServerTest.php @@ -0,0 +1,26 @@ +getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(), + 'file://' . __DIR__ . '/Stubs/public.key' + ); + + try { + $server->validateAuthenticatedRequest(ServerRequestFactory::fromGlobals()); + } catch (OAuthServerException $e) { + $this->assertEquals('Missing "Authorization" header', $e->getHint()); + } + } +} diff --git a/tests/ResponseTypes/BearerResponseTypeTest.php b/tests/ResponseTypes/BearerResponseTypeTest.php new file mode 100644 index 0000000..7f710d9 --- /dev/null +++ b/tests/ResponseTypes/BearerResponseTypeTest.php @@ -0,0 +1,298 @@ +getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + $responseType = new BearerTokenResponse($accessTokenRepositoryMock); + $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $responseType->setEncryptionKey(base64_encode(random_bytes(36))); + + $client = new ClientEntity(); + $client->setIdentifier('clientName'); + + $scope = new ScopeEntity(); + $scope->setIdentifier('basic'); + + $accessToken = new AccessTokenEntity(); + $accessToken->setIdentifier('abcdef'); + $accessToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H'))); + $accessToken->setClient($client); + $accessToken->addScope($scope); + + $refreshToken = new RefreshTokenEntity(); + $refreshToken->setIdentifier('abcdef'); + $refreshToken->setAccessToken($accessToken); + $refreshToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H'))); + + $responseType->setAccessToken($accessToken); + $responseType->setRefreshToken($refreshToken); + + $response = $responseType->generateHttpResponse(new Response()); + + $this->assertTrue($response instanceof ResponseInterface); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('no-cache', $response->getHeader('pragma')[0]); + $this->assertEquals('no-store', $response->getHeader('cache-control')[0]); + $this->assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); + + $response->getBody()->rewind(); + $json = json_decode($response->getBody()->getContents()); + $this->assertEquals('Bearer', $json->token_type); + $this->assertTrue(isset($json->expires_in)); + $this->assertTrue(isset($json->access_token)); + $this->assertTrue(isset($json->refresh_token)); + } + + public function testGenerateHttpResponseWithExtraParams() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + $responseType = new BearerTokenResponseWithParams($accessTokenRepositoryMock); + $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $responseType->setEncryptionKey(base64_encode(random_bytes(36))); + + $client = new ClientEntity(); + $client->setIdentifier('clientName'); + + $scope = new ScopeEntity(); + $scope->setIdentifier('basic'); + + $accessToken = new AccessTokenEntity(); + $accessToken->setIdentifier('abcdef'); + $accessToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H'))); + $accessToken->setClient($client); + $accessToken->addScope($scope); + + $refreshToken = new RefreshTokenEntity(); + $refreshToken->setIdentifier('abcdef'); + $refreshToken->setAccessToken($accessToken); + $refreshToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H'))); + + $responseType->setAccessToken($accessToken); + $responseType->setRefreshToken($refreshToken); + + $response = $responseType->generateHttpResponse(new Response()); + + $this->assertTrue($response instanceof ResponseInterface); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('no-cache', $response->getHeader('pragma')[0]); + $this->assertEquals('no-store', $response->getHeader('cache-control')[0]); + $this->assertEquals('application/json; charset=UTF-8', $response->getHeader('content-type')[0]); + + $response->getBody()->rewind(); + $json = json_decode($response->getBody()->getContents()); + $this->assertEquals('Bearer', $json->token_type); + $this->assertTrue(isset($json->expires_in)); + $this->assertTrue(isset($json->access_token)); + $this->assertTrue(isset($json->refresh_token)); + + $this->assertTrue(isset($json->foo)); + $this->assertEquals('bar', $json->foo); + } + + public function testDetermineAccessTokenInHeaderValidToken() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(false); + + $responseType = new BearerTokenResponse($accessTokenRepositoryMock); + $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $responseType->setEncryptionKey(base64_encode(random_bytes(36))); + + $client = new ClientEntity(); + $client->setIdentifier('clientName'); + + $accessToken = new AccessTokenEntity(); + $accessToken->setIdentifier('abcdef'); + $accessToken->setUserIdentifier(123); + $accessToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H'))); + $accessToken->setClient($client); + + $refreshToken = new RefreshTokenEntity(); + $refreshToken->setIdentifier('abcdef'); + $refreshToken->setAccessToken($accessToken); + $refreshToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H'))); + + $responseType->setAccessToken($accessToken); + $responseType->setRefreshToken($refreshToken); + + $response = $responseType->generateHttpResponse(new Response()); + $json = json_decode((string) $response->getBody()); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(false); + + $authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $request = new ServerRequest(); + $request = $request->withHeader('authorization', sprintf('Bearer %s', $json->access_token)); + + $request = $authorizationValidator->validateAuthorization($request); + + $this->assertEquals('abcdef', $request->getAttribute('oauth_access_token_id')); + $this->assertEquals('clientName', $request->getAttribute('oauth_client_id')); + $this->assertEquals('123', $request->getAttribute('oauth_user_id')); + $this->assertEquals([], $request->getAttribute('oauth_scopes')); + } + + public function testDetermineAccessTokenInHeaderInvalidJWT() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(false); + + $responseType = new BearerTokenResponse($accessTokenRepositoryMock); + $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $responseType->setEncryptionKey(base64_encode(random_bytes(36))); + + $client = new ClientEntity(); + $client->setIdentifier('clientName'); + + $accessToken = new AccessTokenEntity(); + $accessToken->setIdentifier('abcdef'); + $accessToken->setUserIdentifier(123); + $accessToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H'))); + $accessToken->setClient($client); + + $refreshToken = new RefreshTokenEntity(); + $refreshToken->setIdentifier('abcdef'); + $refreshToken->setAccessToken($accessToken); + $refreshToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H'))); + + $responseType->setAccessToken($accessToken); + $responseType->setRefreshToken($refreshToken); + + $response = $responseType->generateHttpResponse(new Response()); + $json = json_decode((string) $response->getBody()); + + $authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $request = new ServerRequest(); + $request = $request->withHeader('authorization', sprintf('Bearer %s', $json->access_token . 'foo')); + + try { + $authorizationValidator->validateAuthorization($request); + } catch (OAuthServerException $e) { + $this->assertEquals( + 'Access token could not be verified', + $e->getHint() + ); + } + } + + public function testDetermineAccessTokenInHeaderRevokedToken() + { + $responseType = new BearerTokenResponse(); + $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $responseType->setEncryptionKey(base64_encode(random_bytes(36))); + + $client = new ClientEntity(); + $client->setIdentifier('clientName'); + + $accessToken = new AccessTokenEntity(); + $accessToken->setIdentifier('abcdef'); + $accessToken->setUserIdentifier(123); + $accessToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H'))); + $accessToken->setClient($client); + + $refreshToken = new RefreshTokenEntity(); + $refreshToken->setIdentifier('abcdef'); + $refreshToken->setAccessToken($accessToken); + $refreshToken->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H'))); + + $responseType->setAccessToken($accessToken); + $responseType->setRefreshToken($refreshToken); + + $response = $responseType->generateHttpResponse(new Response()); + $json = json_decode((string) $response->getBody()); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + $accessTokenRepositoryMock->method('isAccessTokenRevoked')->willReturn(true); + + $authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $request = new ServerRequest(); + $request = $request->withHeader('authorization', sprintf('Bearer %s', $json->access_token)); + + try { + $authorizationValidator->validateAuthorization($request); + } catch (OAuthServerException $e) { + $this->assertEquals( + 'Access token has been revoked', + $e->getHint() + ); + } + } + + public function testDetermineAccessTokenInHeaderInvalidToken() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + $responseType = new BearerTokenResponse($accessTokenRepositoryMock); + $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $responseType->setEncryptionKey(base64_encode(random_bytes(36))); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + $authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $request = new ServerRequest(); + $request = $request->withHeader('authorization', 'Bearer blah'); + + try { + $authorizationValidator->validateAuthorization($request); + } catch (OAuthServerException $e) { + $this->assertEquals( + 'The JWT string must have two dots', + $e->getHint() + ); + } + } + + public function testDetermineMissingBearerInHeader() + { + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + $responseType = new BearerTokenResponse($accessTokenRepositoryMock); + $responseType->setPrivateKey(new CryptKey('file://' . __DIR__ . '/../Stubs/private.key')); + $responseType->setEncryptionKey(base64_encode(random_bytes(36))); + + $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock(); + + $authorizationValidator = new BearerTokenValidator($accessTokenRepositoryMock); + $authorizationValidator->setPublicKey(new CryptKey('file://' . __DIR__ . '/../Stubs/public.key')); + + $request = new ServerRequest(); + $request = $request->withHeader('authorization', 'Bearer blah.blah.blah'); + + try { + $authorizationValidator->validateAuthorization($request); + } catch (OAuthServerException $e) { + $this->assertEquals( + 'Error while decoding to JSON', + $e->getHint() + ); + } + } +} diff --git a/tests/ResponseTypes/BearerTokenResponseWithParams.php b/tests/ResponseTypes/BearerTokenResponseWithParams.php new file mode 100644 index 0000000..4ba09f4 --- /dev/null +++ b/tests/ResponseTypes/BearerTokenResponseWithParams.php @@ -0,0 +1,14 @@ + 'bar', 'token_type' => 'Should not overwrite']; + } +} diff --git a/tests/Stubs/AccessTokenEntity.php b/tests/Stubs/AccessTokenEntity.php new file mode 100644 index 0000000..77a4d22 --- /dev/null +++ b/tests/Stubs/AccessTokenEntity.php @@ -0,0 +1,13 @@ +redirectUri = $uri; + } + + public function setName($name) + { + $this->name = $name; + } +} diff --git a/tests/Stubs/CryptTraitStub.php b/tests/Stubs/CryptTraitStub.php new file mode 100644 index 0000000..a481a84 --- /dev/null +++ b/tests/Stubs/CryptTraitStub.php @@ -0,0 +1,31 @@ +setEncryptionKey(base64_encode(random_bytes(36))); + } + + public function getKey() + { + return $this->encryptionKey; + } + + public function doEncrypt($unencryptedData) + { + return $this->encrypt($unencryptedData); + } + + public function doDecrypt($encryptedData) + { + return $this->decrypt($encryptedData); + } +} diff --git a/tests/Stubs/RefreshTokenEntity.php b/tests/Stubs/RefreshTokenEntity.php new file mode 100644 index 0000000..f145b70 --- /dev/null +++ b/tests/Stubs/RefreshTokenEntity.php @@ -0,0 +1,12 @@ +getIdentifier(); + } +} diff --git a/tests/Stubs/StubResponseType.php b/tests/Stubs/StubResponseType.php new file mode 100644 index 0000000..ac8679d --- /dev/null +++ b/tests/Stubs/StubResponseType.php @@ -0,0 +1,70 @@ +accessToken; + } + + public function getRefreshToken() + { + return $this->refreshToken; + } + + /** + * @param \League\OAuth2\Server\Entities\AccessTokenEntityInterface $accessToken + */ + public function setAccessToken(AccessTokenEntityInterface $accessToken) + { + $this->accessToken = $accessToken; + } + + /** + * @param \League\OAuth2\Server\Entities\RefreshTokenEntityInterface $refreshToken + */ + public function setRefreshToken(RefreshTokenEntityInterface $refreshToken) + { + $this->refreshToken = $refreshToken; + } + + /** + * @param ServerRequestInterface $request + * + * @throws \League\OAuth2\Server\Exception\OAuthServerException + * + * @return \Psr\Http\Message\ServerRequestInterface + */ + public function validateAccessToken(ServerRequestInterface $request) + { + if ($request->getHeader('authorization')[0] === 'Basic test') { + return $request->withAttribute('oauth_access_token_id', 'test'); + } + + throw OAuthServerException::accessDenied(); + } + + /** + * @param ResponseInterface $response + * + * @return ResponseInterface + */ + public function generateHttpResponse(ResponseInterface $response) + { + return new Response(); + } +} diff --git a/tests/Stubs/UserEntity.php b/tests/Stubs/UserEntity.php new file mode 100644 index 0000000..6bbdc8c --- /dev/null +++ b/tests/Stubs/UserEntity.php @@ -0,0 +1,16 @@ +setIdentifier(123); + } +} diff --git a/tests/Stubs/private.key b/tests/Stubs/private.key new file mode 100644 index 0000000..1d6a3bf --- /dev/null +++ b/tests/Stubs/private.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDOBcFjGUlo3BJ9zjwQLgAHn6Oy5Si0uB7MublTiPob8rWTiCE4 +weAFqzPoAB07vB0t0f8c1R8rmwHMD5ljWPBgJ8FewtwAUzprOBcau6DWukd/TKxX +WeVLAl/NZxijI+jR5QDBYLNBtj1G4LBVHMmINd3ryCycbf9ac3rcC8zhrQIDAQAB +AoGADfOJ0wIlXHp6rhZHLvlOezWuSjEGfqZxP3/cMvH1rerTrPfs+AD5AKlFTJKl +aCQm/bFYy0ULZVKL3pu30Wh2bo1nh/wLuLSI9Nz3O8jqAP3z0i07SoRoQmb8fRnn +dwoDFqnk3uGqcOenheSqheIgl9vdW/3avhD6nkMKZGxPYwECQQDoSj/xHogEzMqB +1Z2E5H/exeE9GQ7+dGITRR2MSgo9WvcKdRhGaQ44dsnTmqiZWAfqAPJjTQIIA/Cn +YRRTeBbNAkEA4w0iEvCIygGQOAnWuvVzlh+pxIB+BTeGkbiBG7nkYYc9b6B/Tw1B +GWGRddBr/FIfPvy1X2ip/TBpH+9bHnE2YQJBAIbZw/EYhmIy+UUSW9WwSUNsoOu1 +Rm0V53HEZ/jvaq5fxpa9j5AgoO7KlzROzp3m6wE/93cKV6mLkAO7ae9jAekCQQCf +B6DZIS6+RrAMACAt3SOzf8P6BYG/B7Ayusd7cw2ang4S9JiW9xKkw2kN2wj3t1F5 +XalwBTAjTdgj7ROmU+ehAkEAkOyXKONGBoVfaixRHgBP6jIBSSPbB2Aosi0QAURX +6GOY7wOS1pCSntTOBQxV7wVjqFwYAR10MSxFSNfpJ7RkzA== +-----END RSA PRIVATE KEY----- diff --git a/tests/Stubs/public.key b/tests/Stubs/public.key new file mode 100644 index 0000000..2501010 --- /dev/null +++ b/tests/Stubs/public.key @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOBcFjGUlo3BJ9zjwQLgAHn6Oy +5Si0uB7MublTiPob8rWTiCE4weAFqzPoAB07vB0t0f8c1R8rmwHMD5ljWPBgJ8Fe +wtwAUzprOBcau6DWukd/TKxXWeVLAl/NZxijI+jR5QDBYLNBtj1G4LBVHMmINd3r +yCycbf9ac3rcC8zhrQIDAQAB +-----END PUBLIC KEY-----