├── .github └── workflows │ └── tests.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── src └── OAuth2 │ ├── Autoloader.php │ ├── ClientAssertionType │ ├── ClientAssertionTypeInterface.php │ └── HttpBasic.php │ ├── Controller │ ├── AuthorizeController.php │ ├── AuthorizeControllerInterface.php │ ├── ResourceController.php │ ├── ResourceControllerInterface.php │ ├── TokenController.php │ └── TokenControllerInterface.php │ ├── Encryption │ ├── EncryptionInterface.php │ ├── FirebaseJwt.php │ └── Jwt.php │ ├── GrantType │ ├── AuthorizationCode.php │ ├── ClientCredentials.php │ ├── GrantTypeInterface.php │ ├── JwtBearer.php │ ├── RefreshToken.php │ └── UserCredentials.php │ ├── OpenID │ ├── Controller │ │ ├── AuthorizeController.php │ │ ├── AuthorizeControllerInterface.php │ │ ├── UserInfoController.php │ │ └── UserInfoControllerInterface.php │ ├── GrantType │ │ └── AuthorizationCode.php │ ├── ResponseType │ │ ├── AuthorizationCode.php │ │ ├── AuthorizationCodeInterface.php │ │ ├── CodeIdToken.php │ │ ├── CodeIdTokenInterface.php │ │ ├── IdToken.php │ │ ├── IdTokenInterface.php │ │ ├── IdTokenToken.php │ │ └── IdTokenTokenInterface.php │ └── Storage │ │ ├── AuthorizationCodeInterface.php │ │ └── UserClaimsInterface.php │ ├── Request.php │ ├── RequestInterface.php │ ├── Response.php │ ├── ResponseInterface.php │ ├── ResponseType │ ├── AccessToken.php │ ├── AccessTokenInterface.php │ ├── AuthorizationCode.php │ ├── AuthorizationCodeInterface.php │ ├── JwtAccessToken.php │ └── ResponseTypeInterface.php │ ├── Scope.php │ ├── ScopeInterface.php │ ├── Server.php │ ├── Storage │ ├── AccessTokenInterface.php │ ├── AuthorizationCodeInterface.php │ ├── Cassandra.php │ ├── ClientCredentialsInterface.php │ ├── ClientInterface.php │ ├── CouchbaseDB.php │ ├── DynamoDB.php │ ├── JwtAccessToken.php │ ├── JwtAccessTokenInterface.php │ ├── JwtBearerInterface.php │ ├── Memory.php │ ├── Mongo.php │ ├── MongoDB.php │ ├── Pdo.php │ ├── PublicKeyInterface.php │ ├── Redis.php │ ├── RefreshTokenInterface.php │ ├── ScopeInterface.php │ └── UserCredentialsInterface.php │ └── TokenType │ ├── Bearer.php │ ├── Mac.php │ └── TokenTypeInterface.php └── test ├── OAuth2 ├── AutoloadTest.php ├── Controller │ ├── AuthorizeControllerTest.php │ ├── ResourceControllerTest.php │ └── TokenControllerTest.php ├── Encryption │ ├── FirebaseJwtTest.php │ └── JwtTest.php ├── GrantType │ ├── AuthorizationCodeTest.php │ ├── ClientCredentialsTest.php │ ├── ImplicitTest.php │ ├── JwtBearerTest.php │ ├── RefreshTokenTest.php │ └── UserCredentialsTest.php ├── OpenID │ ├── Controller │ │ ├── AuthorizeControllerTest.php │ │ └── UserInfoControllerTest.php │ ├── GrantType │ │ └── AuthorizationCodeTest.php │ ├── ResponseType │ │ ├── CodeIdTokenTest.php │ │ ├── IdTokenTest.php │ │ └── IdTokenTokenTest.php │ └── Storage │ │ ├── AuthorizationCodeTest.php │ │ └── UserClaimsTest.php ├── RequestTest.php ├── ResponseTest.php ├── ResponseType │ ├── AccessTokenTest.php │ └── JwtAccessTokenTest.php ├── ScopeTest.php ├── ServerTest.php ├── Storage │ ├── AccessTokenTest.php │ ├── AuthorizationCodeTest.php │ ├── ClientCredentialsTest.php │ ├── ClientTest.php │ ├── DynamoDBTest.php │ ├── JwtAccessTokenTest.php │ ├── JwtBearerTest.php │ ├── PdoTest.php │ ├── PublicKeyTest.php │ ├── RefreshTokenTest.php │ ├── ScopeTest.php │ └── UserCredentialsTest.php └── TokenType │ └── BearerTest.php ├── bootstrap.php ├── config ├── keys │ ├── id_rsa │ └── id_rsa.pub └── storage.json └── lib └── OAuth2 ├── Request └── TestRequest.php └── Storage ├── BaseTest.php ├── Bootstrap.php └── NullStorage.php /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test Suite 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | services: 11 | redis: 12 | image: redis 13 | ports: 14 | - 6379:6379 15 | options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 16 | mongodb: 17 | image: mongo 18 | ports: 19 | - 27017:27017 20 | myriadb: 21 | image: mariadb 22 | env: 23 | MYSQL_ROOT_PASSWORD: root 24 | ports: 25 | - 3808:3808 26 | - 3306:3306 27 | postgres: 28 | image: postgres 29 | env: 30 | POSTGRES_DB: oauth2_server_php 31 | POSTGRES_USER: postgres 32 | POSTGRES_PASSWORD: postgres 33 | ports: 34 | - 5432:5432 35 | options: --health-cmd="pg_isready -h localhost" --health-interval=10s --health-timeout=5s --health-retries=5 36 | strategy: 37 | matrix: 38 | php: [ 7.2, 7.3, 7.4, "8.0", 8.1, 8.2 ] 39 | name: "PHP ${{ matrix.php }} Unit Test" 40 | steps: 41 | - uses: actions/checkout@v2 42 | - uses: codecov/codecov-action@v1 43 | - name: Setup PHP 44 | uses: shivammathur/setup-php@v2 45 | with: 46 | php-version: ${{ matrix.php }} 47 | extensions: mongodb, mbstring, intl, redis, pdo_mysql 48 | - name: Install composer dependencies 49 | uses: nick-invision/retry@v1 50 | with: 51 | timeout_minutes: 10 52 | max_attempts: 3 53 | command: composer install 54 | - name: Run PHPUnit 55 | run: vendor/bin/phpunit -v 56 | phpstan: 57 | name: "PHPStan" 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/checkout@v2 61 | - name: Setup PHP 62 | uses: shivammathur/setup-php@v2 63 | with: 64 | php-version: 8.1 65 | - name: Install composer dependencies 66 | uses: nick-invision/retry@v1 67 | with: 68 | timeout_minutes: 10 69 | max_attempts: 3 70 | command: composer install 71 | - name: Run PHPStan 72 | run: | 73 | composer require phpstan/phpstan 74 | vendor/bin/phpstan analyse --level=0 src/ 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014 Brent Shaffer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | oauth2-server-php 2 | ================= 3 | 4 | [![Total Downloads](https://poser.pugx.org/bshaffer/oauth2-server-php/downloads)](https://packagist.org/packages/bshaffer/oauth2-server-php) 5 | 6 | View the [complete documentation](https://bshaffer.github.io/oauth2-server-php-docs/) 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bshaffer/oauth2-server-php", 3 | "description":"OAuth2 Server for PHP", 4 | "keywords":["oauth","oauth2","auth"], 5 | "type":"library", 6 | "license":"MIT", 7 | "authors":[ 8 | { 9 | "name":"Brent Shaffer", 10 | "email": "bshafs@gmail.com", 11 | "homepage":"http://brentertainment.com" 12 | } 13 | ], 14 | "homepage": "http://github.com/bshaffer/oauth2-server-php", 15 | "autoload": { 16 | "psr-0": { "OAuth2": "src/" } 17 | }, 18 | "require":{ 19 | "php":">=7.2" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^7.5||^8.0", 23 | "aws/aws-sdk-php": "^2.8", 24 | "firebase/php-jwt": "^6.4", 25 | "predis/predis": "^1.1", 26 | "thobbs/phpcassa": "dev-master", 27 | "yoast/phpunit-polyfills": "^1.0" 28 | }, 29 | "suggest": { 30 | "predis/predis": "Required to use Redis storage", 31 | "thobbs/phpcassa": "Required to use Cassandra storage", 32 | "aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage", 33 | "firebase/php-jwt": "~v6.4 is required to use JWT features", 34 | "mongodb/mongodb": "^1.1 is required to use MongoDB storage" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/OAuth2/Autoloader.php: -------------------------------------------------------------------------------- 1 | 9 | * @license MIT License 10 | */ 11 | class Autoloader 12 | { 13 | /** 14 | * @var string 15 | */ 16 | private $dir; 17 | 18 | /** 19 | * @param string $dir 20 | */ 21 | public function __construct($dir = null) 22 | { 23 | if (is_null($dir)) { 24 | $dir = dirname(__FILE__).'/..'; 25 | } 26 | $this->dir = $dir; 27 | } 28 | 29 | /** 30 | * Registers OAuth2\Autoloader as an SPL autoloader. 31 | */ 32 | public static function register($dir = null) 33 | { 34 | ini_set('unserialize_callback_func', 'spl_autoload_call'); 35 | spl_autoload_register(array(new self($dir), 'autoload')); 36 | } 37 | 38 | /** 39 | * Handles autoloading of classes. 40 | * 41 | * @param string $class - A class name. 42 | * @return void 43 | */ 44 | public function autoload($class) 45 | { 46 | if (0 !== strpos($class, 'OAuth2')) { 47 | return; 48 | } 49 | 50 | if (file_exists($file = $this->dir.'/'.str_replace('\\', '/', $class).'.php')) { 51 | require $file; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/OAuth2/ClientAssertionType/ClientAssertionTypeInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class HttpBasic implements ClientAssertionTypeInterface 16 | { 17 | private $clientData; 18 | 19 | protected $storage; 20 | protected $config; 21 | 22 | /** 23 | * Config array $config should look as follows: 24 | * @code 25 | * $config = array( 26 | * 'allow_credentials_in_request_body' => true, // whether to look for credentials in the POST body in addition to the Authorize HTTP Header 27 | * 'allow_public_clients' => true // if true, "public clients" (clients without a secret) may be authenticated 28 | * ); 29 | * @endcode 30 | * 31 | * @param ClientCredentialsInterface $storage Storage 32 | * @param array $config Configuration options for the server 33 | */ 34 | public function __construct(ClientCredentialsInterface $storage, array $config = array()) 35 | { 36 | $this->storage = $storage; 37 | $this->config = array_merge(array( 38 | 'allow_credentials_in_request_body' => true, 39 | 'allow_public_clients' => true, 40 | ), $config); 41 | } 42 | 43 | /** 44 | * Validate the OAuth request 45 | * 46 | * @param RequestInterface $request 47 | * @param ResponseInterface $response 48 | * @return bool|mixed 49 | * @throws LogicException 50 | */ 51 | public function validateRequest(RequestInterface $request, ResponseInterface $response) 52 | { 53 | if (!$clientData = $this->getClientCredentials($request, $response)) { 54 | return false; 55 | } 56 | 57 | if (!isset($clientData['client_id'])) { 58 | throw new LogicException('the clientData array must have "client_id" set'); 59 | } 60 | 61 | if (!isset($clientData['client_secret']) || $clientData['client_secret'] == '') { 62 | if (!$this->config['allow_public_clients']) { 63 | $response->setError(400, 'invalid_client', 'client credentials are required'); 64 | 65 | return false; 66 | } 67 | 68 | if (!$this->storage->isPublicClient($clientData['client_id'])) { 69 | $response->setError(400, 'invalid_client', 'This client is invalid or must authenticate using a client secret'); 70 | 71 | return false; 72 | } 73 | } elseif ($this->storage->checkClientCredentials($clientData['client_id'], $clientData['client_secret']) === false) { 74 | $response->setError(400, 'invalid_client', 'The client credentials are invalid'); 75 | 76 | return false; 77 | } 78 | 79 | $this->clientData = $clientData; 80 | 81 | return true; 82 | } 83 | 84 | /** 85 | * Get the client id 86 | * 87 | * @return mixed 88 | */ 89 | public function getClientId() 90 | { 91 | return $this->clientData['client_id']; 92 | } 93 | 94 | /** 95 | * Internal function used to get the client credentials from HTTP basic 96 | * auth or POST data. 97 | * 98 | * According to the spec (draft 20), the client_id can be provided in 99 | * the Basic Authorization header (recommended) or via GET/POST. 100 | * 101 | * @param RequestInterface $request 102 | * @param ResponseInterface $response 103 | * @return array|null A list containing the client identifier and password, for example: 104 | * @code 105 | * return array( 106 | * "client_id" => CLIENT_ID, // REQUIRED the client id 107 | * "client_secret" => CLIENT_SECRET, // OPTIONAL the client secret (may be omitted for public clients) 108 | * ); 109 | * @endcode 110 | * 111 | * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 112 | * 113 | * @ingroup oauth2_section_2 114 | */ 115 | public function getClientCredentials(RequestInterface $request, ?ResponseInterface $response = null) 116 | { 117 | if (!is_null($request->headers('PHP_AUTH_USER')) && !is_null($request->headers('PHP_AUTH_PW'))) { 118 | return array('client_id' => $request->headers('PHP_AUTH_USER'), 'client_secret' => $request->headers('PHP_AUTH_PW')); 119 | } 120 | 121 | if ($this->config['allow_credentials_in_request_body']) { 122 | // Using POST for HttpBasic authorization is not recommended, but is supported by specification 123 | if (!is_null($request->request('client_id'))) { 124 | /** 125 | * client_secret can be null if the client's password is an empty string 126 | * @see http://tools.ietf.org/html/rfc6749#section-2.3.1 127 | */ 128 | return array('client_id' => $request->request('client_id'), 'client_secret' => $request->request('client_secret')); 129 | } 130 | } 131 | 132 | if ($response) { 133 | $message = $this->config['allow_credentials_in_request_body'] ? ' or body' : ''; 134 | $response->setError(400, 'invalid_client', 'Client credentials were not found in the headers'.$message); 135 | } 136 | 137 | return null; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/OAuth2/Controller/AuthorizeControllerInterface.php: -------------------------------------------------------------------------------- 1 | somehowDetermineUserId(); 16 | * $is_authorized = $this->somehowDetermineUserAuthorization(); 17 | * $response = new OAuth2\Response(); 18 | * $authorizeController->handleAuthorizeRequest( 19 | * OAuth2\Request::createFromGlobals(), 20 | * $response, 21 | * $is_authorized, 22 | * $user_id 23 | * ); 24 | * $response->send(); 25 | * @endcode 26 | */ 27 | interface AuthorizeControllerInterface 28 | { 29 | /** 30 | * List of possible authentication response types. 31 | * The "authorization_code" mechanism exclusively supports 'code' 32 | * and the "implicit" mechanism exclusively supports 'token'. 33 | * 34 | * @var string 35 | * @see http://tools.ietf.org/html/rfc6749#section-4.1.1 36 | * @see http://tools.ietf.org/html/rfc6749#section-4.2.1 37 | */ 38 | const RESPONSE_TYPE_AUTHORIZATION_CODE = 'code'; 39 | const RESPONSE_TYPE_ACCESS_TOKEN = 'token'; 40 | 41 | /** 42 | * Handle the OAuth request 43 | * 44 | * @param RequestInterface $request 45 | * @param ResponseInterface $response 46 | * @param $is_authorized 47 | * @param null $user_id 48 | * @return mixed 49 | */ 50 | public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null); 51 | 52 | /** 53 | * @param RequestInterface $request 54 | * @param ResponseInterface $response 55 | * @return bool 56 | */ 57 | public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response); 58 | } 59 | -------------------------------------------------------------------------------- /src/OAuth2/Controller/ResourceController.php: -------------------------------------------------------------------------------- 1 | tokenType = $tokenType; 53 | $this->tokenStorage = $tokenStorage; 54 | 55 | $this->config = array_merge(array( 56 | 'www_realm' => 'Service', 57 | ), $config); 58 | 59 | if (is_null($scopeUtil)) { 60 | $scopeUtil = new Scope(); 61 | } 62 | $this->scopeUtil = $scopeUtil; 63 | } 64 | 65 | /** 66 | * Verify the resource request 67 | * 68 | * @param RequestInterface $request 69 | * @param ResponseInterface $response 70 | * @param null $scope 71 | * @return bool 72 | */ 73 | public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null) 74 | { 75 | $token = $this->getAccessTokenData($request, $response); 76 | 77 | // Check if we have token data 78 | if (is_null($token)) { 79 | return false; 80 | } 81 | 82 | /** 83 | * Check scope, if provided 84 | * If token doesn't have a scope, it's null/empty, or it's insufficient, then throw 403 85 | * @see http://tools.ietf.org/html/rfc6750#section-3.1 86 | */ 87 | if ($scope && (!isset($token["scope"]) || !$token["scope"] || !$this->scopeUtil->checkScope($scope, $token["scope"]))) { 88 | $response->setError(403, 'insufficient_scope', 'The request requires higher privileges than provided by the access token'); 89 | $response->addHttpHeaders(array( 90 | 'WWW-Authenticate' => sprintf('%s realm="%s", scope="%s", error="%s", error_description="%s"', 91 | $this->tokenType->getTokenType(), 92 | $this->config['www_realm'], 93 | $scope, 94 | $response->getParameter('error'), 95 | $response->getParameter('error_description') 96 | ) 97 | )); 98 | 99 | return false; 100 | } 101 | 102 | // allow retrieval of the token 103 | $this->token = $token; 104 | 105 | return (bool) $token; 106 | } 107 | 108 | /** 109 | * Get access token data. 110 | * 111 | * @param RequestInterface $request 112 | * @param ResponseInterface $response 113 | * @return array|null 114 | */ 115 | public function getAccessTokenData(RequestInterface $request, ResponseInterface $response) 116 | { 117 | // Get the token parameter 118 | if ($token_param = $this->tokenType->getAccessTokenParameter($request, $response)) { 119 | // Get the stored token data (from the implementing subclass) 120 | // Check we have a well formed token 121 | // Check token expiration (expires is a mandatory paramter) 122 | if (!$token = $this->tokenStorage->getAccessToken($token_param)) { 123 | $response->setError(401, 'invalid_token', 'The access token provided is invalid'); 124 | } elseif (!isset($token["expires"]) || !isset($token["client_id"])) { 125 | $response->setError(401, 'malformed_token', 'Malformed token (missing "expires")'); 126 | } elseif (time() > $token["expires"]) { 127 | $response->setError(401, 'invalid_token', 'The access token provided has expired'); 128 | } else { 129 | return $token; 130 | } 131 | } 132 | 133 | $authHeader = sprintf('%s realm="%s"', $this->tokenType->getTokenType(), $this->config['www_realm']); 134 | 135 | if ($error = $response->getParameter('error')) { 136 | $authHeader = sprintf('%s, error="%s"', $authHeader, $error); 137 | if ($error_description = $response->getParameter('error_description')) { 138 | $authHeader = sprintf('%s, error_description="%s"', $authHeader, $error_description); 139 | } 140 | } 141 | 142 | $response->addHttpHeaders(array('WWW-Authenticate' => $authHeader)); 143 | 144 | return null; 145 | } 146 | 147 | /** 148 | * convenience method to allow retrieval of the token. 149 | * 150 | * @return array 151 | */ 152 | public function getToken() 153 | { 154 | return $this->token; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/OAuth2/Controller/ResourceControllerInterface.php: -------------------------------------------------------------------------------- 1 | verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) { 15 | * $response->send(); // authorization failed 16 | * die(); 17 | * } 18 | * return json_encode($resource); // valid token! Send the stuff! 19 | * @endcode 20 | */ 21 | interface ResourceControllerInterface 22 | { 23 | /** 24 | * Verify the resource request 25 | * 26 | * @param RequestInterface $request - Request object 27 | * @param ResponseInterface $response - Response object 28 | * @param string $scope 29 | * @return mixed 30 | */ 31 | public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null); 32 | 33 | /** 34 | * Get access token data. 35 | * 36 | * @param RequestInterface $request - Request object 37 | * @param ResponseInterface $response - Response object 38 | * @return mixed 39 | */ 40 | public function getAccessTokenData(RequestInterface $request, ResponseInterface $response); 41 | } 42 | -------------------------------------------------------------------------------- /src/OAuth2/Controller/TokenControllerInterface.php: -------------------------------------------------------------------------------- 1 | handleTokenRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response()); 15 | * $response->send(); 16 | * @endcode 17 | */ 18 | interface TokenControllerInterface 19 | { 20 | /** 21 | * Handle the token request 22 | * 23 | * @param RequestInterface $request - The current http request 24 | * @param ResponseInterface $response - An instance of OAuth2\ResponseInterface to contain the response data 25 | */ 26 | public function handleTokenRequest(RequestInterface $request, ResponseInterface $response); 27 | 28 | /** 29 | * Grant or deny a requested access token. 30 | * This would be called from the "/token" endpoint as defined in the spec. 31 | * You can call your endpoint whatever you want. 32 | * 33 | * @param RequestInterface $request - Request object to grant access token 34 | * @param ResponseInterface $response - Response object 35 | * 36 | * @return mixed 37 | */ 38 | public function grantAccessToken(RequestInterface $request, ResponseInterface $response); 39 | } 40 | -------------------------------------------------------------------------------- /src/OAuth2/Encryption/EncryptionInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class FirebaseJwt implements EncryptionInterface 13 | { 14 | public function __construct() 15 | { 16 | if (!class_exists(JWT::class)) { 17 | throw new \ErrorException('firebase/php-jwt must be installed to use this feature. You can do this by running "composer require firebase/php-jwt"'); 18 | } 19 | } 20 | 21 | public function encode($payload, $key, $alg = 'HS256', $keyId = null) 22 | { 23 | return JWT::encode($payload, $key, $alg, $keyId); 24 | } 25 | 26 | public function decode($jwt, $key = null, $allowedAlgorithms = null) 27 | { 28 | try { 29 | //Maintain BC: Do not verify if no algorithms are passed in. 30 | if (!$allowedAlgorithms) { 31 | $tks = \explode('.', $jwt); 32 | if (\count($tks) === 3) { 33 | [$headb64] = $tks; 34 | $headerRaw = JWT::urlsafeB64Decode($headb64); 35 | if (($header = JWT::jsonDecode($headerRaw))) { 36 | $key = new Key($key, $header->alg); 37 | } 38 | } 39 | } elseif(is_array($allowedAlgorithms)) { 40 | $key = new Key($key, $allowedAlgorithms[0]); 41 | } else { 42 | $key = new Key($key, $allowedAlgorithms); 43 | } 44 | 45 | return (array) JWT::decode($jwt, $key); 46 | } catch (\Exception $e) { 47 | return false; 48 | } 49 | } 50 | 51 | public function urlSafeB64Encode($data) 52 | { 53 | return JWT::urlsafeB64Encode($data); 54 | } 55 | 56 | public function urlSafeB64Decode($b64) 57 | { 58 | return JWT::urlsafeB64Decode($b64); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/OAuth2/Encryption/Jwt.php: -------------------------------------------------------------------------------- 1 | generateJwtHeader($payload, $algo); 23 | 24 | $segments = array( 25 | $this->urlSafeB64Encode(json_encode($header)), 26 | $this->urlSafeB64Encode(json_encode($payload)) 27 | ); 28 | 29 | $signing_input = implode('.', $segments); 30 | 31 | $signature = $this->sign($signing_input, $key, $algo); 32 | $segments[] = $this->urlsafeB64Encode($signature); 33 | 34 | return implode('.', $segments); 35 | } 36 | 37 | /** 38 | * @param string $jwt 39 | * @param null $key 40 | * @param array|bool $allowedAlgorithms 41 | * @return bool|mixed 42 | */ 43 | public function decode($jwt, $key = null, $allowedAlgorithms = true) 44 | { 45 | if (!strpos($jwt, '.')) { 46 | return false; 47 | } 48 | 49 | $tks = explode('.', $jwt); 50 | 51 | if (count($tks) != 3) { 52 | return false; 53 | } 54 | 55 | list($headb64, $payloadb64, $cryptob64) = $tks; 56 | 57 | if (null === ($header = json_decode($this->urlSafeB64Decode($headb64), true))) { 58 | return false; 59 | } 60 | 61 | if (null === $payload = json_decode($this->urlSafeB64Decode($payloadb64), true)) { 62 | return false; 63 | } 64 | 65 | $sig = $this->urlSafeB64Decode($cryptob64); 66 | 67 | if ((bool) $allowedAlgorithms) { 68 | if (!isset($header['alg'])) { 69 | return false; 70 | } 71 | 72 | // check if bool arg supplied here to maintain BC 73 | if (is_array($allowedAlgorithms) && !in_array($header['alg'], $allowedAlgorithms)) { 74 | return false; 75 | } 76 | 77 | if (!$this->verifySignature($sig, "$headb64.$payloadb64", $key, $header['alg'])) { 78 | return false; 79 | } 80 | } 81 | 82 | return $payload; 83 | } 84 | 85 | /** 86 | * @param $signature 87 | * @param $input 88 | * @param $key 89 | * @param string $algo 90 | * @return bool 91 | * @throws InvalidArgumentException 92 | */ 93 | private function verifySignature($signature, $input, $key, $algo = 'HS256') 94 | { 95 | // use constants when possible, for HipHop support 96 | switch ($algo) { 97 | case'HS256': 98 | case'HS384': 99 | case'HS512': 100 | return $this->hash_equals( 101 | $this->sign($input, $key, $algo), 102 | $signature 103 | ); 104 | 105 | case 'RS256': 106 | return openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256') === 1; 107 | 108 | case 'RS384': 109 | return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384') === 1; 110 | 111 | case 'RS512': 112 | return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512') === 1; 113 | 114 | default: 115 | throw new InvalidArgumentException("Unsupported or invalid signing algorithm."); 116 | } 117 | } 118 | 119 | /** 120 | * @param $input 121 | * @param $key 122 | * @param string $algo 123 | * @return string 124 | * @throws Exception 125 | */ 126 | private function sign($input, $key, $algo = 'HS256') 127 | { 128 | switch ($algo) { 129 | case 'HS256': 130 | return hash_hmac('sha256', $input, $key, true); 131 | 132 | case 'HS384': 133 | return hash_hmac('sha384', $input, $key, true); 134 | 135 | case 'HS512': 136 | return hash_hmac('sha512', $input, $key, true); 137 | 138 | case 'RS256': 139 | return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256'); 140 | 141 | case 'RS384': 142 | return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384'); 143 | 144 | case 'RS512': 145 | return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512'); 146 | 147 | default: 148 | throw new Exception("Unsupported or invalid signing algorithm."); 149 | } 150 | } 151 | 152 | /** 153 | * @param $input 154 | * @param $key 155 | * @param string $algo 156 | * @return mixed 157 | * @throws Exception 158 | */ 159 | private function generateRSASignature($input, $key, $algo) 160 | { 161 | if (!openssl_sign($input, $signature, $key, $algo)) { 162 | throw new Exception("Unable to sign data."); 163 | } 164 | 165 | return $signature; 166 | } 167 | 168 | /** 169 | * @param string $data 170 | * @return string 171 | */ 172 | public function urlSafeB64Encode($data) 173 | { 174 | $b64 = base64_encode($data); 175 | $b64 = str_replace(array('+', '/', "\r", "\n", '='), 176 | array('-', '_'), 177 | $b64); 178 | 179 | return $b64; 180 | } 181 | 182 | /** 183 | * @param string $b64 184 | * @return mixed|string 185 | */ 186 | public function urlSafeB64Decode($b64) 187 | { 188 | $b64 = str_replace(array('-', '_'), 189 | array('+', '/'), 190 | $b64); 191 | 192 | return base64_decode($b64); 193 | } 194 | 195 | /** 196 | * Override to create a custom header 197 | */ 198 | protected function generateJwtHeader($payload, $algorithm) 199 | { 200 | return array( 201 | 'typ' => 'JWT', 202 | 'alg' => $algorithm, 203 | ); 204 | } 205 | 206 | /** 207 | * @param string $a 208 | * @param string $b 209 | * @return bool 210 | */ 211 | protected function hash_equals($a, $b) 212 | { 213 | if (function_exists('hash_equals')) { 214 | return hash_equals($a, $b); 215 | } 216 | $diff = strlen($a) ^ strlen($b); 217 | for ($i = 0; $i < strlen($a) && $i < strlen($b); $i++) { 218 | $diff |= ord($a[$i]) ^ ord($b[$i]); 219 | } 220 | 221 | return $diff === 0; 222 | } 223 | } -------------------------------------------------------------------------------- /src/OAuth2/GrantType/AuthorizationCode.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class AuthorizationCode implements GrantTypeInterface 15 | { 16 | /** 17 | * @var AuthorizationCodeInterface 18 | */ 19 | protected $storage; 20 | 21 | /** 22 | * @var array 23 | */ 24 | protected $authCode; 25 | 26 | /** 27 | * @param AuthorizationCodeInterface $storage - REQUIRED Storage class for retrieving authorization code information 28 | */ 29 | public function __construct(AuthorizationCodeInterface $storage) 30 | { 31 | $this->storage = $storage; 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getQueryStringIdentifier() 38 | { 39 | return 'authorization_code'; 40 | } 41 | 42 | /** 43 | * Validate the OAuth request 44 | * 45 | * @param RequestInterface $request 46 | * @param ResponseInterface $response 47 | * @return bool 48 | * @throws Exception 49 | */ 50 | public function validateRequest(RequestInterface $request, ResponseInterface $response) 51 | { 52 | if (!$request->request('code')) { 53 | $response->setError(400, 'invalid_request', 'Missing parameter: "code" is required'); 54 | 55 | return false; 56 | } 57 | 58 | $code = $request->request('code'); 59 | if (!$authCode = $this->storage->getAuthorizationCode($code)) { 60 | $response->setError(400, 'invalid_grant', 'Authorization code doesn\'t exist or is invalid for the client'); 61 | 62 | return false; 63 | } 64 | 65 | /* 66 | * 4.1.3 - ensure that the "redirect_uri" parameter is present if the "redirect_uri" parameter was included in the initial authorization request 67 | * @uri - http://tools.ietf.org/html/rfc6749#section-4.1.3 68 | */ 69 | if (isset($authCode['redirect_uri']) && $authCode['redirect_uri']) { 70 | if (!$request->request('redirect_uri') || urldecode($request->request('redirect_uri')) != urldecode($authCode['redirect_uri'])) { 71 | $response->setError(400, 'redirect_uri_mismatch', "The redirect URI is missing or do not match", "#section-4.1.3"); 72 | 73 | return false; 74 | } 75 | } 76 | 77 | if (!isset($authCode['expires'])) { 78 | throw new \Exception('Storage must return authcode with a value for "expires"'); 79 | } 80 | 81 | if ($authCode["expires"] < time()) { 82 | $response->setError(400, 'invalid_grant', "The authorization code has expired"); 83 | 84 | return false; 85 | } 86 | 87 | if (isset($authCode['code_challenge']) && $authCode['code_challenge']) { 88 | if (!($code_verifier = $request->request('code_verifier'))) { 89 | $response->setError(400, 'code_verifier_missing', "The PKCE code verifier parameter is required."); 90 | 91 | return false; 92 | } 93 | // Validate code_verifier according to RFC-7636 94 | // @see: https://tools.ietf.org/html/rfc7636#section-4.1 95 | if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $code_verifier) !== 1) { 96 | $response->setError(400, 'code_verifier_invalid', "The PKCE code verifier parameter is invalid."); 97 | 98 | return false; 99 | } 100 | $code_verifier = $request->request('code_verifier'); 101 | switch ($authCode['code_challenge_method']) { 102 | case 'S256': 103 | $code_verifier_hashed = strtr(rtrim(base64_encode(hash('sha256', $code_verifier, true)), '='), '+/', '-_'); 104 | break; 105 | 106 | case 'plain': 107 | $code_verifier_hashed = $code_verifier; 108 | break; 109 | 110 | default: 111 | $response->setError(400, 'code_challenge_method_invalid', "Unknown PKCE code challenge method."); 112 | 113 | return FALSE; 114 | } 115 | if ($code_verifier_hashed !== $authCode['code_challenge']) { 116 | $response->setError(400, 'code_verifier_mismatch', "The PKCE code verifier parameter does not match the code challenge."); 117 | 118 | return FALSE; 119 | } 120 | } 121 | 122 | if (!isset($authCode['code'])) { 123 | $authCode['code'] = $code; // used to expire the code after the access token is granted 124 | } 125 | 126 | $this->authCode = $authCode; 127 | 128 | return true; 129 | } 130 | 131 | /** 132 | * Get the client id 133 | * 134 | * @return mixed 135 | */ 136 | public function getClientId() 137 | { 138 | return $this->authCode['client_id']; 139 | } 140 | 141 | /** 142 | * Get the scope 143 | * 144 | * @return string 145 | */ 146 | public function getScope() 147 | { 148 | return isset($this->authCode['scope']) ? $this->authCode['scope'] : null; 149 | } 150 | 151 | /** 152 | * Get the user id 153 | * 154 | * @return mixed 155 | */ 156 | public function getUserId() 157 | { 158 | return isset($this->authCode['user_id']) ? $this->authCode['user_id'] : null; 159 | } 160 | 161 | /** 162 | * Create access token 163 | * 164 | * @param AccessTokenInterface $accessToken 165 | * @param mixed $client_id - client identifier related to the access token. 166 | * @param mixed $user_id - user id associated with the access token 167 | * @param string $scope - scopes to be stored in space-separated string. 168 | * @return array 169 | */ 170 | public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) 171 | { 172 | $token = $accessToken->createAccessToken($client_id, $user_id, $scope); 173 | $this->storage->expireAuthorizationCode($this->authCode['code']); 174 | 175 | return $token; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/OAuth2/GrantType/ClientCredentials.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @see HttpBasic 13 | */ 14 | class ClientCredentials extends HttpBasic implements GrantTypeInterface 15 | { 16 | /** 17 | * @var array 18 | */ 19 | private $clientData; 20 | 21 | /** 22 | * @param ClientCredentialsInterface $storage 23 | * @param array $config 24 | */ 25 | public function __construct(ClientCredentialsInterface $storage, array $config = array()) 26 | { 27 | /** 28 | * The client credentials grant type MUST only be used by confidential clients 29 | * 30 | * @see http://tools.ietf.org/html/rfc6749#section-4.4 31 | */ 32 | $config['allow_public_clients'] = false; 33 | 34 | parent::__construct($storage, $config); 35 | } 36 | 37 | /** 38 | * Get query string identifier 39 | * 40 | * @return string 41 | */ 42 | public function getQueryStringIdentifier() 43 | { 44 | return 'client_credentials'; 45 | } 46 | 47 | /** 48 | * Get scope 49 | * 50 | * @return string|null 51 | */ 52 | public function getScope() 53 | { 54 | $this->loadClientData(); 55 | 56 | return isset($this->clientData['scope']) ? $this->clientData['scope'] : null; 57 | } 58 | 59 | /** 60 | * Get user id 61 | * 62 | * @return mixed 63 | */ 64 | public function getUserId() 65 | { 66 | $this->loadClientData(); 67 | 68 | return isset($this->clientData['user_id']) ? $this->clientData['user_id'] : null; 69 | } 70 | 71 | /** 72 | * Create access token 73 | * 74 | * @param AccessTokenInterface $accessToken 75 | * @param mixed $client_id - client identifier related to the access token. 76 | * @param mixed $user_id - user id associated with the access token 77 | * @param string $scope - scopes to be stored in space-separated string. 78 | * @return array 79 | */ 80 | public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) 81 | { 82 | /** 83 | * Client Credentials Grant does NOT include a refresh token 84 | * 85 | * @see http://tools.ietf.org/html/rfc6749#section-4.4.3 86 | */ 87 | $includeRefreshToken = false; 88 | 89 | return $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); 90 | } 91 | 92 | private function loadClientData() 93 | { 94 | if (!$this->clientData) { 95 | $this->clientData = $this->storage->getClientDetails($this->getClientId()); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/OAuth2/GrantType/GrantTypeInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class RefreshToken implements GrantTypeInterface 14 | { 15 | /** 16 | * @var array 17 | */ 18 | private $refreshToken; 19 | 20 | /** 21 | * @var RefreshTokenInterface 22 | */ 23 | protected $storage; 24 | 25 | /** 26 | * @var array 27 | */ 28 | protected $config; 29 | 30 | /** 31 | * @param RefreshTokenInterface $storage - REQUIRED Storage class for retrieving refresh token information 32 | * @param array $config - OPTIONAL Configuration options for the server 33 | * @code 34 | * $config = array( 35 | * 'always_issue_new_refresh_token' => true, // whether to issue a new refresh token upon successful token request 36 | * 'unset_refresh_token_after_use' => true // whether to unset the refresh token after after using 37 | * ); 38 | * @endcode 39 | */ 40 | public function __construct(RefreshTokenInterface $storage, $config = array()) 41 | { 42 | $this->config = array_merge(array( 43 | 'always_issue_new_refresh_token' => false, 44 | 'unset_refresh_token_after_use' => true 45 | ), $config); 46 | 47 | // to preserve B.C. with v1.6 48 | // @see https://github.com/bshaffer/oauth2-server-php/pull/580 49 | // @todo - remove in v2.0 50 | if (isset($config['always_issue_new_refresh_token']) && !isset($config['unset_refresh_token_after_use'])) { 51 | $this->config['unset_refresh_token_after_use'] = $config['always_issue_new_refresh_token']; 52 | } 53 | 54 | $this->storage = $storage; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getQueryStringIdentifier() 61 | { 62 | return 'refresh_token'; 63 | } 64 | 65 | /** 66 | * Validate the OAuth request 67 | * 68 | * @param RequestInterface $request 69 | * @param ResponseInterface $response 70 | * @return bool|mixed|null 71 | */ 72 | public function validateRequest(RequestInterface $request, ResponseInterface $response) 73 | { 74 | if (!$request->request("refresh_token")) { 75 | $response->setError(400, 'invalid_request', 'Missing parameter: "refresh_token" is required'); 76 | 77 | return null; 78 | } 79 | 80 | if (!$refreshToken = $this->storage->getRefreshToken($request->request("refresh_token"))) { 81 | $response->setError(400, 'invalid_grant', 'Invalid refresh token'); 82 | 83 | return null; 84 | } 85 | 86 | if ($refreshToken['expires'] > 0 && $refreshToken["expires"] < time()) { 87 | $response->setError(400, 'invalid_grant', 'Refresh token has expired'); 88 | 89 | return null; 90 | } 91 | 92 | // store the refresh token locally so we can delete it when a new refresh token is generated 93 | $this->refreshToken = $refreshToken; 94 | 95 | return true; 96 | } 97 | 98 | /** 99 | * Get client id 100 | * 101 | * @return mixed 102 | */ 103 | public function getClientId() 104 | { 105 | return $this->refreshToken['client_id']; 106 | } 107 | 108 | /** 109 | * Get user id 110 | * 111 | * @return mixed|null 112 | */ 113 | public function getUserId() 114 | { 115 | return isset($this->refreshToken['user_id']) ? $this->refreshToken['user_id'] : null; 116 | } 117 | 118 | /** 119 | * Get scope 120 | * 121 | * @return null|string 122 | */ 123 | public function getScope() 124 | { 125 | return isset($this->refreshToken['scope']) ? $this->refreshToken['scope'] : null; 126 | } 127 | 128 | /** 129 | * Create access token 130 | * 131 | * @param AccessTokenInterface $accessToken 132 | * @param mixed $client_id - client identifier related to the access token. 133 | * @param mixed $user_id - user id associated with the access token 134 | * @param string $scope - scopes to be stored in space-separated string. 135 | * @return array 136 | */ 137 | public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) 138 | { 139 | /* 140 | * It is optional to force a new refresh token when a refresh token is used. 141 | * However, if a new refresh token is issued, the old one MUST be expired 142 | * @see http://tools.ietf.org/html/rfc6749#section-6 143 | */ 144 | $issueNewRefreshToken = $this->config['always_issue_new_refresh_token']; 145 | $unsetRefreshToken = $this->config['unset_refresh_token_after_use']; 146 | $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $issueNewRefreshToken); 147 | 148 | if ($unsetRefreshToken) { 149 | $this->storage->unsetRefreshToken($this->refreshToken['refresh_token']); 150 | } 151 | 152 | return $token; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/OAuth2/GrantType/UserCredentials.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class UserCredentials implements GrantTypeInterface 15 | { 16 | /** 17 | * @var array 18 | */ 19 | private $userInfo; 20 | 21 | /** 22 | * @var UserCredentialsInterface 23 | */ 24 | protected $storage; 25 | 26 | /** 27 | * @param UserCredentialsInterface $storage - REQUIRED Storage class for retrieving user credentials information 28 | */ 29 | public function __construct(UserCredentialsInterface $storage) 30 | { 31 | $this->storage = $storage; 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getQueryStringIdentifier() 38 | { 39 | return 'password'; 40 | } 41 | 42 | /** 43 | * @param RequestInterface $request 44 | * @param ResponseInterface $response 45 | * @return bool|mixed|null 46 | * 47 | * @throws LogicException 48 | */ 49 | public function validateRequest(RequestInterface $request, ResponseInterface $response) 50 | { 51 | if (!$request->request("password") || !$request->request("username")) { 52 | $response->setError(400, 'invalid_request', 'Missing parameters: "username" and "password" required'); 53 | 54 | return null; 55 | } 56 | 57 | if (!$this->storage->checkUserCredentials($request->request("username"), $request->request("password"))) { 58 | $response->setError(401, 'invalid_grant', 'Invalid username and password combination'); 59 | 60 | return null; 61 | } 62 | 63 | $userInfo = $this->storage->getUserDetails($request->request("username")); 64 | 65 | if (empty($userInfo)) { 66 | $response->setError(400, 'invalid_grant', 'Unable to retrieve user information'); 67 | 68 | return null; 69 | } 70 | 71 | if (!isset($userInfo['user_id'])) { 72 | throw new \LogicException("you must set the user_id on the array returned by getUserDetails"); 73 | } 74 | 75 | $this->userInfo = $userInfo; 76 | 77 | return true; 78 | } 79 | 80 | /** 81 | * Get client id 82 | * 83 | * @return mixed|null 84 | */ 85 | public function getClientId() 86 | { 87 | return null; 88 | } 89 | 90 | /** 91 | * Get user id 92 | * 93 | * @return mixed 94 | */ 95 | public function getUserId() 96 | { 97 | return $this->userInfo['user_id']; 98 | } 99 | 100 | /** 101 | * Get scope 102 | * 103 | * @return null|string 104 | */ 105 | public function getScope() 106 | { 107 | return isset($this->userInfo['scope']) ? $this->userInfo['scope'] : null; 108 | } 109 | 110 | /** 111 | * Create access token 112 | * 113 | * @param AccessTokenInterface $accessToken 114 | * @param mixed $client_id - client identifier related to the access token. 115 | * @param mixed $user_id - user id associated with the access token 116 | * @param string $scope - scopes to be stored in space-separated string. 117 | * @return array 118 | */ 119 | public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) 120 | { 121 | return $accessToken->createAccessToken($client_id, $user_id, $scope); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/OAuth2/OpenID/Controller/AuthorizeController.php: -------------------------------------------------------------------------------- 1 | query('prompt', 'consent'); 40 | if ($prompt == 'none') { 41 | if (is_null($user_id)) { 42 | $error = 'login_required'; 43 | $error_message = 'The user must log in'; 44 | } else { 45 | $error = 'interaction_required'; 46 | $error_message = 'The user must grant access to your application'; 47 | } 48 | } else { 49 | $error = 'consent_required'; 50 | $error_message = 'The user denied access to your application'; 51 | } 52 | 53 | $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->getState(), $error, $error_message); 54 | } 55 | 56 | /** 57 | * @TODO: add dependency injection for the parameters in this method 58 | * 59 | * @param RequestInterface $request 60 | * @param ResponseInterface $response 61 | * @param mixed $user_id 62 | * @return array 63 | */ 64 | protected function buildAuthorizeParameters($request, $response, $user_id) 65 | { 66 | if (!$params = parent::buildAuthorizeParameters($request, $response, $user_id)) { 67 | return; 68 | } 69 | 70 | // Generate an id token if needed. 71 | if ($this->needsIdToken($this->getScope()) && $this->getResponseType() == self::RESPONSE_TYPE_AUTHORIZATION_CODE) { 72 | $params['id_token'] = $this->responseTypes['id_token']->createIdToken($this->getClientId(), $user_id, $this->nonce); 73 | } 74 | 75 | // add the nonce to return with the redirect URI 76 | $params['nonce'] = $this->nonce; 77 | 78 | // Add PKCE code challenge. 79 | $params['code_challenge'] = $this->code_challenge; 80 | $params['code_challenge_method'] = $this->code_challenge_method; 81 | 82 | return $params; 83 | } 84 | 85 | /** 86 | * @param RequestInterface $request 87 | * @param ResponseInterface $response 88 | * @return bool 89 | */ 90 | public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response) 91 | { 92 | if (!parent::validateAuthorizeRequest($request, $response)) { 93 | return false; 94 | } 95 | 96 | $nonce = $request->query('nonce'); 97 | 98 | // Validate required nonce for "id_token" and "id_token token" 99 | if (!$nonce && in_array($this->getResponseType(), array(self::RESPONSE_TYPE_ID_TOKEN, self::RESPONSE_TYPE_ID_TOKEN_TOKEN))) { 100 | $response->setError(400, 'invalid_nonce', 'This application requires you specify a nonce parameter'); 101 | 102 | return false; 103 | } 104 | 105 | $this->nonce = $nonce; 106 | 107 | $code_challenge = $request->query('code_challenge'); 108 | $code_challenge_method = $request->query('code_challenge_method'); 109 | 110 | if ($this->config['enforce_pkce']) { 111 | if (!$code_challenge) { 112 | $response->setError(400, 'missing_code_challenge', 'This application requires you provide a PKCE code challenge'); 113 | 114 | return false; 115 | } 116 | 117 | if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $code_challenge) !== 1) { 118 | $response->setError(400, 'invalid_code_challenge', 'The PKCE code challenge supplied is invalid'); 119 | 120 | return false; 121 | } 122 | 123 | if (!in_array($code_challenge_method, array('plain', 'S256'), true)) { 124 | $response->setError(400, 'missing_code_challenge_method', 'This application requires you specify a PKCE code challenge method'); 125 | 126 | return false; 127 | } 128 | } 129 | 130 | $this->code_challenge = $code_challenge; 131 | $this->code_challenge_method = $code_challenge_method; 132 | 133 | return true; 134 | } 135 | 136 | /** 137 | * Array of valid response types 138 | * 139 | * @return array 140 | */ 141 | protected function getValidResponseTypes() 142 | { 143 | return array( 144 | self::RESPONSE_TYPE_ACCESS_TOKEN, 145 | self::RESPONSE_TYPE_AUTHORIZATION_CODE, 146 | self::RESPONSE_TYPE_ID_TOKEN, 147 | self::RESPONSE_TYPE_ID_TOKEN_TOKEN, 148 | self::RESPONSE_TYPE_CODE_ID_TOKEN, 149 | ); 150 | } 151 | 152 | /** 153 | * Returns whether the current request needs to generate an id token. 154 | * 155 | * ID Tokens are a part of the OpenID Connect specification, so this 156 | * method checks whether OpenID Connect is enabled in the server settings 157 | * and whether the openid scope was requested. 158 | * 159 | * @param string $request_scope - A space-separated string of scopes. 160 | * @return boolean - TRUE if an id token is needed, FALSE otherwise. 161 | */ 162 | public function needsIdToken($request_scope) 163 | { 164 | // see if the "openid" scope exists in the requested scope 165 | return $this->scopeUtil->checkScope('openid', $request_scope); 166 | } 167 | 168 | /** 169 | * @return mixed 170 | */ 171 | public function getNonce() 172 | { 173 | return $this->nonce; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php: -------------------------------------------------------------------------------- 1 | userClaimsStorage = $userClaimsStorage; 38 | } 39 | 40 | /** 41 | * Handle the user info request 42 | * 43 | * @param RequestInterface $request 44 | * @param ResponseInterface $response 45 | * @return void 46 | */ 47 | public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response) 48 | { 49 | if (!$this->verifyResourceRequest($request, $response, 'openid')) { 50 | return; 51 | } 52 | 53 | $token = $this->getToken(); 54 | $claims = $this->userClaimsStorage->getUserClaims($token['user_id'], $token['scope']); 55 | // The sub Claim MUST always be returned in the UserInfo Response. 56 | // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse 57 | $claims += array( 58 | 'sub' => $token['user_id'], 59 | ); 60 | $response->addParameters($claims); 61 | } 62 | } -------------------------------------------------------------------------------- /src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php: -------------------------------------------------------------------------------- 1 | handleUserInfoRequest( 15 | * OAuth2\Request::createFromGlobals(), 16 | * $response 17 | * ); 18 | * $response->send(); 19 | * @endcode 20 | */ 21 | interface UserInfoControllerInterface 22 | { 23 | /** 24 | * Handle user info request 25 | * 26 | * @param RequestInterface $request 27 | * @param ResponseInterface $response 28 | */ 29 | public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response); 30 | } 31 | -------------------------------------------------------------------------------- /src/OAuth2/OpenID/GrantType/AuthorizationCode.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class AuthorizationCode extends BaseAuthorizationCode 12 | { 13 | /** 14 | * Create access token 15 | * 16 | * @param AccessTokenInterface $accessToken 17 | * @param mixed $client_id - client identifier related to the access token. 18 | * @param mixed $user_id - user id associated with the access token 19 | * @param string $scope - scopes to be stored in space-separated string. 20 | * @return array 21 | */ 22 | public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope) 23 | { 24 | $includeRefreshToken = true; 25 | if (isset($this->authCode['id_token'])) { 26 | // OpenID Connect requests include the refresh token only if the 27 | // offline_access scope has been requested and granted. 28 | $scopes = explode(' ', trim($scope)); 29 | $includeRefreshToken = in_array('offline_access', $scopes); 30 | } 31 | 32 | $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken); 33 | if (isset($this->authCode['id_token'])) { 34 | $token['id_token'] = $this->authCode['id_token']; 35 | } 36 | 37 | $this->storage->expireAuthorizationCode($this->authCode['code']); 38 | 39 | return $token; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/OAuth2/OpenID/ResponseType/AuthorizationCode.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class AuthorizationCode extends BaseAuthorizationCode implements AuthorizationCodeInterface 12 | { 13 | /** 14 | * Constructor 15 | * 16 | * @param AuthorizationCodeStorageInterface $storage 17 | * @param array $config 18 | */ 19 | public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array()) 20 | { 21 | parent::__construct($storage, $config); 22 | } 23 | 24 | /** 25 | * @param $params 26 | * @param null $user_id 27 | * @return array 28 | */ 29 | public function getAuthorizeResponse($params, $user_id = null) 30 | { 31 | // build the URL to redirect to 32 | $result = array('query' => array()); 33 | 34 | $params += array('scope' => null, 'state' => null, 'id_token' => null, 'code_challenge' => null, 'code_challenge_method' => null); 35 | 36 | $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token'], $params['code_challenge'], $params['code_challenge_method']); 37 | 38 | if (isset($params['state'])) { 39 | $result['query']['state'] = $params['state']; 40 | } 41 | 42 | return array($params['redirect_uri'], $result); 43 | } 44 | 45 | /** 46 | * Handle the creation of the authorization code. 47 | * 48 | * @param mixed $client_id - Client identifier related to the authorization code 49 | * @param mixed $user_id - User ID associated with the authorization code 50 | * @param string $redirect_uri - An absolute URI to which the authorization server will redirect the 51 | * user-agent to when the end-user authorization step is completed. 52 | * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. 53 | * @param string $id_token - OPTIONAL The OpenID Connect id_token. 54 | * 55 | * @return string 56 | * @see http://tools.ietf.org/html/rfc6749#section-4 57 | * @ingroup oauth2_section_4 58 | */ 59 | public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) 60 | { 61 | $code = $this->generateAuthorizationCode(); 62 | $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token, $code_challenge, $code_challenge_method); 63 | 64 | return $code; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface 11 | { 12 | /** 13 | * Handle the creation of the authorization code. 14 | * 15 | * @param mixed $client_id - Client identifier related to the authorization code 16 | * @param mixed $user_id - User ID associated with the authorization code 17 | * @param string $redirect_uri - An absolute URI to which the authorization server will redirect the 18 | * user-agent to when the end-user authorization step is completed. 19 | * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. 20 | * @param string $id_token - OPTIONAL The OpenID Connect id_token. 21 | * @return string 22 | * 23 | * @see http://tools.ietf.org/html/rfc6749#section-4 24 | * @ingroup oauth2_section_4 25 | */ 26 | public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null); 27 | } 28 | -------------------------------------------------------------------------------- /src/OAuth2/OpenID/ResponseType/CodeIdToken.php: -------------------------------------------------------------------------------- 1 | authCode = $authCode; 24 | $this->idToken = $idToken; 25 | } 26 | 27 | /** 28 | * @param array $params 29 | * @param mixed $user_id 30 | * @return mixed 31 | */ 32 | public function getAuthorizeResponse($params, $user_id = null) 33 | { 34 | $result = $this->authCode->getAuthorizeResponse($params, $user_id); 35 | $resultIdToken = $this->idToken->getAuthorizeResponse($params, $user_id); 36 | $result[1]['query']['id_token'] = $resultIdToken[1]['fragment']['id_token']; 37 | 38 | return $result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/OAuth2/OpenID/ResponseType/CodeIdTokenInterface.php: -------------------------------------------------------------------------------- 1 | userClaimsStorage = $userClaimsStorage; 44 | $this->publicKeyStorage = $publicKeyStorage; 45 | if (is_null($encryptionUtil)) { 46 | $encryptionUtil = new Jwt(); 47 | } 48 | $this->encryptionUtil = $encryptionUtil; 49 | 50 | if (!isset($config['issuer'])) { 51 | throw new LogicException('config parameter "issuer" must be set'); 52 | } 53 | $this->config = array_merge(array( 54 | 'id_lifetime' => 3600, 55 | ), $config); 56 | } 57 | 58 | /** 59 | * @param array $params 60 | * @param null $userInfo 61 | * @return array|mixed 62 | */ 63 | public function getAuthorizeResponse($params, $userInfo = null) 64 | { 65 | // build the URL to redirect to 66 | $result = array('query' => array()); 67 | $params += array('scope' => null, 'state' => null, 'nonce' => null); 68 | 69 | // create the id token. 70 | list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo); 71 | $userClaims = $this->userClaimsStorage->getUserClaims($user_id, $params['scope']); 72 | 73 | $id_token = $this->createIdToken($params['client_id'], $userInfo, $params['nonce'], $userClaims, null); 74 | $result["fragment"] = array('id_token' => $id_token); 75 | if (isset($params['state'])) { 76 | $result["fragment"]["state"] = $params['state']; 77 | } 78 | 79 | return array($params['redirect_uri'], $result); 80 | } 81 | 82 | /** 83 | * Create id token 84 | * 85 | * @param string $client_id 86 | * @param mixed $userInfo 87 | * @param mixed $nonce 88 | * @param mixed $userClaims 89 | * @param mixed $access_token 90 | * @return mixed|string 91 | */ 92 | public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null) 93 | { 94 | // pull auth_time from user info if supplied 95 | list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo); 96 | 97 | $token = array( 98 | 'iss' => $this->config['issuer'], 99 | 'sub' => $user_id, 100 | 'aud' => $client_id, 101 | 'iat' => time(), 102 | 'exp' => time() + $this->config['id_lifetime'], 103 | 'auth_time' => $auth_time, 104 | ); 105 | 106 | if ($nonce) { 107 | $token['nonce'] = $nonce; 108 | } 109 | 110 | if ($userClaims) { 111 | $token += $userClaims; 112 | } 113 | 114 | if ($access_token) { 115 | $token['at_hash'] = $this->createAtHash($access_token, $client_id); 116 | } 117 | 118 | return $this->encodeToken($token, $client_id); 119 | } 120 | 121 | /** 122 | * @param $access_token 123 | * @param null $client_id 124 | * @return mixed|string 125 | */ 126 | protected function createAtHash($access_token, $client_id = null) 127 | { 128 | // maps HS256 and RS256 to sha256, etc. 129 | $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); 130 | $hash_algorithm = 'sha' . substr($algorithm, 2); 131 | $hash = hash($hash_algorithm, $access_token, true); 132 | $at_hash = substr($hash, 0, strlen($hash) / 2); 133 | 134 | return $this->encryptionUtil->urlSafeB64Encode($at_hash); 135 | } 136 | 137 | /** 138 | * @param array $token 139 | * @param null $client_id 140 | * @return mixed|string 141 | */ 142 | protected function encodeToken(array $token, $client_id = null) 143 | { 144 | $private_key = $this->publicKeyStorage->getPrivateKey($client_id); 145 | $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); 146 | 147 | return $this->encryptionUtil->encode($token, $private_key, $algorithm); 148 | } 149 | 150 | /** 151 | * @param $userInfo 152 | * @return array 153 | * @throws LogicException 154 | */ 155 | private function getUserIdAndAuthTime($userInfo) 156 | { 157 | $auth_time = null; 158 | 159 | // support an array for user_id / auth_time 160 | if (is_array($userInfo)) { 161 | if (!isset($userInfo['user_id'])) { 162 | throw new LogicException('if $user_id argument is an array, user_id index must be set'); 163 | } 164 | 165 | $auth_time = isset($userInfo['auth_time']) ? $userInfo['auth_time'] : null; 166 | $user_id = $userInfo['user_id']; 167 | } else { 168 | $user_id = $userInfo; 169 | } 170 | 171 | if (is_null($auth_time)) { 172 | $auth_time = time(); 173 | } 174 | 175 | // userInfo is a scalar, and so this is the $user_id. Auth Time is null 176 | return array($user_id, $auth_time); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/OAuth2/OpenID/ResponseType/IdTokenInterface.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 28 | $this->idToken = $idToken; 29 | } 30 | 31 | /** 32 | * @param array $params 33 | * @param mixed $user_id 34 | * @return mixed 35 | */ 36 | public function getAuthorizeResponse($params, $user_id = null) 37 | { 38 | $result = $this->accessToken->getAuthorizeResponse($params, $user_id); 39 | $access_token = $result[1]['fragment']['access_token']; 40 | $id_token = $this->idToken->createIdToken($params['client_id'], $user_id, $params['nonce'], null, $access_token); 41 | $result[1]['fragment']['id_token'] = $id_token; 42 | 43 | return $result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/OAuth2/OpenID/ResponseType/IdTokenTokenInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface 14 | { 15 | /** 16 | * Take the provided authorization code values and store them somewhere. 17 | * 18 | * This function should be the storage counterpart to getAuthCode(). 19 | * 20 | * If storage fails for some reason, we're not currently checking for 21 | * any sort of success/failure, so you should bail out of the script 22 | * and provide a descriptive fail message. 23 | * 24 | * Required for OAuth2::GRANT_TYPE_AUTH_CODE. 25 | * 26 | * @param string $code - authorization code to be stored. 27 | * @param mixed $client_id - client identifier to be stored. 28 | * @param mixed $user_id - user identifier to be stored. 29 | * @param string $redirect_uri - redirect URI(s) to be stored in a space-separated string. 30 | * @param int $expires - expiration to be stored as a Unix timestamp. 31 | * @param string $scope - OPTIONAL scopes to be stored in space-separated string. 32 | * @param string $id_token - OPTIONAL the OpenID Connect id_token. 33 | * 34 | * @ingroup oauth2_section_4 35 | */ 36 | public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null); 37 | } 38 | -------------------------------------------------------------------------------- /src/OAuth2/OpenID/Storage/UserClaimsInterface.php: -------------------------------------------------------------------------------- 1 | value format. 31 | * 32 | * @see http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims 33 | */ 34 | public function getUserClaims($user_id, $scope); 35 | } 36 | -------------------------------------------------------------------------------- /src/OAuth2/RequestInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface AccessTokenInterface extends ResponseTypeInterface 9 | { 10 | /** 11 | * Handle the creation of access token, also issue refresh token if supported / desirable. 12 | * 13 | * @param mixed $client_id - client identifier related to the access token. 14 | * @param mixed $user_id - user ID associated with the access token 15 | * @param string $scope - OPTONAL scopes to be stored in space-separated string. 16 | * @param bool $includeRefreshToken - if true, a new refresh_token will be added to the response 17 | * 18 | * @see http://tools.ietf.org/html/rfc6749#section-5 19 | * @ingroup oauth2_section_5 20 | */ 21 | public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true); 22 | 23 | /** 24 | * Handle the revoking of refresh tokens, and access tokens if supported / desirable 25 | * 26 | * @param $token 27 | * @param $tokenTypeHint 28 | * @return mixed 29 | * 30 | * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x 31 | */ 32 | //public function revokeToken($token, $tokenTypeHint); 33 | } -------------------------------------------------------------------------------- /src/OAuth2/ResponseType/AuthorizationCode.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class AuthorizationCode implements AuthorizationCodeInterface 11 | { 12 | protected $storage; 13 | protected $config; 14 | 15 | public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array()) 16 | { 17 | $this->storage = $storage; 18 | $this->config = array_merge(array( 19 | 'enforce_redirect' => false, 20 | 'auth_code_lifetime' => 30, 21 | ), $config); 22 | } 23 | 24 | public function getAuthorizeResponse($params, $user_id = null) 25 | { 26 | // build the URL to redirect to 27 | $result = array('query' => array()); 28 | 29 | $params += array('scope' => null, 'state' => null, 'code_challenge' => null, 'code_challenge_method' => null); 30 | 31 | $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['code_challenge'], $params['code_challenge_method']); 32 | 33 | if (isset($params['state'])) { 34 | $result['query']['state'] = $params['state']; 35 | } 36 | 37 | return array($params['redirect_uri'], $result); 38 | } 39 | 40 | /** 41 | * Handle the creation of the authorization code. 42 | * 43 | * @param $client_id 44 | * Client identifier related to the authorization code 45 | * @param $user_id 46 | * User ID associated with the authorization code 47 | * @param $redirect_uri 48 | * An absolute URI to which the authorization server will redirect the 49 | * user-agent to when the end-user authorization step is completed. 50 | * @param $scope 51 | * (optional) Scopes to be stored in space-separated string. 52 | * 53 | * @see http://tools.ietf.org/html/rfc6749#section-4 54 | * @ingroup oauth2_section_4 55 | */ 56 | public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $code_challenge = null, $code_challenge_method = null) 57 | { 58 | $code = $this->generateAuthorizationCode(); 59 | $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, null, $code_challenge, $code_challenge_method); 60 | 61 | return $code; 62 | } 63 | 64 | /** 65 | * @return 66 | * TRUE if the grant type requires a redirect_uri, FALSE if not 67 | */ 68 | public function enforceRedirect() 69 | { 70 | return $this->config['enforce_redirect']; 71 | } 72 | 73 | /** 74 | * Generates an unique auth code. 75 | * 76 | * Implementing classes may want to override this function to implement 77 | * other auth code generation schemes. 78 | * 79 | * @return 80 | * An unique auth code. 81 | * 82 | * @ingroup oauth2_section_4 83 | */ 84 | protected function generateAuthorizationCode() 85 | { 86 | $tokenLen = 40; 87 | if (function_exists('random_bytes')) { 88 | $randomData = random_bytes(100); 89 | } elseif (function_exists('openssl_random_pseudo_bytes')) { 90 | $randomData = openssl_random_pseudo_bytes(100); 91 | } elseif (function_exists('mcrypt_create_iv')) { 92 | $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM); 93 | } elseif (@file_exists('/dev/urandom')) { // Get 100 bytes of random data 94 | $randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true); 95 | } else { 96 | $randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true); 97 | } 98 | 99 | return substr(hash('sha512', $randomData), 0, $tokenLen); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/OAuth2/ResponseType/AuthorizationCodeInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface AuthorizationCodeInterface extends ResponseTypeInterface 9 | { 10 | /** 11 | * @return 12 | * TRUE if the grant type requires a redirect_uri, FALSE if not 13 | */ 14 | public function enforceRedirect(); 15 | 16 | /** 17 | * Handle the creation of the authorization code. 18 | * 19 | * @param mixed $client_id - Client identifier related to the authorization code 20 | * @param mixed $user_id - User ID associated with the authorization code 21 | * @param string $redirect_uri - An absolute URI to which the authorization server will redirect the 22 | * user-agent to when the end-user authorization step is completed. 23 | * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. 24 | * @return string 25 | * 26 | * @see http://tools.ietf.org/html/rfc6749#section-4 27 | * @ingroup oauth2_section_4 28 | */ 29 | public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null); 30 | } 31 | -------------------------------------------------------------------------------- /src/OAuth2/ResponseType/JwtAccessToken.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class JwtAccessToken extends AccessToken 16 | { 17 | protected $publicKeyStorage; 18 | protected $encryptionUtil; 19 | 20 | /** 21 | * @param PublicKeyInterface $publicKeyStorage - 22 | * @param AccessTokenStorageInterface $tokenStorage - 23 | * @param RefreshTokenInterface $refreshStorage - 24 | * @param array $config - array with key store_encrypted_token_string (bool true) 25 | * whether the entire encrypted string is stored, 26 | * or just the token ID is stored 27 | * @param EncryptionInterface $encryptionUtil - 28 | */ 29 | public function __construct(?PublicKeyInterface $publicKeyStorage = null, ?AccessTokenStorageInterface $tokenStorage = null, ?RefreshTokenInterface $refreshStorage = null, array $config = array(), ?EncryptionInterface $encryptionUtil = null) 30 | { 31 | $this->publicKeyStorage = $publicKeyStorage; 32 | $config = array_merge(array( 33 | 'store_encrypted_token_string' => true, 34 | 'issuer' => '' 35 | ), $config); 36 | if (is_null($tokenStorage)) { 37 | // a pass-thru, so we can call the parent constructor 38 | $tokenStorage = new Memory(); 39 | } 40 | if (is_null($encryptionUtil)) { 41 | $encryptionUtil = new Jwt(); 42 | } 43 | $this->encryptionUtil = $encryptionUtil; 44 | parent::__construct($tokenStorage, $refreshStorage, $config); 45 | } 46 | 47 | /** 48 | * Handle the creation of access token, also issue refresh token if supported / desirable. 49 | * 50 | * @param mixed $client_id - Client identifier related to the access token. 51 | * @param mixed $user_id - User ID associated with the access token 52 | * @param string $scope - (optional) Scopes to be stored in space-separated string. 53 | * @param bool $includeRefreshToken - If true, a new refresh_token will be added to the response 54 | * @return array - The access token 55 | * 56 | * @see http://tools.ietf.org/html/rfc6749#section-5 57 | * @ingroup oauth2_section_5 58 | */ 59 | public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true) 60 | { 61 | // payload to encrypt 62 | $payload = $this->createPayload($client_id, $user_id, $scope); 63 | 64 | /* 65 | * Encode the payload data into a single JWT access_token string 66 | */ 67 | $access_token = $this->encodeToken($payload, $client_id); 68 | 69 | /* 70 | * Save the token to a secondary storage. This is implemented on the 71 | * OAuth2\Storage\JwtAccessToken side, and will not actually store anything, 72 | * if no secondary storage has been supplied 73 | */ 74 | $token_to_store = $this->config['store_encrypted_token_string'] ? $access_token : $payload['id']; 75 | $this->tokenStorage->setAccessToken($token_to_store, $client_id, $user_id, $this->config['access_lifetime'] ? time() + $this->config['access_lifetime'] : null, $scope); 76 | 77 | // token to return to the client 78 | $token = array( 79 | 'access_token' => $access_token, 80 | 'expires_in' => $this->config['access_lifetime'], 81 | 'token_type' => $this->config['token_type'], 82 | 'scope' => $scope 83 | ); 84 | 85 | /* 86 | * Issue a refresh token also, if we support them 87 | * 88 | * Refresh Tokens are considered supported if an instance of OAuth2\Storage\RefreshTokenInterface 89 | * is supplied in the constructor 90 | */ 91 | if ($includeRefreshToken && $this->refreshStorage) { 92 | $refresh_token = $this->generateRefreshToken(); 93 | $expires = 0; 94 | if ($this->config['refresh_token_lifetime'] > 0) { 95 | $expires = time() + $this->config['refresh_token_lifetime']; 96 | } 97 | $this->refreshStorage->setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope); 98 | $token['refresh_token'] = $refresh_token; 99 | } 100 | 101 | return $token; 102 | } 103 | 104 | /** 105 | * @param array $token 106 | * @param mixed $client_id 107 | * @return mixed 108 | */ 109 | protected function encodeToken(array $token, $client_id = null) 110 | { 111 | $private_key = $this->publicKeyStorage->getPrivateKey($client_id); 112 | $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); 113 | 114 | return $this->encryptionUtil->encode($token, $private_key, $algorithm); 115 | } 116 | 117 | /** 118 | * This function can be used to create custom JWT payloads 119 | * 120 | * @param mixed $client_id - Client identifier related to the access token. 121 | * @param mixed $user_id - User ID associated with the access token 122 | * @param string $scope - (optional) Scopes to be stored in space-separated string. 123 | * @return array - The access token 124 | */ 125 | protected function createPayload($client_id, $user_id, $scope = null) 126 | { 127 | // token to encrypt 128 | $expires = time() + $this->config['access_lifetime']; 129 | $id = $this->generateAccessToken(); 130 | 131 | $payload = array( 132 | 'id' => $id, // for BC (see #591) 133 | 'jti' => $id, 134 | 'iss' => $this->config['issuer'], 135 | 'aud' => $client_id, 136 | 'sub' => $user_id, 137 | 'exp' => $expires, 138 | 'iat' => time(), 139 | 'token_type' => $this->config['token_type'], 140 | 'scope' => $scope 141 | ); 142 | 143 | if (isset($this->config['jwt_extra_payload_callable'])) { 144 | if (!is_callable($this->config['jwt_extra_payload_callable'])) { 145 | throw new \InvalidArgumentException('jwt_extra_payload_callable is not callable'); 146 | } 147 | 148 | $extra = call_user_func($this->config['jwt_extra_payload_callable'], $client_id, $user_id, $scope); 149 | 150 | if (!is_array($extra)) { 151 | throw new \InvalidArgumentException('jwt_extra_payload_callable must return array'); 152 | } 153 | 154 | $payload = array_merge($extra, $payload); 155 | } 156 | 157 | return $payload; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/OAuth2/ResponseType/ResponseTypeInterface.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 34 | } 35 | 36 | /** 37 | * Check if everything in required scope is contained in available scope. 38 | * 39 | * @param string $required_scope - A space-separated string of scopes. 40 | * @param string $available_scope - A space-separated string of scopes. 41 | * @return bool - TRUE if everything in required scope is contained in available scope and FALSE 42 | * if it isn't. 43 | * 44 | * @see http://tools.ietf.org/html/rfc6749#section-7 45 | * 46 | * @ingroup oauth2_section_7 47 | */ 48 | public function checkScope($required_scope, $available_scope) 49 | { 50 | $required_scope = explode(' ', trim($required_scope)); 51 | $available_scope = explode(' ', trim($available_scope)); 52 | 53 | return (count(array_diff($required_scope, $available_scope)) == 0); 54 | } 55 | 56 | /** 57 | * Check if the provided scope exists in storage. 58 | * 59 | * @param string $scope - A space-separated string of scopes. 60 | * @return bool - TRUE if it exists, FALSE otherwise. 61 | */ 62 | public function scopeExists($scope) 63 | { 64 | // Check reserved scopes first. 65 | $scope = explode(' ', trim($scope)); 66 | $reservedScope = $this->getReservedScopes(); 67 | $nonReservedScopes = array_diff($scope, $reservedScope); 68 | if (count($nonReservedScopes) == 0) { 69 | return true; 70 | } else { 71 | // Check the storage for non-reserved scopes. 72 | $nonReservedScopes = implode(' ', $nonReservedScopes); 73 | 74 | return $this->storage->scopeExists($nonReservedScopes); 75 | } 76 | } 77 | 78 | /** 79 | * @param RequestInterface $request 80 | * @return string 81 | */ 82 | public function getScopeFromRequest(RequestInterface $request) 83 | { 84 | // "scope" is valid if passed in either POST or QUERY 85 | return $request->request('scope', $request->query('scope')); 86 | } 87 | 88 | /** 89 | * @param null $client_id 90 | * @return mixed 91 | */ 92 | public function getDefaultScope($client_id = null) 93 | { 94 | return $this->storage->getDefaultScope($client_id); 95 | } 96 | 97 | /** 98 | * Get reserved scopes needed by the server. 99 | * 100 | * In case OpenID Connect is used, these scopes must include: 101 | * 'openid', offline_access'. 102 | * 103 | * @return array - An array of reserved scopes. 104 | */ 105 | public function getReservedScopes() 106 | { 107 | return array('openid', 'offline_access'); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/OAuth2/ScopeInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface AccessTokenInterface 12 | { 13 | /** 14 | * Look up the supplied oauth_token from storage. 15 | * 16 | * We need to retrieve access token data as we create and verify tokens. 17 | * 18 | * @param string $oauth_token - oauth_token to be check with. 19 | * 20 | * @return array|null - An associative array as below, and return NULL if the supplied oauth_token is invalid: 21 | * @code 22 | * array( 23 | * 'expires' => $expires, // Stored expiration in unix timestamp. 24 | * 'client_id' => $client_id, // (optional) Stored client identifier. 25 | * 'user_id' => $user_id, // (optional) Stored user identifier. 26 | * 'scope' => $scope, // (optional) Stored scope values in space-separated string. 27 | * 'id_token' => $id_token // (optional) Stored id_token (if "use_openid_connect" is true). 28 | * ); 29 | * @endcode 30 | * 31 | * @ingroup oauth2_section_7 32 | */ 33 | public function getAccessToken($oauth_token); 34 | 35 | /** 36 | * Store the supplied access token values to storage. 37 | * 38 | * We need to store access token data as we create and verify tokens. 39 | * 40 | * @param string $oauth_token - oauth_token to be stored. 41 | * @param mixed $client_id - client identifier to be stored. 42 | * @param mixed $user_id - user identifier to be stored. 43 | * @param int $expires - expiration to be stored as a Unix timestamp. 44 | * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. 45 | * 46 | * @ingroup oauth2_section_4 47 | */ 48 | public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null); 49 | 50 | /** 51 | * Expire an access token. 52 | * 53 | * This is not explicitly required in the spec, but if defined in a draft RFC for token 54 | * revoking (RFC 7009) https://tools.ietf.org/html/rfc7009 55 | * 56 | * @param $access_token 57 | * Access token to be expired. 58 | * 59 | * @return BOOL true if an access token was unset, false if not 60 | * @ingroup oauth2_section_6 61 | * 62 | * @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x 63 | */ 64 | //public function unsetAccessToken($access_token); 65 | } -------------------------------------------------------------------------------- /src/OAuth2/Storage/AuthorizationCodeInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface AuthorizationCodeInterface 13 | { 14 | /** 15 | * The Authorization Code grant type supports a response type of "code". 16 | * 17 | * @var string 18 | * @see http://tools.ietf.org/html/rfc6749#section-1.4.1 19 | * @see http://tools.ietf.org/html/rfc6749#section-4.2 20 | */ 21 | const RESPONSE_TYPE_CODE = "code"; 22 | 23 | /** 24 | * Fetch authorization code data (probably the most common grant type). 25 | * 26 | * Retrieve the stored data for the given authorization code. 27 | * 28 | * Required for OAuth2::GRANT_TYPE_AUTH_CODE. 29 | * 30 | * @param $code 31 | * Authorization code to be check with. 32 | * 33 | * @return 34 | * An associative array as below, and NULL if the code is invalid 35 | * @code 36 | * return array( 37 | * "client_id" => CLIENT_ID, // REQUIRED Stored client identifier 38 | * "user_id" => USER_ID, // REQUIRED Stored user identifier 39 | * "expires" => EXPIRES, // REQUIRED Stored expiration in unix timestamp 40 | * "redirect_uri" => REDIRECT_URI, // REQUIRED Stored redirect URI 41 | * "scope" => SCOPE, // OPTIONAL Stored scope values in space-separated string 42 | * ); 43 | * @endcode 44 | * 45 | * @see http://tools.ietf.org/html/rfc6749#section-4.1 46 | * 47 | * @ingroup oauth2_section_4 48 | */ 49 | public function getAuthorizationCode($code); 50 | 51 | /** 52 | * Take the provided authorization code values and store them somewhere. 53 | * 54 | * This function should be the storage counterpart to getAuthCode(). 55 | * 56 | * If storage fails for some reason, we're not currently checking for 57 | * any sort of success/failure, so you should bail out of the script 58 | * and provide a descriptive fail message. 59 | * 60 | * Required for OAuth2::GRANT_TYPE_AUTH_CODE. 61 | * 62 | * @param string $code - Authorization code to be stored. 63 | * @param mixed $client_id - Client identifier to be stored. 64 | * @param mixed $user_id - User identifier to be stored. 65 | * @param string $redirect_uri - Redirect URI(s) to be stored in a space-separated string. 66 | * @param int $expires - Expiration to be stored as a Unix timestamp. 67 | * @param string $scope - OPTIONAL Scopes to be stored in space-separated string. 68 | * 69 | * @ingroup oauth2_section_4 70 | */ 71 | public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null); 72 | 73 | /** 74 | * once an Authorization Code is used, it must be expired 75 | * 76 | * @see http://tools.ietf.org/html/rfc6749#section-4.1.2 77 | * 78 | * The client MUST NOT use the authorization code 79 | * more than once. If an authorization code is used more than 80 | * once, the authorization server MUST deny the request and SHOULD 81 | * revoke (when possible) all tokens previously issued based on 82 | * that authorization code 83 | * 84 | */ 85 | public function expireAuthorizationCode($code); 86 | } 87 | -------------------------------------------------------------------------------- /src/OAuth2/Storage/ClientCredentialsInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface ClientCredentialsInterface extends ClientInterface 12 | { 13 | 14 | /** 15 | * Make sure that the client credentials is valid. 16 | * 17 | * @param $client_id 18 | * Client identifier to be check with. 19 | * @param $client_secret 20 | * (optional) If a secret is required, check that they've given the right one. 21 | * 22 | * @return 23 | * TRUE if the client credentials are valid, and MUST return FALSE if it isn't. 24 | * @endcode 25 | * 26 | * @see http://tools.ietf.org/html/rfc6749#section-3.1 27 | * 28 | * @ingroup oauth2_section_3 29 | */ 30 | public function checkClientCredentials($client_id, $client_secret = null); 31 | 32 | /** 33 | * Determine if the client is a "public" client, and therefore 34 | * does not require passing credentials for certain grant types 35 | * 36 | * @param $client_id 37 | * Client identifier to be check with. 38 | * 39 | * @return 40 | * TRUE if the client is public, and FALSE if it isn't. 41 | * @endcode 42 | * 43 | * @see http://tools.ietf.org/html/rfc6749#section-2.3 44 | * @see https://github.com/bshaffer/oauth2-server-php/issues/257 45 | * 46 | * @ingroup oauth2_section_2 47 | */ 48 | public function isPublicClient($client_id); 49 | } 50 | -------------------------------------------------------------------------------- /src/OAuth2/Storage/ClientInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface ClientInterface 12 | { 13 | /** 14 | * Get client details corresponding client_id. 15 | * 16 | * OAuth says we should store request URIs for each registered client. 17 | * Implement this function to grab the stored URI for a given client id. 18 | * 19 | * @param $client_id 20 | * Client identifier to be check with. 21 | * 22 | * @return array 23 | * Client details. The only mandatory key in the array is "redirect_uri". 24 | * This function MUST return FALSE if the given client does not exist or is 25 | * invalid. "redirect_uri" can be space-delimited to allow for multiple valid uris. 26 | * 27 | * return array( 28 | * "redirect_uri" => REDIRECT_URI, // REQUIRED redirect_uri registered for the client 29 | * "client_id" => CLIENT_ID, // OPTIONAL the client id 30 | * "grant_types" => GRANT_TYPES, // OPTIONAL an array of restricted grant types 31 | * "user_id" => USER_ID, // OPTIONAL the user identifier associated with this client 32 | * "scope" => SCOPE, // OPTIONAL the scopes allowed for this client 33 | * ); 34 | * 35 | * 36 | * @ingroup oauth2_section_4 37 | */ 38 | public function getClientDetails($client_id); 39 | 40 | /** 41 | * Get the scope associated with this client 42 | * 43 | * @return 44 | * STRING the space-delineated scope list for the specified client_id 45 | */ 46 | public function getClientScope($client_id); 47 | 48 | /** 49 | * Check restricted grant types of corresponding client identifier. 50 | * 51 | * If you want to restrict clients to certain grant types, override this 52 | * function. 53 | * 54 | * @param $client_id 55 | * Client identifier to be check with. 56 | * @param $grant_type 57 | * Grant type to be check with 58 | * 59 | * @return 60 | * TRUE if the grant type is supported by this client identifier, and 61 | * FALSE if it isn't. 62 | * 63 | * @ingroup oauth2_section_4 64 | */ 65 | public function checkRestrictedGrantType($client_id, $grant_type); 66 | } 67 | -------------------------------------------------------------------------------- /src/OAuth2/Storage/JwtAccessToken.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class JwtAccessToken implements JwtAccessTokenInterface 12 | { 13 | protected $publicKeyStorage; 14 | protected $tokenStorage; 15 | protected $encryptionUtil; 16 | 17 | /** 18 | * @param OAuth2\Encryption\PublicKeyInterface $publicKeyStorage the public key encryption to use 19 | * @param OAuth2\Storage\AccessTokenInterface $tokenStorage OPTIONAL persist the access token to another storage. This is useful if 20 | * you want to retain access token grant information somewhere, but 21 | * is not necessary when using this grant type. 22 | * @param OAuth2\Encryption\EncryptionInterface $encryptionUtil OPTIONAL class to use for "encode" and "decode" functions. 23 | */ 24 | public function __construct(PublicKeyInterface $publicKeyStorage, ?AccessTokenInterface $tokenStorage = null, ?EncryptionInterface $encryptionUtil = null) 25 | { 26 | $this->publicKeyStorage = $publicKeyStorage; 27 | $this->tokenStorage = $tokenStorage; 28 | if (is_null($encryptionUtil)) { 29 | $encryptionUtil = new Jwt; 30 | } 31 | $this->encryptionUtil = $encryptionUtil; 32 | } 33 | 34 | public function getAccessToken($oauth_token) 35 | { 36 | // just decode the token, don't verify 37 | if (!$tokenData = $this->encryptionUtil->decode($oauth_token, null, false)) { 38 | return false; 39 | } 40 | 41 | $client_id = isset($tokenData['aud']) ? $tokenData['aud'] : null; 42 | $public_key = $this->publicKeyStorage->getPublicKey($client_id); 43 | $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id); 44 | 45 | // now that we have the client_id, verify the token 46 | if (false === $this->encryptionUtil->decode($oauth_token, $public_key, array($algorithm))) { 47 | return false; 48 | } 49 | 50 | // normalize the JWT claims to the format expected by other components in this library 51 | return $this->convertJwtToOAuth2($tokenData); 52 | } 53 | 54 | public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null) 55 | { 56 | if ($this->tokenStorage) { 57 | return $this->tokenStorage->setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope); 58 | } 59 | } 60 | 61 | public function unsetAccessToken($access_token) 62 | { 63 | if ($this->tokenStorage) { 64 | return $this->tokenStorage->unsetAccessToken($access_token); 65 | } 66 | } 67 | 68 | 69 | // converts a JWT access token into an OAuth2-friendly format 70 | protected function convertJwtToOAuth2($tokenData) 71 | { 72 | $keyMapping = array( 73 | 'aud' => 'client_id', 74 | 'exp' => 'expires', 75 | 'sub' => 'user_id' 76 | ); 77 | 78 | foreach ($keyMapping as $jwtKey => $oauth2Key) { 79 | if (isset($tokenData[$jwtKey])) { 80 | $tokenData[$oauth2Key] = $tokenData[$jwtKey]; 81 | unset($tokenData[$jwtKey]); 82 | } 83 | } 84 | 85 | return $tokenData; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/OAuth2/Storage/JwtAccessTokenInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface JwtAccessTokenInterface extends AccessTokenInterface 12 | { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/OAuth2/Storage/JwtBearerInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface JwtBearerInterface 16 | { 17 | /** 18 | * Get the public key associated with a client_id 19 | * 20 | * @param $client_id 21 | * Client identifier to be checked with. 22 | * 23 | * @return 24 | * STRING Return the public key for the client_id if it exists, and MUST return FALSE if it doesn't. 25 | */ 26 | public function getClientKey($client_id, $subject); 27 | 28 | /** 29 | * Get a jti (JSON token identifier) by matching against the client_id, subject, audience and expiration. 30 | * 31 | * @param $client_id 32 | * Client identifier to match. 33 | * 34 | * @param $subject 35 | * The subject to match. 36 | * 37 | * @param $audience 38 | * The audience to match. 39 | * 40 | * @param $expiration 41 | * The expiration of the jti. 42 | * 43 | * @param $jti 44 | * The jti to match. 45 | * 46 | * @return 47 | * An associative array as below, and return NULL if the jti does not exist. 48 | * - issuer: Stored client identifier. 49 | * - subject: Stored subject. 50 | * - audience: Stored audience. 51 | * - expires: Stored expiration in unix timestamp. 52 | * - jti: The stored jti. 53 | */ 54 | public function getJti($client_id, $subject, $audience, $expiration, $jti); 55 | 56 | /** 57 | * Store a used jti so that we can check against it to prevent replay attacks. 58 | * @param $client_id 59 | * Client identifier to insert. 60 | * 61 | * @param $subject 62 | * The subject to insert. 63 | * 64 | * @param $audience 65 | * The audience to insert. 66 | * 67 | * @param $expiration 68 | * The expiration of the jti. 69 | * 70 | * @param $jti 71 | * The jti to insert. 72 | */ 73 | public function setJti($client_id, $subject, $audience, $expiration, $jti); 74 | } 75 | -------------------------------------------------------------------------------- /src/OAuth2/Storage/PublicKeyInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface PublicKeyInterface 12 | { 13 | /** 14 | * @param mixed $client_id 15 | * @return mixed 16 | */ 17 | public function getPublicKey($client_id = null); 18 | 19 | /** 20 | * @param mixed $client_id 21 | * @return mixed 22 | */ 23 | public function getPrivateKey($client_id = null); 24 | 25 | /** 26 | * @param mixed $client_id 27 | * @return mixed 28 | */ 29 | public function getEncryptionAlgorithm($client_id = null); 30 | } -------------------------------------------------------------------------------- /src/OAuth2/Storage/RefreshTokenInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface RefreshTokenInterface 13 | { 14 | /** 15 | * Grant refresh access tokens. 16 | * 17 | * Retrieve the stored data for the given refresh token. 18 | * 19 | * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN. 20 | * 21 | * @param $refresh_token 22 | * Refresh token to be check with. 23 | * 24 | * @return 25 | * An associative array as below, and NULL if the refresh_token is 26 | * invalid: 27 | * - refresh_token: Refresh token identifier. 28 | * - client_id: Client identifier. 29 | * - user_id: User identifier. 30 | * - expires: Expiration unix timestamp, or 0 if the token doesn't expire. 31 | * - scope: (optional) Scope values in space-separated string. 32 | * 33 | * @see http://tools.ietf.org/html/rfc6749#section-6 34 | * 35 | * @ingroup oauth2_section_6 36 | */ 37 | public function getRefreshToken($refresh_token); 38 | 39 | /** 40 | * Take the provided refresh token values and store them somewhere. 41 | * 42 | * This function should be the storage counterpart to getRefreshToken(). 43 | * 44 | * If storage fails for some reason, we're not currently checking for 45 | * any sort of success/failure, so you should bail out of the script 46 | * and provide a descriptive fail message. 47 | * 48 | * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN. 49 | * 50 | * @param $refresh_token 51 | * Refresh token to be stored. 52 | * @param $client_id 53 | * Client identifier to be stored. 54 | * @param $user_id 55 | * User identifier to be stored. 56 | * @param $expires 57 | * Expiration timestamp to be stored. 0 if the token doesn't expire. 58 | * @param $scope 59 | * (optional) Scopes to be stored in space-separated string. 60 | * 61 | * @ingroup oauth2_section_6 62 | */ 63 | public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null); 64 | 65 | /** 66 | * Expire a used refresh token. 67 | * 68 | * This is not explicitly required in the spec, but is almost implied. 69 | * After granting a new refresh token, the old one is no longer useful and 70 | * so should be forcibly expired in the data store so it can't be used again. 71 | * 72 | * If storage fails for some reason, we're not currently checking for 73 | * any sort of success/failure, so you should bail out of the script 74 | * and provide a descriptive fail message. 75 | * 76 | * @param $refresh_token 77 | * Refresh token to be expired. 78 | * 79 | * @ingroup oauth2_section_6 80 | */ 81 | public function unsetRefreshToken($refresh_token); 82 | } 83 | -------------------------------------------------------------------------------- /src/OAuth2/Storage/ScopeInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface ScopeInterface 13 | { 14 | /** 15 | * Check if the provided scope exists. 16 | * 17 | * @param $scope 18 | * A space-separated string of scopes. 19 | * 20 | * @return 21 | * TRUE if it exists, FALSE otherwise. 22 | */ 23 | public function scopeExists($scope); 24 | 25 | /** 26 | * The default scope to use in the event the client 27 | * does not request one. By returning "false", a 28 | * request_error is returned by the server to force a 29 | * scope request by the client. By returning "null", 30 | * opt out of requiring scopes 31 | * 32 | * @param $client_id 33 | * An optional client id that can be used to return customized default scopes. 34 | * 35 | * @return 36 | * string representation of default scope, null if 37 | * scopes are not defined, or false to force scope 38 | * request by the client 39 | * 40 | * ex: 41 | * 'default' 42 | * ex: 43 | * null 44 | */ 45 | public function getDefaultScope($client_id = null); 46 | } 47 | -------------------------------------------------------------------------------- /src/OAuth2/Storage/UserCredentialsInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface UserCredentialsInterface 13 | { 14 | /** 15 | * Grant access tokens for basic user credentials. 16 | * 17 | * Check the supplied username and password for validity. 18 | * 19 | * You can also use the $client_id param to do any checks required based 20 | * on a client, if you need that. 21 | * 22 | * Required for OAuth2::GRANT_TYPE_USER_CREDENTIALS. 23 | * 24 | * @param $username 25 | * Username to be check with. 26 | * @param $password 27 | * Password to be check with. 28 | * 29 | * @return 30 | * TRUE if the username and password are valid, and FALSE if it isn't. 31 | * Moreover, if the username and password are valid, and you want to 32 | * 33 | * @see http://tools.ietf.org/html/rfc6749#section-4.3 34 | * 35 | * @ingroup oauth2_section_4 36 | */ 37 | public function checkUserCredentials($username, $password); 38 | 39 | /** 40 | * @param string $username - username to get details for 41 | * @return array|false - the associated "user_id" and optional "scope" values 42 | * This function MUST return FALSE if the requested user does not exist or is 43 | * invalid. "scope" is a space-separated list of restricted scopes. 44 | * @code 45 | * return array( 46 | * "user_id" => USER_ID, // REQUIRED user_id to be stored with the authorization code or access token 47 | * "scope" => SCOPE // OPTIONAL space-separated list of restricted scopes 48 | * ); 49 | * @endcode 50 | */ 51 | public function getUserDetails($username); 52 | } 53 | -------------------------------------------------------------------------------- /src/OAuth2/TokenType/Bearer.php: -------------------------------------------------------------------------------- 1 | config = array_merge(array( 18 | 'token_param_name' => 'access_token', 19 | 'token_bearer_header_name' => 'Bearer', 20 | ), $config); 21 | } 22 | 23 | public function getTokenType() 24 | { 25 | return 'Bearer'; 26 | } 27 | 28 | /** 29 | * Check if the request has supplied token 30 | * 31 | * @see https://github.com/bshaffer/oauth2-server-php/issues/349#issuecomment-37993588 32 | */ 33 | public function requestHasToken(RequestInterface $request) 34 | { 35 | $headers = $request->headers('AUTHORIZATION'); 36 | 37 | // check the header, then the querystring, then the request body 38 | return !empty($headers) || (bool) ($request->request($this->config['token_param_name'])) || (bool) ($request->query($this->config['token_param_name'])); 39 | } 40 | 41 | /** 42 | * This is a convenience function that can be used to get the token, which can then 43 | * be passed to getAccessTokenData(). The constraints specified by the draft are 44 | * attempted to be adheared to in this method. 45 | * 46 | * As per the Bearer spec (draft 8, section 2) - there are three ways for a client 47 | * to specify the bearer token, in order of preference: Authorization Header, 48 | * POST and GET. 49 | * 50 | * NB: Resource servers MUST accept tokens via the Authorization scheme 51 | * (http://tools.ietf.org/html/rfc6750#section-2). 52 | * 53 | * @todo Should we enforce TLS/SSL in this function? 54 | * 55 | * @see http://tools.ietf.org/html/rfc6750#section-2.1 56 | * @see http://tools.ietf.org/html/rfc6750#section-2.2 57 | * @see http://tools.ietf.org/html/rfc6750#section-2.3 58 | * 59 | * Old Android version bug (at least with version 2.2) 60 | * @see http://code.google.com/p/android/issues/detail?id=6684 61 | * 62 | */ 63 | public function getAccessTokenParameter(RequestInterface $request, ResponseInterface $response) 64 | { 65 | $headers = $request->headers('AUTHORIZATION'); 66 | 67 | /** 68 | * Ensure more than one method is not used for including an 69 | * access token 70 | * 71 | * @see http://tools.ietf.org/html/rfc6750#section-3.1 72 | */ 73 | $methodsUsed = !empty($headers) + (bool) ($request->query($this->config['token_param_name'])) + (bool) ($request->request($this->config['token_param_name'])); 74 | if ($methodsUsed > 1) { 75 | $response->setError(400, 'invalid_request', 'Only one method may be used to authenticate at a time (Auth header, GET or POST)'); 76 | 77 | return null; 78 | } 79 | 80 | /** 81 | * If no authentication is provided, set the status code 82 | * to 401 and return no other error information 83 | * 84 | * @see http://tools.ietf.org/html/rfc6750#section-3.1 85 | */ 86 | if ($methodsUsed == 0) { 87 | $response->setStatusCode(401); 88 | 89 | return null; 90 | } 91 | 92 | // HEADER: Get the access token from the header 93 | if (!empty($headers)) { 94 | if (!preg_match('/' . $this->config['token_bearer_header_name'] . '\s(\S+)/i', $headers, $matches)) { 95 | $response->setError(400, 'invalid_request', 'Malformed auth header'); 96 | 97 | return null; 98 | } 99 | 100 | return $matches[1]; 101 | } 102 | 103 | if ($request->request($this->config['token_param_name'])) { 104 | // // POST: Get the token from POST data 105 | if (!in_array(strtolower($request->server('REQUEST_METHOD')), array('post', 'put'))) { 106 | $response->setError(400, 'invalid_request', 'When putting the token in the body, the method must be POST or PUT', '#section-2.2'); 107 | 108 | return null; 109 | } 110 | 111 | $contentType = $request->server('CONTENT_TYPE'); 112 | if (false !== $pos = strpos((string) $contentType, ';')) { 113 | $contentType = substr($contentType, 0, $pos); 114 | } 115 | 116 | if ($contentType !== null && $contentType != 'application/x-www-form-urlencoded') { 117 | // IETF specifies content-type. NB: Not all webservers populate this _SERVER variable 118 | // @see http://tools.ietf.org/html/rfc6750#section-2.2 119 | $response->setError(400, 'invalid_request', 'The content type for POST requests must be "application/x-www-form-urlencoded"'); 120 | 121 | return null; 122 | } 123 | 124 | return $request->request($this->config['token_param_name']); 125 | } 126 | 127 | // GET method 128 | return $request->query($this->config['token_param_name']); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/OAuth2/TokenType/Mac.php: -------------------------------------------------------------------------------- 1 | assertTrue(class_exists('OAuth2\Server')); 13 | $this->assertTrue(class_exists('OAuth2\Request')); 14 | $this->assertTrue(class_exists('OAuth2\Response')); 15 | $this->assertTrue(class_exists('OAuth2\GrantType\UserCredentials')); 16 | $this->assertTrue(interface_exists('OAuth2\Storage\AccessTokenInterface')); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/OAuth2/Encryption/FirebaseJwtTest.php: -------------------------------------------------------------------------------- 1 | privateKey = << $client_id, 40 | 'exp' => time() + 1000, 41 | 'iat' => time(), 42 | 'sub' => 'testuser@ourdomain.com', 43 | 'aud' => 'http://myapp.com/oauth/auth', 44 | 'scope' => null, 45 | ); 46 | 47 | $encoded = $jwtUtil->encode($params, $this->privateKey, 'RS256'); 48 | 49 | // test BC behaviour of trusting the algorithm in the header 50 | $payload = $jwtUtil->decode($encoded, $client_key, array('RS256')); 51 | $this->assertEquals($params, $payload); 52 | 53 | // test BC behaviour of not verifying by passing false 54 | $payload = $jwtUtil->decode($encoded, $client_key, false); 55 | $this->assertEquals($params, $payload); 56 | 57 | // test the new restricted algorithms header 58 | $payload = $jwtUtil->decode($encoded, $client_key, array('RS256')); 59 | $this->assertEquals($params, $payload); 60 | } 61 | 62 | public function testInvalidJwt() 63 | { 64 | $jwtUtil = new FirebaseJwt(); 65 | 66 | $this->assertFalse($jwtUtil->decode('goob')); 67 | $this->assertFalse($jwtUtil->decode('go.o.b')); 68 | } 69 | 70 | /** @dataProvider provideClientCredentials */ 71 | public function testInvalidJwtHeader($client_id, $client_key) 72 | { 73 | $jwtUtil = new FirebaseJwt(); 74 | 75 | $params = array( 76 | 'iss' => $client_id, 77 | 'exp' => time() + 1000, 78 | 'iat' => time(), 79 | 'sub' => 'testuser@ourdomain.com', 80 | 'aud' => 'http://myapp.com/oauth/auth', 81 | 'scope' => null, 82 | ); 83 | 84 | // testing for algorithm tampering when only RSA256 signing is allowed 85 | // @see https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ 86 | $tampered = $jwtUtil->encode($params, $client_key, 'HS256'); 87 | 88 | $payload = $jwtUtil->decode($tampered, $client_key, array('RS256')); 89 | 90 | $this->assertFalse($payload); 91 | } 92 | 93 | public function provideClientCredentials() 94 | { 95 | $storage = Bootstrap::getInstance()->getMemoryStorage(); 96 | $client_id = 'Test Client ID'; 97 | $client_key = $storage->getClientKey($client_id, "testuser@ourdomain.com"); 98 | 99 | return array( 100 | array($client_id, $client_key), 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/OAuth2/Encryption/JwtTest.php: -------------------------------------------------------------------------------- 1 | privateKey = << $client_id, 40 | 'exp' => time() + 1000, 41 | 'iat' => time(), 42 | 'sub' => 'testuser@ourdomain.com', 43 | 'aud' => 'http://myapp.com/oauth/auth', 44 | 'scope' => null, 45 | ); 46 | 47 | $encoded = $jwtUtil->encode($params, $this->privateKey, 'RS256'); 48 | 49 | // test BC behaviour of trusting the algorithm in the header 50 | $payload = $jwtUtil->decode($encoded, $client_key); 51 | $this->assertEquals($params, $payload); 52 | 53 | // test BC behaviour of not verifying by passing false 54 | $payload = $jwtUtil->decode($encoded, $client_key, false); 55 | $this->assertEquals($params, $payload); 56 | 57 | // test the new restricted algorithms header 58 | $payload = $jwtUtil->decode($encoded, $client_key, array('RS256')); 59 | $this->assertEquals($params, $payload); 60 | } 61 | 62 | public function testInvalidJwt() 63 | { 64 | $jwtUtil = new Jwt(); 65 | 66 | $this->assertFalse($jwtUtil->decode('goob')); 67 | $this->assertFalse($jwtUtil->decode('go.o.b')); 68 | } 69 | 70 | /** @dataProvider provideClientCredentials */ 71 | public function testInvalidJwtHeader($client_id, $client_key) 72 | { 73 | $jwtUtil = new Jwt(); 74 | 75 | $params = array( 76 | 'iss' => $client_id, 77 | 'exp' => time() + 1000, 78 | 'iat' => time(), 79 | 'sub' => 'testuser@ourdomain.com', 80 | 'aud' => 'http://myapp.com/oauth/auth', 81 | 'scope' => null, 82 | ); 83 | 84 | // testing for algorithm tampering when only RSA256 signing is allowed 85 | // @see https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/ 86 | $tampered = $jwtUtil->encode($params, $client_key, 'HS256'); 87 | 88 | $payload = $jwtUtil->decode($tampered, $client_key, array('RS256')); 89 | 90 | $this->assertFalse($payload); 91 | } 92 | 93 | public function provideClientCredentials() 94 | { 95 | $storage = Bootstrap::getInstance()->getMemoryStorage(); 96 | $client_id = 'Test Client ID'; 97 | $client_key = $storage->getClientKey($client_id, "testuser@ourdomain.com"); 98 | 99 | return array( 100 | array($client_id, $client_key), 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/OAuth2/GrantType/ClientCredentialsTest.php: -------------------------------------------------------------------------------- 1 | getTestServer(); 17 | $request = TestRequest::createPost(array( 18 | 'grant_type' => 'client_credentials', // valid grant type 19 | 'client_id' => 'Test Client ID', // valid client id 20 | 'client_secret' => 'FakeSecret', // valid client secret 21 | )); 22 | $server->handleTokenRequest($request, $response = new Response()); 23 | 24 | $this->assertEquals($response->getStatusCode(), 400); 25 | $this->assertEquals($response->getParameter('error'), 'invalid_client'); 26 | $this->assertEquals($response->getParameter('error_description'), 'The client credentials are invalid'); 27 | } 28 | 29 | public function testValidCredentials() 30 | { 31 | $server = $this->getTestServer(); 32 | $request = TestRequest::createPost(array( 33 | 'grant_type' => 'client_credentials', // valid grant type 34 | 'client_id' => 'Test Client ID', // valid client id 35 | 'client_secret' => 'TestSecret', // valid client secret 36 | )); 37 | $token = $server->grantAccessToken($request, new Response()); 38 | 39 | $this->assertNotNull($token); 40 | $this->assertArrayHasKey('scope', $token); 41 | $this->assertNull($token['scope']); 42 | } 43 | 44 | public function testValidCredentialsWithScope() 45 | { 46 | $server = $this->getTestServer(); 47 | $request = TestRequest::createPost(array( 48 | 'grant_type' => 'client_credentials', // valid grant type 49 | 'client_id' => 'Test Client ID', // valid client id 50 | 'client_secret' => 'TestSecret', // valid client secret 51 | 'scope' => 'scope1', 52 | )); 53 | $token = $server->grantAccessToken($request, new Response()); 54 | 55 | $this->assertNotNull($token); 56 | $this->assertArrayHasKey('access_token', $token); 57 | $this->assertArrayHasKey('scope', $token); 58 | $this->assertEquals($token['scope'], 'scope1'); 59 | } 60 | 61 | public function testValidCredentialsInvalidScope() 62 | { 63 | $server = $this->getTestServer(); 64 | $request = TestRequest::createPost(array( 65 | 'grant_type' => 'client_credentials', // valid grant type 66 | 'client_id' => 'Test Client ID', // valid client id 67 | 'client_secret' => 'TestSecret', // valid client secret 68 | 'scope' => 'invalid-scope', 69 | )); 70 | $token = $server->grantAccessToken($request, $response = new Response()); 71 | 72 | $this->assertEquals($response->getStatusCode(), 400); 73 | $this->assertEquals($response->getParameter('error'), 'invalid_scope'); 74 | $this->assertEquals($response->getParameter('error_description'), 'An unsupported scope was requested'); 75 | } 76 | 77 | public function testValidCredentialsInHeader() 78 | { 79 | // create with HTTP_AUTHORIZATION in header 80 | $server = $this->getTestServer(); 81 | $headers = array('HTTP_AUTHORIZATION' => 'Basic '.base64_encode('Test Client ID:TestSecret'), 'REQUEST_METHOD' => 'POST'); 82 | $params = array('grant_type' => 'client_credentials'); 83 | $request = new Request(array(), $params, array(), array(), array(), $headers); 84 | $token = $server->grantAccessToken($request, new Response()); 85 | 86 | $this->assertNotNull($token); 87 | $this->assertArrayHasKey('access_token', $token); 88 | $this->assertNotNull($token['access_token']); 89 | 90 | // create using PHP Authorization Globals 91 | $headers = array('PHP_AUTH_USER' => 'Test Client ID', 'PHP_AUTH_PW' => 'TestSecret', 'REQUEST_METHOD' => 'POST'); 92 | $params = array('grant_type' => 'client_credentials'); 93 | $request = new Request(array(), $params, array(), array(), array(), $headers); 94 | $token = $server->grantAccessToken($request, new Response()); 95 | 96 | $this->assertNotNull($token); 97 | $this->assertArrayHasKey('access_token', $token); 98 | $this->assertNotNull($token['access_token']); 99 | } 100 | 101 | public function testValidCredentialsInRequest() 102 | { 103 | $server = $this->getTestServer(); 104 | $request = TestRequest::createPost(array( 105 | 'grant_type' => 'client_credentials', // valid grant type 106 | 'client_id' => 'Test Client ID', // valid client id 107 | 'client_secret' => 'TestSecret', // valid client secret 108 | )); 109 | $token = $server->grantAccessToken($request, new Response()); 110 | 111 | $this->assertNotNull($token); 112 | $this->assertArrayHasKey('access_token', $token); 113 | $this->assertNotNull($token['access_token']); 114 | } 115 | 116 | public function testValidCredentialsInQuerystring() 117 | { 118 | $server = $this->getTestServer(); 119 | $request = TestRequest::createPost(array( 120 | 'grant_type' => 'client_credentials', // valid grant type 121 | 'client_id' => 'Test Client ID', // valid client id 122 | 'client_secret' => 'TestSecret', // valid client secret 123 | )); 124 | $token = $server->grantAccessToken($request, new Response()); 125 | 126 | $this->assertNotNull($token); 127 | $this->assertArrayHasKey('access_token', $token); 128 | $this->assertNotNull($token['access_token']); 129 | } 130 | 131 | public function testClientUserIdIsSetInAccessToken() 132 | { 133 | $server = $this->getTestServer(); 134 | $request = TestRequest::createPost(array( 135 | 'grant_type' => 'client_credentials', // valid grant type 136 | 'client_id' => 'Client ID With User ID', // valid client id 137 | 'client_secret' => 'TestSecret', // valid client secret 138 | )); 139 | $token = $server->grantAccessToken($request, new Response()); 140 | 141 | $this->assertNotNull($token); 142 | $this->assertArrayHasKey('access_token', $token); 143 | 144 | // verify the user_id was associated with the token 145 | $storage = $server->getStorage('client'); 146 | $token = $storage->getAccessToken($token['access_token']); 147 | $this->assertNotNull($token); 148 | $this->assertArrayHasKey('user_id', $token); 149 | $this->assertEquals($token['user_id'], 'brent@brentertainment.com'); 150 | } 151 | 152 | private function getTestServer() 153 | { 154 | $storage = Bootstrap::getInstance()->getMemoryStorage(); 155 | $server = new Server($storage); 156 | $server->addGrantType(new ClientCredentials($storage)); 157 | 158 | return $server; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /test/OAuth2/GrantType/ImplicitTest.php: -------------------------------------------------------------------------------- 1 | getTestServer(); 16 | $request = new Request(array( 17 | 'client_id' => 'Test Client ID', // valid client id 18 | 'redirect_uri' => 'http://adobe.com', // valid redirect URI 19 | 'response_type' => 'token', // invalid response type 20 | )); 21 | $server->handleAuthorizeRequest($request, $response = new Response(), false); 22 | 23 | $this->assertEquals($response->getStatusCode(), 302); 24 | $location = $response->getHttpHeader('Location'); 25 | $parts = parse_url($location); 26 | parse_str($parts['query'], $query); 27 | 28 | $this->assertEquals($query['error'], 'unsupported_response_type'); 29 | $this->assertEquals($query['error_description'], 'implicit grant type not supported'); 30 | } 31 | 32 | public function testUserDeniesAccessResponse() 33 | { 34 | $server = $this->getTestServer(array('allow_implicit' => true)); 35 | $request = new Request(array( 36 | 'client_id' => 'Test Client ID', // valid client id 37 | 'redirect_uri' => 'http://adobe.com', // valid redirect URI 38 | 'response_type' => 'token', // valid response type 39 | 'state' => 'xyz', 40 | )); 41 | $server->handleAuthorizeRequest($request, $response = new Response(), false); 42 | 43 | $this->assertEquals($response->getStatusCode(), 302); 44 | $location = $response->getHttpHeader('Location'); 45 | $parts = parse_url($location); 46 | parse_str($parts['query'], $query); 47 | 48 | $this->assertEquals($query['error'], 'access_denied'); 49 | $this->assertEquals($query['error_description'], 'The user denied access to your application'); 50 | } 51 | 52 | public function testSuccessfulRequestFragmentParameter() 53 | { 54 | $server = $this->getTestServer(array('allow_implicit' => true)); 55 | $request = new Request(array( 56 | 'client_id' => 'Test Client ID', // valid client id 57 | 'redirect_uri' => 'http://adobe.com', // valid redirect URI 58 | 'response_type' => 'token', // valid response type 59 | 'state' => 'xyz', 60 | )); 61 | $server->handleAuthorizeRequest($request, $response = new Response(), true); 62 | 63 | $this->assertEquals($response->getStatusCode(), 302); 64 | $this->assertNull($response->getParameter('error')); 65 | $this->assertNull($response->getParameter('error_description')); 66 | 67 | $location = $response->getHttpHeader('Location'); 68 | $parts = parse_url($location); 69 | 70 | $this->assertEquals('http', $parts['scheme']); // same as passed in to redirect_uri 71 | $this->assertEquals('adobe.com', $parts['host']); // same as passed in to redirect_uri 72 | $this->assertArrayHasKey('fragment', $parts); 73 | $this->assertFalse(isset($parts['query'])); 74 | 75 | // assert fragment is in "application/x-www-form-urlencoded" format 76 | parse_str($parts['fragment'], $params); 77 | $this->assertNotNull($params); 78 | $this->assertArrayHasKey('access_token', $params); 79 | $this->assertArrayHasKey('expires_in', $params); 80 | $this->assertArrayHasKey('token_type', $params); 81 | } 82 | 83 | public function testSuccessfulRequestReturnsStateParameter() 84 | { 85 | $server = $this->getTestServer(array('allow_implicit' => true)); 86 | $request = new Request(array( 87 | 'client_id' => 'Test Client ID', // valid client id 88 | 'redirect_uri' => 'http://adobe.com', // valid redirect URI 89 | 'response_type' => 'token', // valid response type 90 | 'state' => 'test', // valid state string (just needs to be passed back to us) 91 | )); 92 | $server->handleAuthorizeRequest($request, $response = new Response(), true); 93 | 94 | $this->assertEquals($response->getStatusCode(), 302); 95 | $this->assertNull($response->getParameter('error')); 96 | $this->assertNull($response->getParameter('error_description')); 97 | 98 | $location = $response->getHttpHeader('Location'); 99 | $parts = parse_url($location); 100 | $this->assertArrayHasKey('fragment', $parts); 101 | parse_str($parts['fragment'], $params); 102 | 103 | $this->assertArrayHasKey('state', $params); 104 | $this->assertEquals($params['state'], 'test'); 105 | } 106 | 107 | public function testSuccessfulRequestStripsExtraParameters() 108 | { 109 | $server = $this->getTestServer(array('allow_implicit' => true)); 110 | $request = new Request(array( 111 | 'client_id' => 'Test Client ID', // valid client id 112 | 'redirect_uri' => 'http://adobe.com?fake=something', // valid redirect URI 113 | 'response_type' => 'token', // valid response type 114 | 'state' => 'test', // valid state string (just needs to be passed back to us) 115 | 'fake' => 'something', // add extra param to querystring 116 | )); 117 | $server->handleAuthorizeRequest($request, $response = new Response(), true); 118 | 119 | $this->assertEquals($response->getStatusCode(), 302); 120 | $this->assertNull($response->getParameter('error')); 121 | $this->assertNull($response->getParameter('error_description')); 122 | 123 | $location = $response->getHttpHeader('Location'); 124 | $parts = parse_url($location); 125 | $this->assertFalse(isset($parts['fake'])); 126 | $this->assertArrayHasKey('fragment', $parts); 127 | parse_str($parts['fragment'], $params); 128 | 129 | $this->assertFalse(isset($params['fake'])); 130 | $this->assertArrayHasKey('state', $params); 131 | $this->assertEquals($params['state'], 'test'); 132 | } 133 | 134 | private function getTestServer($config = array()) 135 | { 136 | $storage = Bootstrap::getInstance()->getMemoryStorage(); 137 | $server = new Server($storage, $config); 138 | 139 | // Add the two types supported for authorization grant 140 | $server->addGrantType(new AuthorizationCode($storage)); 141 | 142 | return $server; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /test/OAuth2/OpenID/Controller/UserInfoControllerTest.php: -------------------------------------------------------------------------------- 1 | handleUserInfoRequest(new Request(), $response); 21 | $this->assertEquals(401, $response->getStatusCode()); 22 | } 23 | 24 | public function testValidToken() 25 | { 26 | $server = $this->getTestServer(); 27 | $request = Request::createFromGlobals(); 28 | $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-openid-connect'; 29 | $response = new Response(); 30 | 31 | $server->handleUserInfoRequest($request, $response); 32 | $parameters = $response->getParameters(); 33 | $this->assertEquals($parameters['sub'], 'testuser'); 34 | $this->assertEquals($parameters['email'], 'testuser@test.com'); 35 | $this->assertEquals($parameters['email_verified'], true); 36 | } 37 | 38 | private function getTestServer($config = array()) 39 | { 40 | $storage = Bootstrap::getInstance()->getMemoryStorage(); 41 | $server = new Server($storage, $config); 42 | 43 | return $server; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php: -------------------------------------------------------------------------------- 1 | getTestServer(); 16 | $request = TestRequest::createPost(array( 17 | 'grant_type' => 'authorization_code', // valid grant type 18 | 'client_id' => 'Test Client ID', // valid client id 19 | 'client_secret' => 'TestSecret', // valid client secret 20 | 'code' => 'testcode-openid', // valid code 21 | )); 22 | $token = $server->grantAccessToken($request, new Response()); 23 | 24 | $this->assertNotNull($token); 25 | $this->assertArrayHasKey('id_token', $token); 26 | $this->assertEquals('test_id_token', $token['id_token']); 27 | 28 | // this is only true if "offline_access" was requested 29 | $this->assertFalse(isset($token['refresh_token'])); 30 | } 31 | 32 | public function testOfflineAccess() 33 | { 34 | $server = $this->getTestServer(); 35 | $request = TestRequest::createPost(array( 36 | 'grant_type' => 'authorization_code', // valid grant type 37 | 'client_id' => 'Test Client ID', // valid client id 38 | 'client_secret' => 'TestSecret', // valid client secret 39 | 'code' => 'testcode-openid', // valid code 40 | 'scope' => 'offline_access', // valid code 41 | )); 42 | $token = $server->grantAccessToken($request, new Response()); 43 | 44 | $this->assertNotNull($token); 45 | $this->assertArrayHasKey('id_token', $token); 46 | $this->assertEquals('test_id_token', $token['id_token']); 47 | $this->assertTrue(isset($token['refresh_token'])); 48 | } 49 | 50 | private function getTestServer() 51 | { 52 | $storage = Bootstrap::getInstance()->getMemoryStorage(); 53 | $server = new Server($storage, array('use_openid_connect' => true)); 54 | $server->addGrantType(new AuthorizationCode($storage)); 55 | 56 | return $server; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php: -------------------------------------------------------------------------------- 1 | getTestServer(); 18 | 19 | $request = new Request(array( 20 | 'response_type' => 'code id_token', 21 | 'redirect_uri' => 'http://adobe.com', 22 | 'client_id' => 'Test Client ID', 23 | 'scope' => 'openid', 24 | 'state' => 'test', 25 | 'nonce' => 'test', 26 | )); 27 | 28 | $server->handleAuthorizeRequest($request, $response = new Response(), true); 29 | 30 | $this->assertEquals($response->getStatusCode(), 302); 31 | $location = $response->getHttpHeader('Location'); 32 | $this->assertStringNotContainsString('error', $location); 33 | 34 | $parts = parse_url($location); 35 | $this->assertArrayHasKey('query', $parts); 36 | 37 | // assert fragment is in "application/x-www-form-urlencoded" format 38 | parse_str($parts['query'], $params); 39 | $this->assertNotNull($params); 40 | $this->assertArrayHasKey('id_token', $params); 41 | $this->assertArrayHasKey('code', $params); 42 | 43 | // validate ID Token 44 | $parts = explode('.', $params['id_token']); 45 | foreach ($parts as &$part) { 46 | // Each part is a base64url encoded json string. 47 | $part = str_replace(array('-', '_'), array('+', '/'), $part); 48 | $part = base64_decode($part); 49 | $part = json_decode($part, true); 50 | } 51 | list($header, $claims, $signature) = $parts; 52 | 53 | $this->assertArrayHasKey('iss', $claims); 54 | $this->assertArrayHasKey('sub', $claims); 55 | $this->assertArrayHasKey('aud', $claims); 56 | $this->assertArrayHasKey('iat', $claims); 57 | $this->assertArrayHasKey('exp', $claims); 58 | $this->assertArrayHasKey('auth_time', $claims); 59 | $this->assertArrayHasKey('nonce', $claims); 60 | 61 | // only exists if an access token was granted along with the id_token 62 | $this->assertArrayNotHasKey('at_hash', $claims); 63 | 64 | $this->assertEquals($claims['iss'], 'test'); 65 | $this->assertEquals($claims['aud'], 'Test Client ID'); 66 | $this->assertEquals($claims['nonce'], 'test'); 67 | $duration = $claims['exp'] - $claims['iat']; 68 | $this->assertEquals($duration, 3600); 69 | } 70 | 71 | public function testUserClaimsWithUserId() 72 | { 73 | // add the test parameters in memory 74 | $server = $this->getTestServer(); 75 | 76 | $request = new Request(array( 77 | 'response_type' => 'code id_token', 78 | 'redirect_uri' => 'http://adobe.com', 79 | 'client_id' => 'Test Client ID', 80 | 'scope' => 'openid email', 81 | 'state' => 'test', 82 | 'nonce' => 'test', 83 | )); 84 | 85 | $userId = 'testuser'; 86 | $server->handleAuthorizeRequest($request, $response = new Response(), true, $userId); 87 | 88 | $this->assertEquals($response->getStatusCode(), 302); 89 | $location = $response->getHttpHeader('Location'); 90 | $this->assertStringNotContainsString('error', $location); 91 | 92 | $parts = parse_url($location); 93 | $this->assertArrayHasKey('query', $parts); 94 | 95 | // assert fragment is in "application/x-www-form-urlencoded" format 96 | parse_str($parts['query'], $params); 97 | $this->assertNotNull($params); 98 | $this->assertArrayHasKey('id_token', $params); 99 | $this->assertArrayHasKey('code', $params); 100 | 101 | // validate ID Token 102 | $parts = explode('.', $params['id_token']); 103 | foreach ($parts as &$part) { 104 | // Each part is a base64url encoded json string. 105 | $part = str_replace(array('-', '_'), array('+', '/'), $part); 106 | $part = base64_decode($part); 107 | $part = json_decode($part, true); 108 | } 109 | list($header, $claims, $signature) = $parts; 110 | 111 | $this->assertArrayHasKey('email', $claims); 112 | $this->assertArrayHasKey('email_verified', $claims); 113 | $this->assertNotNull($claims['email']); 114 | $this->assertNotNull($claims['email_verified']); 115 | } 116 | 117 | public function testUserClaimsWithoutUserId() 118 | { 119 | // add the test parameters in memory 120 | $server = $this->getTestServer(); 121 | 122 | $request = new Request(array( 123 | 'response_type' => 'code id_token', 124 | 'redirect_uri' => 'http://adobe.com', 125 | 'client_id' => 'Test Client ID', 126 | 'scope' => 'openid email', 127 | 'state' => 'test', 128 | 'nonce' => 'test', 129 | )); 130 | 131 | $userId = null; 132 | $server->handleAuthorizeRequest($request, $response = new Response(), true, $userId); 133 | 134 | $this->assertEquals($response->getStatusCode(), 302); 135 | $location = $response->getHttpHeader('Location'); 136 | $this->assertStringNotContainsString('error', $location); 137 | 138 | $parts = parse_url($location); 139 | $this->assertArrayHasKey('query', $parts); 140 | 141 | // assert fragment is in "application/x-www-form-urlencoded" format 142 | parse_str($parts['query'], $params); 143 | $this->assertNotNull($params); 144 | $this->assertArrayHasKey('id_token', $params); 145 | $this->assertArrayHasKey('code', $params); 146 | 147 | // validate ID Token 148 | $parts = explode('.', $params['id_token']); 149 | foreach ($parts as &$part) { 150 | // Each part is a base64url encoded json string. 151 | $part = str_replace(array('-', '_'), array('+', '/'), $part); 152 | $part = base64_decode($part); 153 | $part = json_decode($part, true); 154 | } 155 | list($header, $claims, $signature) = $parts; 156 | 157 | $this->assertArrayNotHasKey('email', $claims); 158 | $this->assertArrayNotHasKey('email_verified', $claims); 159 | } 160 | 161 | private function getTestServer($config = array()) 162 | { 163 | $config += array( 164 | 'use_openid_connect' => true, 165 | 'issuer' => 'test', 166 | 'id_lifetime' => 3600, 167 | 'allow_implicit' => true, 168 | ); 169 | 170 | $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); 171 | $memoryStorage->supportedScopes[] = 'email'; 172 | $responseTypes = array( 173 | 'code' => $code = new AuthorizationCode($memoryStorage), 174 | 'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $config), 175 | 'code id_token' => new CodeIdToken($code, $idToken), 176 | ); 177 | 178 | $server = new Server($memoryStorage, $config, array(), $responseTypes); 179 | $server->addGrantType(new ClientCredentials($memoryStorage)); 180 | 181 | return $server; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php: -------------------------------------------------------------------------------- 1 | getTestServer(array('allow_implicit' => true)); 20 | 21 | $request = new Request(array( 22 | 'response_type' => 'id_token token', 23 | 'redirect_uri' => 'http://adobe.com', 24 | 'client_id' => 'Test Client ID', 25 | 'scope' => 'openid', 26 | 'state' => 'test', 27 | 'nonce' => 'test', 28 | )); 29 | 30 | $server->handleAuthorizeRequest($request, $response = new Response(), true); 31 | 32 | $this->assertEquals($response->getStatusCode(), 302); 33 | $location = $response->getHttpHeader('Location'); 34 | $this->assertStringNotContainsString('error', $location); 35 | 36 | $parts = parse_url($location); 37 | $this->assertArrayHasKey('fragment', $parts); 38 | $this->assertFalse(isset($parts['query'])); 39 | 40 | // assert fragment is in "application/x-www-form-urlencoded" format 41 | parse_str($parts['fragment'], $params); 42 | $this->assertNotNull($params); 43 | $this->assertArrayHasKey('id_token', $params); 44 | $this->assertArrayHasKey('access_token', $params); 45 | 46 | // validate ID Token 47 | $parts = explode('.', $params['id_token']); 48 | foreach ($parts as &$part) { 49 | // Each part is a base64url encoded json string. 50 | $part = str_replace(array('-', '_'), array('+', '/'), $part); 51 | $part = base64_decode($part); 52 | $part = json_decode($part, true); 53 | } 54 | list($header, $claims, $signature) = $parts; 55 | 56 | $this->assertArrayHasKey('iss', $claims); 57 | $this->assertArrayHasKey('sub', $claims); 58 | $this->assertArrayHasKey('aud', $claims); 59 | $this->assertArrayHasKey('iat', $claims); 60 | $this->assertArrayHasKey('exp', $claims); 61 | $this->assertArrayHasKey('auth_time', $claims); 62 | $this->assertArrayHasKey('nonce', $claims); 63 | $this->assertArrayHasKey('at_hash', $claims); 64 | 65 | $this->assertEquals($claims['iss'], 'test'); 66 | $this->assertEquals($claims['aud'], 'Test Client ID'); 67 | $this->assertEquals($claims['nonce'], 'test'); 68 | $duration = $claims['exp'] - $claims['iat']; 69 | $this->assertEquals($duration, 3600); 70 | } 71 | 72 | private function getTestServer($config = array()) 73 | { 74 | $config += array( 75 | 'use_openid_connect' => true, 76 | 'issuer' => 'test', 77 | 'id_lifetime' => 3600, 78 | ); 79 | 80 | $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); 81 | $responseTypes = array( 82 | 'token' => $token = new AccessToken($memoryStorage, $memoryStorage), 83 | 'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $config), 84 | 'id_token token' => new IdTokenToken($token, $idToken), 85 | ); 86 | 87 | $server = new Server($memoryStorage, $config, array(), $responseTypes); 88 | $server->addGrantType(new ClientCredentials($memoryStorage)); 89 | 90 | return $server; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 15 | 16 | return; 17 | } 18 | 19 | if (!$storage instanceof AuthorizationCodeInterface) { 20 | return; 21 | } 22 | 23 | // assert code we are about to add does not exist 24 | $code = $storage->getAuthorizationCode('new-openid-code'); 25 | $this->assertFalse($code); 26 | 27 | // add new code 28 | $expires = time() + 20; 29 | $scope = null; 30 | $id_token = 'fake_id_token'; 31 | $success = $storage->setAuthorizationCode('new-openid-code', 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, $id_token); 32 | $this->assertTrue($success); 33 | 34 | $code = $storage->getAuthorizationCode('new-openid-code'); 35 | $this->assertNotNull($code); 36 | $this->assertArrayHasKey('authorization_code', $code); 37 | $this->assertArrayHasKey('client_id', $code); 38 | $this->assertArrayHasKey('user_id', $code); 39 | $this->assertArrayHasKey('redirect_uri', $code); 40 | $this->assertArrayHasKey('expires', $code); 41 | $this->assertEquals($code['authorization_code'], 'new-openid-code'); 42 | $this->assertEquals($code['client_id'], 'client ID'); 43 | $this->assertEquals($code['user_id'], 'SOMEUSERID'); 44 | $this->assertEquals($code['redirect_uri'], 'http://example.com'); 45 | $this->assertEquals($code['expires'], $expires); 46 | $this->assertEquals($code['id_token'], $id_token); 47 | 48 | // change existing code 49 | $expires = time() + 42; 50 | $new_id_token = 'fake_id_token-2'; 51 | $success = $storage->setAuthorizationCode('new-openid-code', 'client ID2', 'SOMEOTHERID', 'http://example.org', $expires, $scope, $new_id_token); 52 | $this->assertTrue($success); 53 | 54 | $code = $storage->getAuthorizationCode('new-openid-code'); 55 | $this->assertNotNull($code); 56 | $this->assertArrayHasKey('authorization_code', $code); 57 | $this->assertArrayHasKey('client_id', $code); 58 | $this->assertArrayHasKey('user_id', $code); 59 | $this->assertArrayHasKey('redirect_uri', $code); 60 | $this->assertArrayHasKey('expires', $code); 61 | $this->assertEquals($code['authorization_code'], 'new-openid-code'); 62 | $this->assertEquals($code['client_id'], 'client ID2'); 63 | $this->assertEquals($code['user_id'], 'SOMEOTHERID'); 64 | $this->assertEquals($code['redirect_uri'], 'http://example.org'); 65 | $this->assertEquals($code['expires'], $expires); 66 | $this->assertEquals($code['id_token'], $new_id_token); 67 | } 68 | 69 | /** @dataProvider provideStorage */ 70 | public function testRemoveIdTokenFromAuthorizationCode($storage) 71 | { 72 | // add new code 73 | $expires = time() + 20; 74 | $scope = null; 75 | $id_token = 'fake_id_token_to_remove'; 76 | $authcode = 'new-openid-code-'.rand(); 77 | $success = $storage->setAuthorizationCode($authcode, 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, $id_token); 78 | $this->assertTrue($success); 79 | 80 | // verify params were set 81 | $code = $storage->getAuthorizationCode($authcode); 82 | $this->assertNotNull($code); 83 | $this->assertArrayHasKey('id_token', $code); 84 | $this->assertEquals($code['id_token'], $id_token); 85 | 86 | // remove the id_token 87 | $success = $storage->setAuthorizationCode($authcode, 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, null); 88 | 89 | // verify the "id_token" is now null 90 | $code = $storage->getAuthorizationCode($authcode); 91 | $this->assertNotNull($code); 92 | $this->assertArrayHasKey('id_token', $code); 93 | $this->assertEquals($code['id_token'], null); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/OAuth2/OpenID/Storage/UserClaimsTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 15 | 16 | return; 17 | } 18 | 19 | if (!$storage instanceof UserClaimsInterface) { 20 | // incompatible storage 21 | return; 22 | } 23 | 24 | // invalid user 25 | $claims = $storage->getUserClaims('fake-user', ''); 26 | $this->assertFalse($claims); 27 | 28 | // valid user (no scope) 29 | $claims = $storage->getUserClaims('testuser', ''); 30 | 31 | /* assert the decoded token is the same */ 32 | $this->assertFalse(isset($claims['email'])); 33 | 34 | // valid user 35 | $claims = $storage->getUserClaims('testuser', 'email'); 36 | 37 | /* assert the decoded token is the same */ 38 | $this->assertEquals($claims['email'], "testuser@test.com"); 39 | $this->assertEquals($claims['email_verified'], true); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/OAuth2/RequestTest.php: -------------------------------------------------------------------------------- 1 | getTestServer(); 16 | 17 | // Smoke test for override request class 18 | // $server->handleTokenRequest($request, $response = new Response()); 19 | // $this->assertInstanceOf('Response', $response); 20 | // $server->handleAuthorizeRequest($request, $response = new Response(), true); 21 | // $this->assertInstanceOf('Response', $response); 22 | // $response = $server->verifyResourceRequest($request, $response = new Response()); 23 | // $this->assertTrue(is_bool($response)); 24 | 25 | /*** make some valid requests ***/ 26 | 27 | // Valid Token Request 28 | $request->setPost(array( 29 | 'grant_type' => 'authorization_code', 30 | 'client_id' => 'Test Client ID', 31 | 'client_secret' => 'TestSecret', 32 | 'code' => 'testcode', 33 | )); 34 | $server->handleTokenRequest($request, $response = new Response()); 35 | $this->assertEquals($response->getStatusCode(), 200); 36 | $this->assertNull($response->getParameter('error')); 37 | $this->assertNotNUll($response->getParameter('access_token')); 38 | } 39 | 40 | public function testHeadersReturnsValueByKey() 41 | { 42 | $request = new Request( 43 | array(), 44 | array(), 45 | array(), 46 | array(), 47 | array(), 48 | array(), 49 | array(), 50 | array('AUTHORIZATION' => 'Basic secret') 51 | ); 52 | 53 | $this->assertEquals('Basic secret', $request->headers('AUTHORIZATION')); 54 | } 55 | 56 | public function testHeadersReturnsDefaultIfHeaderNotPresent() 57 | { 58 | $request = new Request(); 59 | 60 | $this->assertEquals('Bearer', $request->headers('AUTHORIZATION', 'Bearer')); 61 | } 62 | 63 | public function testHeadersIsCaseInsensitive() 64 | { 65 | $request = new Request( 66 | array(), 67 | array(), 68 | array(), 69 | array(), 70 | array(), 71 | array(), 72 | array(), 73 | array('AUTHORIZATION' => 'Basic secret') 74 | ); 75 | 76 | $this->assertEquals('Basic secret', $request->headers('Authorization')); 77 | } 78 | 79 | public function testRequestReturnsPostParamIfNoQueryParamAvailable() 80 | { 81 | $request = new Request( 82 | array(), 83 | array('client_id' => 'correct') 84 | ); 85 | 86 | $this->assertEquals('correct', $request->query('client_id', $request->request('client_id'))); 87 | } 88 | 89 | public function testRequestHasHeadersAndServerHeaders() 90 | { 91 | $request = new Request( 92 | array(), 93 | array(), 94 | array(), 95 | array(), 96 | array(), 97 | array('CONTENT_TYPE' => 'text/xml', 'PHP_AUTH_USER' => 'client_id', 'PHP_AUTH_PW' => 'client_pass'), 98 | null, 99 | array('CONTENT_TYPE' => 'application/json') 100 | ); 101 | 102 | $this->assertSame('client_id', $request->headers('PHP_AUTH_USER')); 103 | $this->assertSame('client_pass', $request->headers('PHP_AUTH_PW')); 104 | $this->assertSame('application/json', $request->headers('CONTENT_TYPE')); 105 | } 106 | 107 | private function getTestServer($config = array()) 108 | { 109 | $storage = Bootstrap::getInstance()->getMemoryStorage(); 110 | $server = new Server($storage, $config); 111 | 112 | // Add the two types supported for authorization grant 113 | $server->addGrantType(new AuthorizationCode($storage)); 114 | 115 | return $server; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /test/OAuth2/ResponseTest.php: -------------------------------------------------------------------------------- 1 | 'bar', 13 | 'halland' => 'oates', 14 | )); 15 | 16 | $string = $response->getResponseBody('xml'); 17 | $this->assertStringContainsString('baroates', $string); 18 | } 19 | 20 | public function testSetRedirect() 21 | { 22 | $response = new Response(); 23 | $url = 'https://foo/bar'; 24 | $state = 'stateparam'; 25 | $response->setRedirect(301, $url, $state); 26 | $this->assertEquals( 27 | sprintf('%s?state=%s', $url, $state), 28 | $response->getHttpHeader('Location') 29 | ); 30 | 31 | $query = 'query=foo'; 32 | $response->setRedirect(301, $url . '?' . $query, $state); 33 | $this->assertEquals( 34 | sprintf('%s?%s&state=%s', $url, $query, $state), 35 | $response->getHttpHeader('Location') 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/OAuth2/ResponseType/AccessTokenTest.php: -------------------------------------------------------------------------------- 1 | array( 15 | 'revoke' => array('mytoken'), 16 | ), 17 | )); 18 | 19 | $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); 20 | $accessToken = new AccessToken($tokenStorage); 21 | $accessToken->revokeToken('revoke', 'access_token'); 22 | $this->assertFalse($tokenStorage->getAccessToken('revoke')); 23 | } 24 | 25 | public function testRevokeAccessTokenWithoutTypeHint() 26 | { 27 | $tokenStorage = new Memory(array( 28 | 'access_tokens' => array( 29 | 'revoke' => array('mytoken'), 30 | ), 31 | )); 32 | 33 | $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); 34 | $accessToken = new AccessToken($tokenStorage); 35 | $accessToken->revokeToken('revoke'); 36 | $this->assertFalse($tokenStorage->getAccessToken('revoke')); 37 | } 38 | 39 | public function testRevokeRefreshTokenWithTypeHint() 40 | { 41 | $tokenStorage = new Memory(array( 42 | 'refresh_tokens' => array( 43 | 'revoke' => array('mytoken'), 44 | ), 45 | )); 46 | 47 | $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke')); 48 | $accessToken = new AccessToken(new Memory, $tokenStorage); 49 | $accessToken->revokeToken('revoke', 'refresh_token'); 50 | $this->assertFalse($tokenStorage->getRefreshToken('revoke')); 51 | } 52 | 53 | public function testRevokeRefreshTokenWithoutTypeHint() 54 | { 55 | $tokenStorage = new Memory(array( 56 | 'refresh_tokens' => array( 57 | 'revoke' => array('mytoken'), 58 | ), 59 | )); 60 | 61 | $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke')); 62 | $accessToken = new AccessToken(new Memory, $tokenStorage); 63 | $accessToken->revokeToken('revoke'); 64 | $this->assertFalse($tokenStorage->getRefreshToken('revoke')); 65 | } 66 | 67 | public function testRevokeAccessTokenWithRefreshTokenTypeHint() 68 | { 69 | $tokenStorage = new Memory(array( 70 | 'access_tokens' => array( 71 | 'revoke' => array('mytoken'), 72 | ), 73 | )); 74 | 75 | $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); 76 | $accessToken = new AccessToken($tokenStorage); 77 | $accessToken->revokeToken('revoke', 'refresh_token'); 78 | $this->assertFalse($tokenStorage->getAccessToken('revoke')); 79 | } 80 | 81 | public function testRevokeAccessTokenWithBogusTypeHint() 82 | { 83 | $tokenStorage = new Memory(array( 84 | 'access_tokens' => array( 85 | 'revoke' => array('mytoken'), 86 | ), 87 | )); 88 | 89 | $this->assertEquals(array('mytoken'), $tokenStorage->getAccessToken('revoke')); 90 | $accessToken = new AccessToken($tokenStorage); 91 | $accessToken->revokeToken('revoke', 'foo'); 92 | $this->assertFalse($tokenStorage->getAccessToken('revoke')); 93 | } 94 | 95 | public function testRevokeRefreshTokenWithBogusTypeHint() 96 | { 97 | $tokenStorage = new Memory(array( 98 | 'refresh_tokens' => array( 99 | 'revoke' => array('mytoken'), 100 | ), 101 | )); 102 | 103 | $this->assertEquals(array('mytoken'), $tokenStorage->getRefreshToken('revoke')); 104 | $accessToken = new AccessToken(new Memory, $tokenStorage); 105 | $accessToken->revokeToken('revoke', 'foo'); 106 | $this->assertFalse($tokenStorage->getRefreshToken('revoke')); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test/OAuth2/ScopeTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($scopeUtil->checkScope('invalid', 'list of scopes')); 15 | $this->assertTrue($scopeUtil->checkScope('valid', 'valid and-some other-scopes')); 16 | $this->assertTrue($scopeUtil->checkScope('valid another-valid', 'valid another-valid and-some other-scopes')); 17 | // all scopes must match 18 | $this->assertFalse($scopeUtil->checkScope('valid invalid', 'valid and-some other-scopes')); 19 | $this->assertFalse($scopeUtil->checkScope('valid valid2 invalid', 'valid valid2 and-some other-scopes')); 20 | } 21 | 22 | public function testScopeStorage() 23 | { 24 | $scopeUtil = new Scope(); 25 | $this->assertEquals($scopeUtil->getDefaultScope(), null); 26 | 27 | $scopeUtil = new Scope(array( 28 | 'default_scope' => 'default', 29 | 'supported_scopes' => array('this', 'that', 'another'), 30 | )); 31 | $this->assertEquals($scopeUtil->getDefaultScope(), 'default'); 32 | $this->assertTrue($scopeUtil->scopeExists('this that another', 'client_id')); 33 | 34 | $memoryStorage = new Memory(array( 35 | 'default_scope' => 'base', 36 | 'supported_scopes' => array('only-this-one'), 37 | )); 38 | $scopeUtil = new Scope($memoryStorage); 39 | 40 | $this->assertEquals($scopeUtil->getDefaultScope(), 'base'); 41 | $this->assertTrue($scopeUtil->scopeExists('only-this-one', 'client_id')); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/OAuth2/Storage/AccessTokenTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 12 | 13 | return; 14 | } 15 | 16 | // assert token we are about to add does not exist 17 | $token = $storage->getAccessToken('newtoken'); 18 | $this->assertFalse($token); 19 | 20 | // add new token 21 | $expires = time() + 20; 22 | $success = $storage->setAccessToken('newtoken', 'client ID', 'SOMEUSERID', $expires); 23 | $this->assertTrue($success); 24 | 25 | $token = $storage->getAccessToken('newtoken'); 26 | $this->assertNotNull($token); 27 | $this->assertArrayHasKey('access_token', $token); 28 | $this->assertArrayHasKey('client_id', $token); 29 | $this->assertArrayHasKey('user_id', $token); 30 | $this->assertArrayHasKey('expires', $token); 31 | $this->assertEquals($token['access_token'], 'newtoken'); 32 | $this->assertEquals($token['client_id'], 'client ID'); 33 | $this->assertEquals($token['user_id'], 'SOMEUSERID'); 34 | $this->assertEquals($token['expires'], $expires); 35 | 36 | // change existing token 37 | $expires = time() + 42; 38 | $success = $storage->setAccessToken('newtoken', 'client ID2', 'SOMEOTHERID', $expires); 39 | $this->assertTrue($success); 40 | 41 | $token = $storage->getAccessToken('newtoken'); 42 | $this->assertNotNull($token); 43 | $this->assertArrayHasKey('access_token', $token); 44 | $this->assertArrayHasKey('client_id', $token); 45 | $this->assertArrayHasKey('user_id', $token); 46 | $this->assertArrayHasKey('expires', $token); 47 | $this->assertEquals($token['access_token'], 'newtoken'); 48 | $this->assertEquals($token['client_id'], 'client ID2'); 49 | $this->assertEquals($token['user_id'], 'SOMEOTHERID'); 50 | $this->assertEquals($token['expires'], $expires); 51 | 52 | // add token with scope having an empty string value 53 | $expires = time() + 42; 54 | $success = $storage->setAccessToken('newtoken', 'client ID', 'SOMEOTHERID', $expires, ''); 55 | $this->assertTrue($success); 56 | } 57 | 58 | /** @dataProvider provideStorage */ 59 | public function testUnsetAccessToken(AccessTokenInterface $storage) 60 | { 61 | if ($storage instanceof NullStorage || !method_exists($storage, 'unsetAccessToken')) { 62 | $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 63 | 64 | return; 65 | } 66 | 67 | // assert token we are about to unset does not exist 68 | $token = $storage->getAccessToken('revokabletoken'); 69 | $this->assertFalse($token); 70 | 71 | // add new token 72 | $expires = time() + 20; 73 | $success = $storage->setAccessToken('revokabletoken', 'client ID', 'SOMEUSERID', $expires); 74 | $this->assertTrue($success); 75 | 76 | // assert unsetAccessToken returns true 77 | $result = $storage->unsetAccessToken('revokabletoken'); 78 | $this->assertTrue($result); 79 | 80 | // assert token we unset does not exist 81 | $token = $storage->getAccessToken('revokabletoken'); 82 | $this->assertFalse($token); 83 | } 84 | 85 | /** @dataProvider provideStorage */ 86 | public function testUnsetAccessTokenReturnsFalse(AccessTokenInterface $storage) 87 | { 88 | if ($storage instanceof NullStorage || !method_exists($storage, 'unsetAccessToken')) { 89 | $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 90 | 91 | return; 92 | } 93 | 94 | // assert token we are about to unset does not exist 95 | $token = $storage->getAccessToken('nonexistanttoken'); 96 | $this->assertFalse($token); 97 | 98 | // assert unsetAccessToken returns false 99 | $result = $storage->unsetAccessToken('nonexistanttoken'); 100 | $this->assertFalse($result); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/OAuth2/Storage/AuthorizationCodeTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 12 | 13 | return; 14 | } 15 | 16 | // nonexistant client_id 17 | $details = $storage->getAuthorizationCode('faketoken'); 18 | $this->assertFalse($details); 19 | 20 | // valid client_id 21 | $details = $storage->getAuthorizationCode('testtoken'); 22 | $this->assertNotNull($details); 23 | } 24 | 25 | /** @dataProvider provideStorage */ 26 | public function testSetAuthorizationCode(AuthorizationCodeInterface $storage) 27 | { 28 | if ($storage instanceof NullStorage) { 29 | $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 30 | 31 | return; 32 | } 33 | 34 | // assert code we are about to add does not exist 35 | $code = $storage->getAuthorizationCode('newcode'); 36 | $this->assertFalse($code); 37 | 38 | // add new code 39 | $expires = time() + 20; 40 | $success = $storage->setAuthorizationCode('newcode', 'client ID', 'SOMEUSERID', 'http://example.com', $expires); 41 | $this->assertTrue($success); 42 | 43 | $code = $storage->getAuthorizationCode('newcode'); 44 | $this->assertNotNull($code); 45 | $this->assertArrayHasKey('authorization_code', $code); 46 | $this->assertArrayHasKey('client_id', $code); 47 | $this->assertArrayHasKey('user_id', $code); 48 | $this->assertArrayHasKey('redirect_uri', $code); 49 | $this->assertArrayHasKey('expires', $code); 50 | $this->assertEquals($code['authorization_code'], 'newcode'); 51 | $this->assertEquals($code['client_id'], 'client ID'); 52 | $this->assertEquals($code['user_id'], 'SOMEUSERID'); 53 | $this->assertEquals($code['redirect_uri'], 'http://example.com'); 54 | $this->assertEquals($code['expires'], $expires); 55 | 56 | // change existing code 57 | $expires = time() + 42; 58 | $success = $storage->setAuthorizationCode('newcode', 'client ID2', 'SOMEOTHERID', 'http://example.org', $expires); 59 | $this->assertTrue($success); 60 | 61 | $code = $storage->getAuthorizationCode('newcode'); 62 | $this->assertNotNull($code); 63 | $this->assertArrayHasKey('authorization_code', $code); 64 | $this->assertArrayHasKey('client_id', $code); 65 | $this->assertArrayHasKey('user_id', $code); 66 | $this->assertArrayHasKey('redirect_uri', $code); 67 | $this->assertArrayHasKey('expires', $code); 68 | $this->assertEquals($code['authorization_code'], 'newcode'); 69 | $this->assertEquals($code['client_id'], 'client ID2'); 70 | $this->assertEquals($code['user_id'], 'SOMEOTHERID'); 71 | $this->assertEquals($code['redirect_uri'], 'http://example.org'); 72 | $this->assertEquals($code['expires'], $expires); 73 | 74 | // add new code with scope having an empty string value 75 | $expires = time() + 20; 76 | $success = $storage->setAuthorizationCode('newcode', 'client ID', 'SOMEUSERID', 'http://example.com', $expires, ''); 77 | $this->assertTrue($success); 78 | } 79 | 80 | /** @dataProvider provideStorage */ 81 | public function testExpireAccessToken(AccessTokenInterface $storage) 82 | { 83 | if ($storage instanceof NullStorage) { 84 | $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 85 | 86 | return; 87 | } 88 | 89 | // create a valid code 90 | $expires = time() + 20; 91 | $success = $storage->setAuthorizationCode('code-to-expire', 'client ID', 'SOMEUSERID', 'http://example.com', time() + 20); 92 | $this->assertTrue($success); 93 | 94 | // verify the new code exists 95 | $code = $storage->getAuthorizationCode('code-to-expire'); 96 | $this->assertNotNull($code); 97 | 98 | $this->assertArrayHasKey('authorization_code', $code); 99 | $this->assertEquals($code['authorization_code'], 'code-to-expire'); 100 | 101 | // now expire the code and ensure it's no longer available 102 | $storage->expireAuthorizationCode('code-to-expire'); 103 | $code = $storage->getAuthorizationCode('code-to-expire'); 104 | $this->assertFalse($code); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/OAuth2/Storage/ClientCredentialsTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 12 | 13 | return; 14 | } 15 | 16 | // nonexistant client_id 17 | $pass = $storage->checkClientCredentials('fakeclient', 'testpass'); 18 | $this->assertFalse($pass); 19 | 20 | // invalid password 21 | $pass = $storage->checkClientCredentials('oauth_test_client', 'invalidcredentials'); 22 | $this->assertFalse($pass); 23 | 24 | // valid credentials 25 | $pass = $storage->checkClientCredentials('oauth_test_client', 'testpass'); 26 | $this->assertTrue($pass); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/OAuth2/Storage/ClientTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 12 | 13 | return; 14 | } 15 | 16 | // nonexistant client_id 17 | $details = $storage->getClientDetails('fakeclient'); 18 | $this->assertFalse($details); 19 | 20 | // valid client_id 21 | $details = $storage->getClientDetails('oauth_test_client'); 22 | $this->assertNotNull($details); 23 | $this->assertArrayHasKey('client_id', $details); 24 | $this->assertArrayHasKey('client_secret', $details); 25 | $this->assertArrayHasKey('redirect_uri', $details); 26 | } 27 | 28 | /** @dataProvider provideStorage */ 29 | public function testCheckRestrictedGrantType(ClientInterface $storage) 30 | { 31 | if ($storage instanceof NullStorage) { 32 | $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 33 | 34 | return; 35 | } 36 | 37 | // Check invalid 38 | $pass = $storage->checkRestrictedGrantType('oauth_test_client', 'authorization_code'); 39 | $this->assertFalse($pass); 40 | 41 | // Check valid 42 | $pass = $storage->checkRestrictedGrantType('oauth_test_client', 'implicit'); 43 | $this->assertTrue($pass); 44 | } 45 | 46 | /** @dataProvider provideStorage */ 47 | public function testGetAccessToken(ClientInterface $storage) 48 | { 49 | if ($storage instanceof NullStorage) { 50 | $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 51 | 52 | return; 53 | } 54 | 55 | // nonexistant client_id 56 | $details = $storage->getAccessToken('faketoken'); 57 | $this->assertFalse($details); 58 | 59 | // valid client_id 60 | $details = $storage->getAccessToken('testtoken'); 61 | $this->assertNotNull($details); 62 | } 63 | 64 | /** @dataProvider provideStorage */ 65 | public function testIsPublicClient(ClientInterface $storage) 66 | { 67 | if ($storage instanceof NullStorage) { 68 | $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 69 | 70 | return; 71 | } 72 | 73 | $publicClientId = 'public-client-'.rand(); 74 | $confidentialClientId = 'confidential-client-'.rand(); 75 | 76 | // create a new client 77 | $success1 = $storage->setClientDetails($publicClientId, ''); 78 | $success2 = $storage->setClientDetails($confidentialClientId, 'some-secret'); 79 | $this->assertTrue($success1); 80 | $this->assertTrue($success2); 81 | 82 | // assert isPublicClient for both 83 | $this->assertTrue($storage->isPublicClient($publicClientId)); 84 | $this->assertFalse($storage->isPublicClient($confidentialClientId)); 85 | } 86 | 87 | /** @dataProvider provideStorage */ 88 | public function testSaveClient(ClientInterface $storage) 89 | { 90 | if ($storage instanceof NullStorage) { 91 | $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 92 | 93 | return; 94 | } 95 | 96 | $clientId = 'some-client-'.rand(); 97 | 98 | // create a new client 99 | $success = $storage->setClientDetails($clientId, 'somesecret', 'http://test.com', 'client_credentials', 'clientscope1', 'brent@brentertainment.com'); 100 | $this->assertTrue($success); 101 | 102 | // valid client_id 103 | $details = $storage->getClientDetails($clientId); 104 | $this->assertEquals($details['client_secret'], 'somesecret'); 105 | $this->assertEquals($details['redirect_uri'], 'http://test.com'); 106 | $this->assertEquals($details['grant_types'], 'client_credentials'); 107 | $this->assertEquals($details['scope'], 'clientscope1'); 108 | $this->assertEquals($details['user_id'], 'brent@brentertainment.com'); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /test/OAuth2/Storage/DynamoDBTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('\Aws\DynamoDb\DynamoDbClient') 10 | ->disableOriginalConstructor() 11 | ->setMethods(array('query')) 12 | ->getMock(); 13 | 14 | $return = $this->getMockBuilder('\Guzzle\Service\Resource\Model') 15 | ->setMethods(array('count', 'toArray')) 16 | ->getMock(); 17 | 18 | $data = array( 19 | 'Items' => array(), 20 | 'Count' => 0, 21 | 'ScannedCount'=> 0 22 | ); 23 | 24 | $return->expects($this->once()) 25 | ->method('count') 26 | ->will($this->returnValue(count($data))); 27 | 28 | $return->expects($this->once()) 29 | ->method('toArray') 30 | ->will($this->returnValue($data)); 31 | 32 | // should return null default scope if none is set in database 33 | $client->expects($this->once()) 34 | ->method('query') 35 | ->will($this->returnValue($return)); 36 | 37 | $storage = new DynamoDB($client); 38 | $this->assertNull($storage->getDefaultScope()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/OAuth2/Storage/JwtAccessTokenTest.php: -------------------------------------------------------------------------------- 1 | getMemoryStorage(); 20 | $encryptionUtil = new Jwt(); 21 | 22 | $jwtAccessToken = array( 23 | 'access_token' => rand(), 24 | 'expires' => time() + 100, 25 | 'scope' => 'foo', 26 | ); 27 | 28 | $token = $encryptionUtil->encode($jwtAccessToken, $storage->getPrivateKey(), $storage->getEncryptionAlgorithm()); 29 | 30 | $this->assertNotNull($token); 31 | 32 | $tokenData = $crypto->getAccessToken($token); 33 | 34 | $this->assertTrue(is_array($tokenData)); 35 | 36 | /* assert the decoded token is the same */ 37 | $this->assertEquals($tokenData['access_token'], $jwtAccessToken['access_token']); 38 | $this->assertEquals($tokenData['expires'], $jwtAccessToken['expires']); 39 | $this->assertEquals($tokenData['scope'], $jwtAccessToken['scope']); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/OAuth2/Storage/JwtBearerTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 12 | 13 | return; 14 | } 15 | 16 | // nonexistant client_id 17 | $key = $storage->getClientKey('this-is-not-real', 'nor-is-this'); 18 | $this->assertFalse($key); 19 | 20 | // valid client_id and subject 21 | $key = $storage->getClientKey('oauth_test_client', 'test_subject'); 22 | $this->assertNotNull($key); 23 | $this->assertEquals($key, Bootstrap::getInstance()->getTestPublicKey()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/OAuth2/Storage/PdoTest.php: -------------------------------------------------------------------------------- 1 | getSqliteDir()); 14 | $pdo = new \PDO($dsn); 15 | $storage = new Pdo($pdo); 16 | 17 | $this->assertNotNull($storage->getClientDetails('oauth_test_client')); 18 | } 19 | 20 | public function testCreatePdoStorageUsingDSN() 21 | { 22 | $dsn = sprintf('sqlite:%s', Bootstrap::getInstance()->getSqliteDir()); 23 | $storage = new Pdo($dsn); 24 | 25 | $this->assertNotNull($storage->getClientDetails('oauth_test_client')); 26 | } 27 | 28 | public function testCreatePdoStorageUsingConfig() 29 | { 30 | $dsn = sprintf('sqlite:%s', Bootstrap::getInstance()->getSqliteDir()); 31 | $config = array('dsn' => $dsn); 32 | $storage = new Pdo($config); 33 | 34 | $this->assertNotNull($storage->getClientDetails('oauth_test_client')); 35 | } 36 | 37 | public function testCreatePdoStorageWithoutDSNThrowsException() 38 | { 39 | $this->expectErrorMessage('dsn'); 40 | $config = array('username' => 'brent', 'password' => 'brentisaballer'); 41 | $storage = new Pdo($config); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/OAuth2/Storage/PublicKeyTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 12 | 13 | return; 14 | } 15 | 16 | if (!$storage instanceof PublicKeyInterface) { 17 | // incompatible storage 18 | return; 19 | } 20 | 21 | $configDir = Bootstrap::getInstance()->getConfigDir(); 22 | $globalPublicKey = file_get_contents($configDir.'/keys/id_rsa.pub'); 23 | $globalPrivateKey = file_get_contents($configDir.'/keys/id_rsa'); 24 | 25 | /* assert values from storage */ 26 | $this->assertEquals($storage->getPublicKey(), $globalPublicKey); 27 | $this->assertEquals($storage->getPrivateKey(), $globalPrivateKey); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/OAuth2/Storage/RefreshTokenTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 12 | 13 | return; 14 | } 15 | 16 | // assert token we are about to add does not exist 17 | $token = $storage->getRefreshToken('refreshtoken'); 18 | $this->assertFalse($token); 19 | 20 | // add new token 21 | $expires = time() + 20; 22 | $success = $storage->setRefreshToken('refreshtoken', 'client ID', 'SOMEUSERID', $expires); 23 | $this->assertTrue($success); 24 | 25 | $token = $storage->getRefreshToken('refreshtoken'); 26 | $this->assertNotNull($token); 27 | $this->assertArrayHasKey('refresh_token', $token); 28 | $this->assertArrayHasKey('client_id', $token); 29 | $this->assertArrayHasKey('user_id', $token); 30 | $this->assertArrayHasKey('expires', $token); 31 | $this->assertEquals($token['refresh_token'], 'refreshtoken'); 32 | $this->assertEquals($token['client_id'], 'client ID'); 33 | $this->assertEquals($token['user_id'], 'SOMEUSERID'); 34 | $this->assertEquals($token['expires'], $expires); 35 | 36 | // add token with scope having an empty string value 37 | $expires = time() + 20; 38 | $success = $storage->setRefreshToken('refreshtoken2', 'client ID', 'SOMEUSERID', $expires, ''); 39 | $this->assertTrue($success); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/OAuth2/Storage/ScopeTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 14 | 15 | return; 16 | } 17 | 18 | if (!$storage instanceof ScopeInterface) { 19 | // incompatible storage 20 | return; 21 | } 22 | 23 | //Test getting scopes 24 | $scopeUtil = new Scope($storage); 25 | $this->assertTrue($scopeUtil->scopeExists('supportedscope1')); 26 | $this->assertTrue($scopeUtil->scopeExists('supportedscope1 supportedscope2 supportedscope3')); 27 | $this->assertFalse($scopeUtil->scopeExists('fakescope')); 28 | $this->assertFalse($scopeUtil->scopeExists('supportedscope1 supportedscope2 supportedscope3 fakescope')); 29 | } 30 | 31 | /** @dataProvider provideStorage */ 32 | public function testGetDefaultScope($storage) 33 | { 34 | if ($storage instanceof NullStorage) { 35 | $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 36 | 37 | return; 38 | } 39 | 40 | if (!$storage instanceof ScopeInterface) { 41 | // incompatible storage 42 | return; 43 | } 44 | 45 | // test getting default scope 46 | $scopeUtil = new Scope($storage); 47 | $expected = explode(' ', $scopeUtil->getDefaultScope()); 48 | $actual = explode(' ', 'defaultscope1 defaultscope2'); 49 | sort($expected); 50 | sort($actual); 51 | $this->assertEquals($expected, $actual); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/OAuth2/Storage/UserCredentialsTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Skipped Storage: ' . $storage->getMessage()); 12 | 13 | return; 14 | } 15 | 16 | // create a new user for testing 17 | $success = $storage->setUser('testusername', 'testpass', 'Test', 'User'); 18 | $this->assertTrue($success); 19 | 20 | // correct credentials 21 | $this->assertTrue($storage->checkUserCredentials('testusername', 'testpass')); 22 | // invalid password 23 | $this->assertFalse($storage->checkUserCredentials('testusername', 'fakepass')); 24 | // invalid username 25 | $this->assertFalse($storage->checkUserCredentials('fakeusername', 'testpass')); 26 | 27 | // invalid username 28 | $this->assertFalse($storage->getUserDetails('fakeusername')); 29 | 30 | // ensure all properties are set 31 | $user = $storage->getUserDetails('testusername'); 32 | $this->assertTrue($user !== false); 33 | $this->assertArrayHasKey('user_id', $user); 34 | $this->assertArrayHasKey('first_name', $user); 35 | $this->assertArrayHasKey('last_name', $user); 36 | $this->assertEquals($user['user_id'], 'testusername'); 37 | $this->assertEquals($user['first_name'], 'Test'); 38 | $this->assertEquals($user['last_name'], 'User'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/OAuth2/TokenType/BearerTest.php: -------------------------------------------------------------------------------- 1 | 'ThisIsMyAccessToken' 16 | )); 17 | $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8'; 18 | 19 | $param = $bearer->getAccessTokenParameter($request, $response = new Response()); 20 | $this->assertEquals($param, 'ThisIsMyAccessToken'); 21 | } 22 | 23 | public function testInvalidContentType() 24 | { 25 | $bearer = new Bearer(); 26 | $request = TestRequest::createPost(array( 27 | 'access_token' => 'ThisIsMyAccessToken' 28 | )); 29 | $request->server['CONTENT_TYPE'] = 'application/json; charset=UTF-8'; 30 | 31 | $param = $bearer->getAccessTokenParameter($request, $response = new Response()); 32 | $this->assertNull($param); 33 | $this->assertEquals($response->getStatusCode(), 400); 34 | $this->assertEquals($response->getParameter('error'), 'invalid_request'); 35 | $this->assertEquals($response->getParameter('error_description'), 'The content type for POST requests must be "application/x-www-form-urlencoded"'); 36 | } 37 | 38 | public function testMissingContentTypeExpectsToBeCorrectContent() 39 | { 40 | $bearer = new Bearer(); 41 | $request = TestRequest::createPost(array( 42 | 'access_token' => 'ThisIsMyAccessToken' 43 | )); 44 | 45 | $param = $bearer->getAccessTokenParameter($request, $response = new Response()); 46 | $this->assertEquals($param, 'ThisIsMyAccessToken'); 47 | } 48 | 49 | public function testValidRequestUsingAuthorizationHeader() 50 | { 51 | $bearer = new Bearer(); 52 | $request = new TestRequest(); 53 | $request->headers['AUTHORIZATION'] = 'Bearer MyToken'; 54 | $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8'; 55 | 56 | $param = $bearer->getAccessTokenParameter($request, $response = new Response()); 57 | $this->assertEquals('MyToken', $param); 58 | } 59 | 60 | public function testValidRequestUsingAuthorizationHeaderCaseInsensitive() 61 | { 62 | $bearer = new Bearer(); 63 | $request = new TestRequest(); 64 | $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8'; 65 | $request->headers['Authorization'] = 'Bearer MyToken'; 66 | 67 | $param = $bearer->getAccessTokenParameter($request, $response = new Response()); 68 | $this->assertEquals('MyToken', $param); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | query = $_GET; 18 | $this->request = $_POST; 19 | $this->server = $_SERVER; 20 | $this->headers = array(); 21 | } 22 | 23 | public function query($name, $default = null) 24 | { 25 | return isset($this->query[$name]) ? $this->query[$name] : $default; 26 | } 27 | 28 | public function request($name, $default = null) 29 | { 30 | return isset($this->request[$name]) ? $this->request[$name] : $default; 31 | } 32 | 33 | public function server($name, $default = null) 34 | { 35 | return isset($this->server[$name]) ? $this->server[$name] : $default; 36 | } 37 | 38 | public function getAllQueryParameters() 39 | { 40 | return $this->query; 41 | } 42 | 43 | public function setQuery(array $query) 44 | { 45 | $this->query = $query; 46 | } 47 | 48 | public function setMethod($method) 49 | { 50 | $this->server['REQUEST_METHOD'] = $method; 51 | } 52 | 53 | public function setPost(array $params) 54 | { 55 | $this->setMethod('POST'); 56 | $this->request = $params; 57 | } 58 | 59 | public static function createPost(array $params = array()) 60 | { 61 | $request = new self(); 62 | $request->setPost($params); 63 | 64 | return $request; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/lib/OAuth2/Storage/BaseTest.php: -------------------------------------------------------------------------------- 1 | getMemoryStorage(); 12 | $sqlite = Bootstrap::getInstance()->getSqlitePdo(); 13 | $mysql = Bootstrap::getInstance()->getMysqlPdo(); 14 | $postgres = Bootstrap::getInstance()->getPostgresPdo(); 15 | $mongo = Bootstrap::getInstance()->getMongo(); 16 | $mongoDb = Bootstrap::getInstance()->getMongoDB(); 17 | $redis = Bootstrap::getInstance()->getRedisStorage(); 18 | $cassandra = Bootstrap::getInstance()->getCassandraStorage(); 19 | $dynamodb = Bootstrap::getInstance()->getDynamoDbStorage(); 20 | $couchbase = Bootstrap::getInstance()->getCouchbase(); 21 | 22 | /* hack until we can fix "default_scope" dependencies in other tests */ 23 | $memory->defaultScope = 'defaultscope1 defaultscope2'; 24 | 25 | return array( 26 | array($memory), 27 | array($sqlite), 28 | array($mysql), 29 | array($postgres), 30 | array($mongo), 31 | array($mongoDb), 32 | array($redis), 33 | array($cassandra), 34 | array($dynamodb), 35 | array($couchbase), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/lib/OAuth2/Storage/NullStorage.php: -------------------------------------------------------------------------------- 1 | name = $name; 16 | $this->description = $description; 17 | } 18 | 19 | public function __toString() 20 | { 21 | return $this->name; 22 | } 23 | 24 | public function getMessage() 25 | { 26 | if ($this->description) { 27 | return $this->description; 28 | } 29 | 30 | return $this->name; 31 | } 32 | } 33 | --------------------------------------------------------------------------------