├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.txt ├── LICENSE.txt ├── README.md ├── composer.json ├── config.doxy ├── lib ├── IOAuth2.php ├── IOAuth2GrantClient.php ├── IOAuth2GrantCode.php ├── IOAuth2GrantExtension.php ├── IOAuth2GrantImplicit.php ├── IOAuth2GrantUser.php ├── IOAuth2RefreshTokens.php ├── IOAuth2Storage.php ├── Model │ ├── IOAuth2AccessToken.php │ ├── IOAuth2AuthCode.php │ ├── IOAuth2Client.php │ ├── IOAuth2RefreshToken.php │ ├── IOAuth2Token.php │ ├── OAuth2AccessToken.php │ ├── OAuth2AuthCode.php │ ├── OAuth2Client.php │ ├── OAuth2RefreshToken.php │ └── OAuth2Token.php ├── OAuth2.php ├── OAuth2AuthenticateException.php ├── OAuth2Client.php ├── OAuth2Exception.php ├── OAuth2RedirectException.php └── OAuth2ServerException.php ├── phpunit.xml.dist ├── server └── examples │ ├── mongo │ ├── addclient.php │ ├── authorize.php │ ├── lib │ │ └── OAuth2StorageMongo.php │ ├── protected_resource.php │ └── token.php │ └── pdo │ ├── addclient.php │ ├── authorize.php │ ├── lib │ ├── OAuth2StoragePdo.php │ └── bootstrap.php │ ├── mysql_create_tables.sql │ ├── protected_resource.php │ └── token.php └── tests ├── ExtraHeadersTest.php ├── Fixtures ├── OAuth2GrantCodeStub.php ├── OAuth2GrantExtensionJwtBearer.php ├── OAuth2GrantExtensionLifetimeStub.php ├── OAuth2GrantExtensionStub.php ├── OAuth2GrantUserStub.php ├── OAuth2ImplicitStub.php └── OAuth2StorageStub.php ├── Model └── OAuth2TokenTest.php ├── OAuth2ImplicitGrantTypeTest.php ├── OAuth2OutputTest.php └── OAuth2Test.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | build: 13 | name: PHP ${{ matrix.php }} 14 | runs-on: 'ubuntu-latest' 15 | strategy: 16 | matrix: 17 | php: [ '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0' ] 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Setup PHP ${{ matrix.php }} and composer 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | coverage: none # disable xdebug, pcov 26 | tools: composer:v2 27 | 28 | - name: "Set composer cache directory" 29 | id: composer-cache 30 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 31 | 32 | - name: "Cache composer" 33 | uses: actions/cache@v2.1.2 34 | with: 35 | path: ${{ steps.composer-cache.outputs.dir }} 36 | key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('composer.json') }} 37 | restore-keys: ${{ runner.os }}-${{ matrix.php-version }}-composer- 38 | 39 | - name: Install dependencies 40 | run: composer update 41 | 42 | - name: Run CI 43 | run: vendor/bin/phpunit 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | phpunit.xml 4 | .phpunit.result.cache 5 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | oauth2-php revision xxx, xxxx-xx-xx (development version) 2 | ---------------------- 3 | 4 | oauth2-php revision 23, 2011-01-25 5 | ---------------------- 6 | * introduce Drupal style getVariable() and setVariable, replace legacy 7 | variable get/set functions. 8 | * remove hardcode PHP display_error and errror_reporting, as this should 9 | be manually implement within 3rd party integration. 10 | * make verbose error as configurable and default disable, as this should 11 | be manually enable within 3rd party integration. 12 | * add lib/OAuth2Client.inc and lib/OAuth2Exception.inc for client-side 13 | implementation. 14 | 15 | oauth2-php revision 21, 2010-12-18 16 | ---------------------- 17 | * cleanup tabs and trailing whitespace at the end. 18 | * remove server/examples/mongo/lib/oauth.php and 19 | server/examples/pdo/lib/oauth.php, so only keep single copy as 20 | lib /oauth.php. 21 | * issue #5: Wrong variable name in get_access_token() in pdo_oatuh.php. 22 | * issue #6: mysql_create_tables.sql should allow scope to be NULL. 23 | * issue #7: authorize_client_response_type() is never used. 24 | * issue #9: Change "redirect_uri" filtering from FILTER_VALIDATE_URL to 25 | FILTER_SANITIZE_URL. 26 | * better coding syntax for error() and callback_error(). 27 | * better pdo_oauth2.php variable naming with change to 28 | mysql_create_tables.sql. 29 | * change REGEX_CLIENT_ID as 3-32 characters long, so will work with md5() 30 | result directly. 31 | * debug linkage to oauth2.php during previous commit. 32 | * debug redirect_uri check for AUTH_CODE_GRANT_TYPE, clone from 33 | get_authorize_params(). 34 | * update mysql_create_tables.sql with phpmyadmin export format. 35 | * rename library files, prepare for adding client-side implementation. 36 | * code cleanup with indent and spacing. 37 | * code cleanup true/false/null with TRUE/FALSE/NULL. 38 | * rename constants with OAUTH2_ prefix, prevent 3rd party integration 39 | conflict. 40 | * remove HTTP 400 response constant, as useless refer to draft v10. 41 | * merge ERROR_INVALID_CLIENT_ID and ERROR_UNAUTHORIZED_CLIENT as 42 | OAUTH2_ERROR_INVALID_CLIENT, as refer to that of draft v9 to v10 changes. 43 | * improve constants comment with doxygen syntax. 44 | * update class function call naming. 45 | * coding style clean up. 46 | * update part of documents. 47 | * change expirseRefreshToken() as unsetRefreshToken(). 48 | * update token and auth code generation as md5() result, simpler for manual 49 | debug with web browser. 50 | * update all documents. 51 | * restructure @ingroup. 52 | * rename checkRestrictedClientResponseTypes() as 53 | checkRestrictedAuthResponseType(). 54 | * rename checkRestrictedClientGrantTypes() as checkRestrictedGrantType(). 55 | * rename error() as errorJsonResponse(). 56 | * rename errorCallback() as errorDoRedirectUriCallback(). 57 | * rename send401Unauthorized() as errorWWWAuthenticateResponseHeader(), 58 | update support with different HTTP status code. 59 | * update __construct() with array input. 60 | * update finishClientAuthorization() with array input. 61 | * add get/set functions for $access_token_lifetime, $auth_code_lifetime and 62 | $refresh_token_lifetime. 63 | * fix a lots of typos. 64 | * document all sample server implementation. 65 | * more documents. 66 | * add config.doxy for doxygen default setup. 67 | * add MIT LICENSE.txt. 68 | * add CHANGELOG.txt. 69 | 70 | oauth2-php revision 9, 2010-09-04 71 | ---------------------- 72 | - fixes for issues #2 and #4, updates oauth lib in the example folders to 73 | the latest version in the 'lib' folder. 74 | - updates server library to revision 10 of the OAuth 2.0 spec. 75 | - adds an option for more verbose error messages to be returned in the JSON 76 | response. 77 | - adds method to be overridden for expiring used refresh tokens. 78 | - fixes bug checking token expiration. 79 | - makes some more methods protected instead of private so they can be 80 | overridden. 81 | - fixes issue #1 http://code.google.com/p/oauth2-php/issues/detail?id=1 82 | 83 | oauth2-php revision 7, 2010-06-29 84 | ---------------------- 85 | - fixed mongo connection constants. 86 | - updated store_refresh_token to include expires time. 87 | - changed example server directory structure 88 | - corrected "false" return result on get_stored_auth_code. 89 | - implemented PDO example adapter. 90 | - corrected an error in assertion grant type. 91 | - updated for ietf draft v9: 92 | http://tools.ietf.org/html/draft-ietf-oauth-v2-09. 93 | - updated updated to support v9 lib. 94 | - added mysql table creation script. 95 | 96 | oauth2-php revision 0, 2010-06-27 97 | ---------------------- 98 | - initial commit. 99 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Tim Ridgely 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 Implementation 2 | ============================ 3 | 4 | [![Build Status](https://secure.travis-ci.org/FriendsOfSymfony/oauth2-php.png?branch=master)](http://travis-ci.org/FriendsOfSymfony/oauth2-php) 5 | [![HHVM Status](http://hhvm.h4cc.de/badge/FriendsOfSymfony/oauth2-php.svg)](http://hhvm.h4cc.de/package/FriendsOfSymfony/oauth2-php) 6 | 7 | This library now implements draft 20 of OAuth 2.0. 8 | The client is still only draft-10. 9 | 10 | This version of oauth2-php is a fork of https://github.com/quizlet/oauth2-php with the following changes: 11 | 12 | - Namespaced 13 | - No more require(_once) 14 | - [PSR-4](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md) autoloading compatible 15 | - Uses [HttpFoundation](https://github.com/symfony/HttpFoundation) Request and Response for input/output 16 | - More testable design 17 | - Better test coverage 18 | 19 | (pull request is pending) 20 | 21 | https://github.com/quizlet/oauth2-php is a fork of http://code.google.com/p/oauth2-php/ updated against OAuth2.0 draft 22 | 20, with a better OO design. 23 | 24 | http://code.google.com/p/oauth2-php/ is the original repository, which seems abandonned. 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "friendsofsymfony/oauth2-php", 3 | "type": "library", 4 | "description": "OAuth2 library", 5 | "license": "MIT", 6 | "keywords": ["oauth", "oauth2"], 7 | "homepage": "https://github.com/FriendsOfSymfony/oauth2-php", 8 | "authors": [ 9 | { 10 | "name": "Arnaud Le Blanc", 11 | "email": "arnaud.lb@gmail.com" 12 | }, 13 | { 14 | "name": "FriendsOfSymfony Community", 15 | "homepage": "https://github.com/FriendsOfSymfony/oauth2-php/contributors" 16 | } 17 | ], 18 | 19 | "require": { 20 | "php": "^5.5.9 || ^7.0.8 || ^7.1.3 || ^7.2.5 || ^8.0.0", 21 | "symfony/http-foundation": "~3.0|~4.0|~5.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^5.0 || ^6.0 || ^8.0 || ^9.0.0" 25 | }, 26 | 27 | "autoload": { 28 | "psr-4": { 29 | "OAuth2\\": "lib/" 30 | }, 31 | "exclude-from-classmap": [ 32 | "/tests/" 33 | ] 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "OAuth2\\Tests\\": "tests/" 38 | } 39 | }, 40 | 41 | "extra": { 42 | "branch-alias": { 43 | "dev-master": "1.2.x-dev" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/IOAuth2.php: -------------------------------------------------------------------------------- 1 | . 12 | */ 13 | interface IOAuth2 14 | { 15 | /** 16 | * Returns a persistent variable. 17 | * 18 | * @param string $name The name of the variable to return. 19 | * @param mixed $default The default value to use if this variable has never been set. 20 | * 21 | * @return mixed The value of the variable. 22 | */ 23 | public function getVariable($name, $default = null); 24 | 25 | /** 26 | * Sets a persistent variable. 27 | * 28 | * @param string $name The name of the variable to set. 29 | * @param mixed $value The value to set. 30 | * 31 | * @return OAuth2 The application (for chained calls of this method) 32 | */ 33 | public function setVariable($name, $value); 34 | 35 | /** 36 | * Check that a valid access token has been provided. 37 | * The token is returned (as an associative array) if valid. 38 | * 39 | * The scope parameter defines any required scope that the token must have. 40 | * If a scope param is provided and the token does not have the required 41 | * scope, we bounce the request. 42 | * 43 | * Some implementations may choose to return a subset of the protected 44 | * resource (i.e. "public" data) if the user has not provided an access 45 | * token or if the access token is invalid or expired. 46 | * 47 | * The IETF spec says that we should send a 401 Unauthorized header and 48 | * bail immediately so that's what the defaults are set to. You can catch 49 | * the exception thrown and behave differently if you like (log errors, allow 50 | * public access for missing tokens, etc) 51 | * 52 | * @param string $tokenParam 53 | * @param string $scope A space-separated string of required scope(s), if you want to check for scope. 54 | * 55 | * @return IOAuth2AccessToken Token 56 | * 57 | * @throws OAuth2AuthenticateException 58 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-7 59 | * 60 | * @ingroup oauth2_section_7 61 | */ 62 | public function verifyAccessToken($tokenParam, $scope = null); 63 | 64 | /** 65 | * This is a convenience function that can be used to get the token, which can then 66 | * be passed to verifyAccessToken(). The constraints specified by the draft are 67 | * attempted to be adheared to in this method. 68 | * 69 | * As per the Bearer spec (draft 8, section 2) - there are three ways for a client 70 | * to specify the bearer token, in order of preference: Authorization Header, 71 | * POST and GET. 72 | * 73 | * NB: Resource servers MUST accept tokens via the Authorization scheme 74 | * (http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08#section-2). 75 | * 76 | * @param Request $request 77 | * @param bool $removeFromRequest 78 | * 79 | * @return string|null 80 | * @throws OAuth2AuthenticateException 81 | * @todo Should we enforce TLS/SSL in this function? 82 | * 83 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08#section-2.1 84 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08#section-2.2 85 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-08#section-2.3 86 | * 87 | */ 88 | public function getBearerToken(Request $request = null, $removeFromRequest = false); 89 | 90 | /** 91 | * Grant or deny a requested access token. 92 | * 93 | * This would be called from the "/token" endpoint as defined in the spec. 94 | * Obviously, you can call your endpoint whatever you want. 95 | * Draft specifies that the authorization parameters should be retrieved from POST, but you can override to whatever method you like. 96 | * 97 | * @param Request $request (optional) The request 98 | * 99 | * @return Response 100 | * @throws OAuth2ServerException 101 | * 102 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4 103 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-21#section-10.6 104 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-21#section-4.1.3 105 | * 106 | * @ingroup oauth2_section_4 107 | */ 108 | public function grantAccessToken(Request $request = null); 109 | 110 | /** 111 | * Redirect the user appropriately after approval. 112 | * 113 | * After the user has approved or denied the access request the authorization server should call this function to 114 | * redirect the user appropriately. 115 | * 116 | * @param bool $isAuthorized true or false depending on whether the user authorized the access. 117 | * @param mixed $data Application data 118 | * @param Request $request 119 | * @param string|null $scope 120 | * 121 | * @return Response 122 | * @throws OAuth2RedirectException 123 | * 124 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4 125 | * 126 | * @ingroup oauth2_section_4 127 | */ 128 | public function finishClientAuthorization($isAuthorized, $data = null, Request $request = null, $scope = null); 129 | 130 | /** 131 | * Handle the creation of access token, also issue refresh token if support. 132 | * 133 | * This belongs in a separate factory, but to keep it simple, I'm just keeping it here. 134 | * 135 | * @param IOAuth2Client $client 136 | * @param mixed $data 137 | * @param string|null $scope 138 | * @param int|null $access_token_lifetime How long the access token should live in seconds 139 | * @param bool $issue_refresh_token Issue a refresh tokeniIf true and the storage mechanism supports it 140 | * @param int|null $refresh_token_lifetime How long the refresh token should life in seconds 141 | * 142 | * @return array 143 | * 144 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-5 145 | * 146 | * @ingroup oauth2_section_5 147 | */ 148 | public function createAccessToken(IOAuth2Client $client, $data, $scope = null, $access_token_lifetime = null, $issue_refresh_token = true, $refresh_token_lifetime = null); 149 | } 150 | -------------------------------------------------------------------------------- /lib/IOAuth2GrantClient.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.4 13 | */ 14 | interface IOAuth2GrantClient extends IOAuth2Storage 15 | { 16 | /** 17 | * Required for OAuth2::GRANT_TYPE_CLIENT_CREDENTIALS. 18 | * 19 | * @param IOAuth2Client $client The client for which to check credentials. 20 | * @param string $clientSecret (optional) If a secret is required, check that they've given the right one. 21 | * 22 | * @return bool|array Returns true if the client credentials are valid, and MUST return false if they aren't. 23 | * When using "client credentials" grant mechanism and you want to 24 | * verify the scope of a user's access, return an associative array 25 | * with the scope values as below. We'll check the scope you provide 26 | * against the requested scope before providing an access token: 27 | * @code 28 | * return array( 29 | * 'scope' => , 30 | * ); 31 | * @endcode 32 | * 33 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.4.2 34 | * 35 | * @ingroup oauth2_section_4 36 | */ 37 | public function checkClientCredentialsGrant(IOAuth2Client $client, $clientSecret); 38 | } 39 | -------------------------------------------------------------------------------- /lib/IOAuth2GrantCode.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.1 14 | */ 15 | interface IOAuth2GrantCode extends IOAuth2Storage 16 | { 17 | /** 18 | * The Authorization Code grant type supports a response type of "code". 19 | * 20 | * @var string 21 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-1.4.1 22 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.2 23 | */ 24 | const RESPONSE_TYPE_CODE = OAuth2::RESPONSE_TYPE_AUTH_CODE; 25 | 26 | /** 27 | * Fetch authorization code data (probably the most common grant type). 28 | * 29 | * Retrieve the stored data for the given authorization code. 30 | * Required for OAuth2::GRANT_TYPE_AUTH_CODE. 31 | * 32 | * @param string $code The authorization code string for which to fetch data. 33 | * 34 | * @return IOAuth2AuthCode 35 | * 36 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.1 37 | * 38 | * @ingroup oauth2_section_4 39 | */ 40 | public function getAuthCode($code); 41 | 42 | /** 43 | * Take the provided authorization code values and store them somewhere. 44 | * 45 | * This function should be the storage counterpart to getAuthCode(). 46 | * If storage fails for some reason, we're not currently checking for any sort of success/failure, so you should 47 | * bail out of the script and provide a descriptive fail message. 48 | * Required for OAuth2::GRANT_TYPE_AUTH_CODE. 49 | * 50 | * @param string $code Authorization code string to be stored. 51 | * @param IOAuth2Client $client The client associated with this authorization code. 52 | * @param mixed $data Application data to associate with this authorization code, such as a User object. 53 | * @param string $redirectUri Redirect URI to be stored. 54 | * @param int $expires The timestamp when the authorization code will expire. 55 | * @param string $scope l(optional) Scopes to be stored in space-separated string. 56 | * 57 | * @ingroup oauth2_section_4 58 | */ 59 | public function createAuthCode($code, IOAuth2Client $client, $data, $redirectUri, $expires, $scope = null); 60 | 61 | /** 62 | * Marks auth code as expired. 63 | * 64 | * Depending on implementation it can change expiration date on auth code or remove it at all. 65 | * 66 | * @param string $code 67 | */ 68 | public function markAuthCodeAsUsed($code); 69 | } 70 | -------------------------------------------------------------------------------- /lib/IOAuth2GrantExtension.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * 14 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.5 15 | */ 16 | interface IOAuth2GrantExtension extends IOAuth2Storage 17 | { 18 | /** 19 | * Check any extended grant types. 20 | * 21 | * @param IOAuth2Client $client 22 | * @param string $uri URI of the grant type definition 23 | * @param array $inputData Unfiltered input data. The source is *not* guaranteed to be POST (but is likely to be). 24 | * @param array $authHeaders Authorization headers 25 | * 26 | * @return bool|array Returns false if the authorization is rejected or not support. Returns true or an associative array if you 27 | * want to verify the scope: 28 | * @code 29 | * return array( 30 | * 'scope' => , 31 | * ); 32 | * @endcode 33 | * 34 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-1.4.5 35 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.2 36 | */ 37 | public function checkGrantExtension(IOAuth2Client $client, $uri, array $inputData, array $authHeaders); 38 | } 39 | -------------------------------------------------------------------------------- /lib/IOAuth2GrantImplicit.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.2 11 | */ 12 | interface IOAuth2GrantImplicit extends IOAuth2Storage 13 | { 14 | /** 15 | * The Implicit grant type supports a response type of "token". 16 | * 17 | * @var string 18 | * 19 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-1.4.2 20 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.2 21 | */ 22 | const RESPONSE_TYPE_TOKEN = OAuth2::RESPONSE_TYPE_ACCESS_TOKEN; 23 | } 24 | -------------------------------------------------------------------------------- /lib/IOAuth2GrantUser.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.3 13 | */ 14 | interface IOAuth2GrantUser extends IOAuth2Storage 15 | { 16 | /** 17 | * Grant access tokens for basic user credentials. 18 | * 19 | * Check the supplied username and password for validity. 20 | * You can also use the $client param to do any checks required based on a client, if you need that. 21 | * Required for OAuth2::GRANT_TYPE_USER_CREDENTIALS. 22 | * 23 | * @param IOAuth2Client $client Client to check. 24 | * @param string $username Username to check. 25 | * @param string $password Password to check. 26 | * 27 | * @return bool|array Returns true if the username and password are valid or false if they aren't. 28 | * Moreover, if the username and password are valid, and you want to 29 | * verify the scope of a user's access, return an associative array 30 | * with the scope values as below. We'll check the scope you provide 31 | * against the requested scope before providing an access token: 32 | * @code 33 | * return array( 34 | * 'scope' => , 35 | * ); 36 | * @endcode 37 | * 38 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.3 39 | * 40 | * @ingroup oauth2_section_4 41 | */ 42 | public function checkUserCredentials(IOAuth2Client $client, $username, $password); 43 | } 44 | -------------------------------------------------------------------------------- /lib/IOAuth2RefreshTokens.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-6 14 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-1.5 15 | */ 16 | interface IOAuth2RefreshTokens extends IOAuth2Storage 17 | { 18 | /** 19 | * Grant refresh access tokens. 20 | * 21 | * Retrieve the stored data for the given refresh token. 22 | * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN. 23 | * 24 | * @param string $refreshToken Refresh token string. 25 | * 26 | * @return IOAuth2Token 27 | * 28 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-6 29 | * 30 | * @ingroup oauth2_section_6 31 | */ 32 | public function getRefreshToken($refreshToken); 33 | 34 | /** 35 | * Take the provided refresh token values and store them somewhere. 36 | * 37 | * This function should be the storage counterpart to getRefreshToken(). 38 | * If storage fails for some reason, we're not currently checking for 39 | * any sort of success/failure, so you should bail out of the script 40 | * and provide a descriptive fail message. 41 | * Required for OAuth2::GRANT_TYPE_REFRESH_TOKEN. 42 | * 43 | * @param string $refreshToken The refresh token string to be stored. 44 | * @param IOAuth2Client $client The client associated with this refresh token. 45 | * @param mixed $data Application data associated with the refresh token, such as a User object. 46 | * @param int $expires The timestamp when the refresh token will expire. 47 | * @param string $scope (optional) Scopes to be stored in space-separated string. 48 | * 49 | * @ingroup oauth2_section_6 50 | */ 51 | public function createRefreshToken($refreshToken, IOAuth2Client $client, $data, $expires, $scope = null); 52 | 53 | /** 54 | * Expire a used refresh token. 55 | * 56 | * This is not explicitly required in the spec, but is almost implied. After granting a new refresh token, the old 57 | * one is no longer useful and so should be forcibly expired in the data store so it can't be used again. 58 | * If storage fails for some reason, we're not currently checking for any sort of success/failure, so you should 59 | * bail out of the script and provide a descriptive fail message. 60 | * 61 | * @param string $refreshToken The refresh token string to expire. 62 | * 63 | * @ingroup oauth2_section_6 64 | */ 65 | public function unsetRefreshToken($refreshToken); 66 | } 67 | -------------------------------------------------------------------------------- /lib/IOAuth2Storage.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface IOAuth2Storage 14 | { 15 | /** 16 | * Get a client by its ID. 17 | * 18 | * @param string $clientId 19 | * 20 | * @return IOAuth2Client 21 | */ 22 | public function getClient($clientId); 23 | 24 | /** 25 | * Make sure that the client credentials are valid. 26 | * 27 | * @param IOAuth2Client $client The client for which to check credentials. 28 | * @param string $clientSecret (optional) If a secret is required, check that they've given the right one. 29 | * 30 | * @return bool TRUE if the client credentials are valid, and MUST return FALSE if they aren't. 31 | * 32 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-3.1 33 | * 34 | * @ingroup oauth2_section_3 35 | */ 36 | public function checkClientCredentials(IOAuth2Client $client, $clientSecret = null); 37 | 38 | /** 39 | * Look up the supplied oauth_token from storage. 40 | * 41 | * We need to retrieve access token data as we create and verify tokens. 42 | * 43 | * @param string $oauthToken The token string. 44 | * 45 | * @return IOAuth2AccessToken 46 | * 47 | * @ingroup oauth2_section_7 48 | */ 49 | public function getAccessToken($oauthToken); 50 | 51 | /** 52 | * Store the supplied access token values to storage. 53 | * 54 | * We need to store access token data as we create and verify tokens. 55 | * 56 | * @param string $oauthToken The access token string to be stored. 57 | * @param IOAuth2Client $client The client associated with this refresh token. 58 | * @param mixed $data Application data associated with the refresh token, such as a User object. 59 | * @param int $expires The timestamp when the refresh token will expire. 60 | * @param string $scope (optional) Scopes to be stored in space-separated string. 61 | * 62 | * @ingroup oauth2_section_4 63 | */ 64 | public function createAccessToken($oauthToken, IOAuth2Client $client, $data, $expires, $scope = null); 65 | 66 | /** 67 | * Check restricted grant types of corresponding client identifier. 68 | * 69 | * If you want to restrict clients to certain grant types, override this 70 | * function. 71 | * 72 | * @param IOAuth2Client $client Client to check. 73 | * @param string $grantType Grant type to check. One of the values contained in OAuth2::GRANT_TYPE_REGEXP. 74 | * 75 | * @return bool Returns true if the grant type is supported by this client identifier or false if it isn't. 76 | * 77 | * @ingroup oauth2_section_4 78 | */ 79 | public function checkRestrictedGrantType(IOAuth2Client $client, $grantType); 80 | } 81 | -------------------------------------------------------------------------------- /lib/Model/IOAuth2AccessToken.php: -------------------------------------------------------------------------------- 1 | setRedirectUri($redirectUri); 24 | } 25 | 26 | /** 27 | * @param null|string $uri 28 | */ 29 | public function setRedirectUri($uri) 30 | { 31 | $this->redirectUri = $uri; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getRedirectUri() 38 | { 39 | return $this->redirectUri; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/Model/OAuth2Client.php: -------------------------------------------------------------------------------- 1 | setPublicId($id); 30 | $this->setSecret($secret); 31 | $this->setRedirectUris($redirectUris); 32 | } 33 | 34 | /** 35 | * @param string $id 36 | */ 37 | public function setPublicId($id) 38 | { 39 | $this->id = $id; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function getPublicId() 46 | { 47 | return $this->id; 48 | } 49 | 50 | /** 51 | * @param string $secret 52 | */ 53 | public function setSecret($secret) 54 | { 55 | $this->secret = $secret; 56 | } 57 | 58 | /** 59 | * @param mixed $secret 60 | * 61 | * @return boolean 62 | */ 63 | public function checkSecret($secret) 64 | { 65 | return $this->secret === null || $secret === $this->secret; 66 | } 67 | 68 | /** 69 | * @param array $redirectUris 70 | */ 71 | public function setRedirectUris(array $redirectUris) 72 | { 73 | $this->redirectUris = $redirectUris; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function getRedirectUris() 80 | { 81 | return $this->redirectUris; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/Model/OAuth2RefreshToken.php: -------------------------------------------------------------------------------- 1 | setClientId($clientId); 42 | $this->setToken($token); 43 | $this->setExpiresAt($expiresAt); 44 | $this->setScope($scope); 45 | $this->setData($data); 46 | } 47 | 48 | /** 49 | * @param string $id 50 | */ 51 | public function setClientId($id) 52 | { 53 | $this->clientId = $id; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function getClientId() 60 | { 61 | return $this->clientId; 62 | } 63 | 64 | /** 65 | * @param null|integer $timestamp 66 | */ 67 | public function setExpiresAt($timestamp) 68 | { 69 | $this->expiresAt = $timestamp; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function getExpiresIn() 76 | { 77 | if ($this->expiresAt) { 78 | return $this->expiresAt - time(); 79 | } else { 80 | return PHP_INT_MAX; 81 | } 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function hasExpired() 88 | { 89 | return time() > $this->expiresAt; 90 | } 91 | 92 | /** 93 | * @param string $token 94 | */ 95 | public function setToken($token) 96 | { 97 | $this->token = $token; 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function getToken() 104 | { 105 | return $this->token; 106 | } 107 | 108 | /** 109 | * @param null|string $scope 110 | */ 111 | public function setScope($scope) 112 | { 113 | $this->scope = $scope; 114 | } 115 | 116 | /** 117 | * {@inheritdoc} 118 | */ 119 | public function getScope() 120 | { 121 | return $this->scope; 122 | } 123 | 124 | /** 125 | * @param null|string $data 126 | */ 127 | public function setData($data) 128 | { 129 | $this->data = $data; 130 | } 131 | 132 | /** 133 | * {@inheritdoc} 134 | */ 135 | public function getData() 136 | { 137 | return $this->data; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/OAuth2AuthenticateException.php: -------------------------------------------------------------------------------- 1 | errorData['scope'] = $scope; 34 | } 35 | 36 | // Build header 37 | $header = sprintf('%s realm=%s', ucwords($tokenType), $this->quote($realm)); 38 | foreach ($this->errorData as $key => $value) { 39 | $header .= sprintf(', %s=%s', $key, $this->quote($value)); 40 | } 41 | 42 | $this->header = array('WWW-Authenticate' => $header); 43 | } 44 | 45 | /** 46 | * @return array 47 | */ 48 | public function getResponseHeaders() 49 | { 50 | return $this->header + parent::getResponseHeaders(); 51 | } 52 | 53 | /** 54 | * Adds quotes around $text 55 | * 56 | * @param string $text 57 | * 58 | * @return string 59 | */ 60 | private function quote($text) 61 | { 62 | // https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-17#section-3.2.3 63 | $text = preg_replace( 64 | '~ 65 | [^ 66 | \x21-\x7E 67 | \x80-\xFF 68 | \ \t 69 | ] 70 | ~x', 71 | '', 72 | $text 73 | ); 74 | 75 | $text = addcslashes($text, '"\\'); 76 | 77 | return '"' . $text . '"'; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/OAuth2Client.php: -------------------------------------------------------------------------------- 1 | . 9 | * @author Update to draft v10 by Edison Wong . 10 | * 11 | * @sa Facebook PHP SDK. 12 | */ 13 | abstract class OAuth2Client 14 | { 15 | /** 16 | * The default Cache Lifetime (in seconds). 17 | * 18 | * @var int 19 | */ 20 | const DEFAULT_EXPIRES_IN = 3600; 21 | 22 | /** 23 | * The default Base domain for the Cookie. 24 | * 25 | * @var string 26 | */ 27 | const DEFAULT_BASE_DOMAIN = ''; 28 | 29 | /** 30 | * Array of persistent variables stored. 31 | * 32 | * @var array 33 | */ 34 | protected $conf = array(); 35 | 36 | /** 37 | * Returns a persistent variable. 38 | * 39 | * To avoid problems, always use lower case for persistent variable names. 40 | * 41 | * @param string $name The name of the variable to return. 42 | * @param mixed $default The default value to use if this variable has never been set. 43 | * 44 | * @return mixed The value of the variable. 45 | */ 46 | public function getVariable($name, $default = null) 47 | { 48 | return isset($this->conf[$name]) ? $this->conf[$name] : $default; 49 | } 50 | 51 | /** 52 | * Sets a persistent variable. 53 | * 54 | * To avoid problems, always use lower case for persistent variable names. 55 | * 56 | * @param string $name The name of the variable to set. 57 | * @param mixed $value The value to set. 58 | * 59 | * @return $this The client (for chained calls of this method) 60 | */ 61 | public function setVariable($name, $value) 62 | { 63 | $this->conf[$name] = $value; 64 | 65 | return $this; 66 | } 67 | 68 | // Stuff that should get overridden by subclasses. 69 | // 70 | // I don't want to make these abstract, because then subclasses would have 71 | // to implement all of them, which is too much work. 72 | // 73 | // So they're just stubs. Override the ones you need. 74 | 75 | /** 76 | * Initialize a Drupal OAuth2.0 Application. 77 | * 78 | * @param array $config An associative array as below: 79 | * - base_uri: The base URI for the OAuth2.0 endpoints. 80 | * - code: (optional) The authorization code. 81 | * - username: (optional) The username. 82 | * - password: (optional) The password. 83 | * - client_id: (optional) The application ID. 84 | * - client_secret: (optional) The application secret. 85 | * - authorize_uri: (optional) The end-user authorization endpoint URI. 86 | * - access_token_uri: (optional) The token endpoint URI. 87 | * - services_uri: (optional) The services endpoint URI. 88 | * - cookie_support: (optional) true to enable cookie support. 89 | * - base_domain: (optional) The domain for the cookie. 90 | * - file_upload_support: (optional) true if file uploads are enabled. 91 | */ 92 | public function __construct($config = array()) 93 | { 94 | // We must set base_uri first. 95 | $this->setVariable('base_uri', $config['base_uri']); 96 | unset($config['base_uri']); 97 | 98 | // Use predefined OAuth2.0 params, or get it from $_REQUEST. 99 | foreach (array('code', 'username', 'password') as $name) { 100 | if (isset($config[$name])) { 101 | $this->setVariable($name, $config[$name]); 102 | } else { 103 | if (isset($_REQUEST[$name]) && !empty($_REQUEST[$name])) { 104 | $this->setVariable($name, $_REQUEST[$name]); 105 | } 106 | } 107 | unset($config[$name]); 108 | } 109 | 110 | // Endpoint URIs. 111 | foreach (array('authorize_uri', 'access_token_uri', 'services_uri') as $name) { 112 | if (isset($config[$name])) { 113 | if (substr($config[$name], 0, 4) == "http") { 114 | $this->setVariable($name, $config[$name]); 115 | } else { 116 | $this->setVariable($name, $this->getVariable('base_uri') . $config[$name]); 117 | } 118 | } 119 | unset($config[$name]); 120 | } 121 | 122 | // Other else configurations. 123 | foreach ($config as $name => $value) { 124 | $this->setVariable($name, $value); 125 | } 126 | } 127 | 128 | /** 129 | * Try to get session object from custom method. 130 | * 131 | * By default we generate session object based on access_token response, or 132 | * if it is provided from server with $_REQUEST. For sure, if it is provided 133 | * by server it should follow our session object format. 134 | * 135 | * Session object provided by server can ensure the correct expires and 136 | * base_domain setup as predefined in server, also you may get more useful 137 | * information for custom functionality, too. BTW, this may require for 138 | * additional remote call overhead. 139 | * 140 | * You may wish to override this function with your custom version due to 141 | * your own server-side implementation. 142 | * 143 | * @param $accessToken (optional) A valid access token in associative array as below: 144 | * - accessToken: A valid accessToken generated by OAuth2.0 authorization endpoint. 145 | * - expires_in: (optional) A valid expires_in generated by OAuth2.0 authorization endpoint. 146 | * - refresh_token: (optional) A valid refresh_token generated by OAuth2.0 authorization endpoint. 147 | * - scope: (optional) A valid scope generated by OAuth2.0 authorization endpoint. 148 | * 149 | * @return string|null A valid session object in associative array for setup cookie, and null if not able to generate it with custom method. 150 | */ 151 | protected function getSessionObject($accessToken = null) 152 | { 153 | $session = null; 154 | 155 | // Try generate local version of session cookie. 156 | if (!empty($accessToken) && isset($accessToken['access_token'])) { 157 | $session['access_token'] = $accessToken['access_token']; 158 | $session['base_domain'] = $this->getVariable('base_domain', self::DEFAULT_BASE_DOMAIN); 159 | $session['expires'] = isset($accessToken['expires_in']) ? time() + $accessToken['expires_in'] : time( 160 | ) + $this->getVariable('expires_in', self::DEFAULT_EXPIRES_IN); 161 | $session['refresh_token'] = isset($accessToken['refresh_token']) ? $accessToken['refresh_token'] : ''; 162 | $session['scope'] = isset($accessToken['scope']) ? $accessToken['scope'] : ''; 163 | $session['secret'] = md5( 164 | base64_encode(pack('N6', mt_rand(), mt_rand(), mt_rand(), mt_rand(), mt_rand(), uniqid())) 165 | ); 166 | 167 | // Provide our own signature. 168 | $sig = self::generateSignature( 169 | $session, 170 | $this->getVariable('client_secret') 171 | ); 172 | $session['sig'] = $sig; 173 | } 174 | 175 | // Try loading session from $_REQUEST. 176 | if (!$session && isset($_REQUEST['session'])) { 177 | $session = json_decode( 178 | get_magic_quotes_gpc() 179 | ? stripslashes($_REQUEST['session']) 180 | : $_REQUEST['session'], 181 | true 182 | ); 183 | } 184 | 185 | return $session; 186 | } 187 | 188 | /** 189 | * Make an API call. 190 | * 191 | * Support both OAuth2.0 or normal GET/POST API call, with relative 192 | * or absolute URI. 193 | * 194 | * If no valid OAuth2.0 access token found in session object, this function 195 | * will automatically switch as normal remote API call without "oauth_token" 196 | * parameter. 197 | * 198 | * Assume server reply in JSON object and always decode during return. If 199 | * you hope to issue a raw query, please use makeRequest(). 200 | * 201 | * @param string $path The target path, relative to base_path/service_uri or an absolute URI. 202 | * @param string $method (optional) The HTTP method (default 'GET'). 203 | * @param array $params (optional The GET/POST parameters. 204 | * 205 | * @return string The JSON decoded response object. 206 | * 207 | * @throws OAuth2Exception 208 | */ 209 | public function api($path, $method = 'GET', $params = array()) 210 | { 211 | if (is_array($method) && empty($params)) { 212 | $params = $method; 213 | $method = 'GET'; 214 | } 215 | 216 | // json_encode all params values that are not strings. 217 | foreach ($params as $key => $value) { 218 | if (!is_string($value)) { 219 | $params[$key] = json_encode($value); 220 | } 221 | } 222 | 223 | $result = json_decode( 224 | $this->makeOAuth2Request( 225 | $this->getUri($path), 226 | $method, 227 | $params 228 | ), 229 | true 230 | ); 231 | 232 | // Results are returned, errors are thrown. 233 | if (is_array($result) && isset($result['error'])) { 234 | $e = new OAuth2Exception($result); 235 | switch ($e->getType()) { 236 | // OAuth 2.0 Draft 10 style. 237 | case 'invalid_token': 238 | $this->setSession(null); // TODO: what is this?!?! 239 | break; 240 | default: 241 | $this->setSession(null); 242 | } 243 | throw $e; 244 | } 245 | 246 | return $result; 247 | } 248 | 249 | // End stuff that should get overridden. 250 | 251 | /** 252 | * Default options for cURL. 253 | * 254 | * @var array 255 | */ 256 | public static $CURL_OPTS = array( 257 | CURLOPT_CONNECTTIMEOUT => 10, 258 | CURLOPT_RETURNTRANSFER => true, 259 | CURLOPT_HEADER => true, 260 | CURLOPT_TIMEOUT => 60, 261 | CURLOPT_USERAGENT => 'oauth2-draft-v10', 262 | CURLOPT_HTTPHEADER => array("Accept: application/json"), 263 | ); 264 | 265 | /** 266 | * Set the Session. 267 | * 268 | * @param $session (optional) The session object to be set. null if hope to frush existing session object. 269 | * @param $writeCookie (optional) true if a cookie should be written. This value is ignored if cookie support has been disabled. 270 | * 271 | * @return self The current OAuth2.0 client-side instance. 272 | */ 273 | public function setSession($session = null, $writeCookie = true) 274 | { 275 | $this->setVariable('_session', $this->validateSessionObject($session)); 276 | $this->setVariable('_session_loaded', true); 277 | if ($writeCookie) { 278 | $this->setCookieFromSession($this->getVariable('_session')); 279 | } 280 | 281 | return $this; 282 | } 283 | 284 | /** 285 | * Get the session object. 286 | * 287 | * This will automatically look for a signed session via custom method, OAuth2.0 grant type with authorization_code, 288 | * OAuth2.0 grant type with password, or cookie that we had already setup. 289 | * 290 | * @return array|null The valid session object with OAuth2.0 information, and null if not able to discover any cases. 291 | */ 292 | public function getSession() 293 | { 294 | if (!$this->getVariable('_session_loaded')) { 295 | $session = null; 296 | $write_cookie = true; 297 | 298 | // Try obtain login session by custom method. 299 | $session = $this->getSessionObject(null); 300 | $session = $this->validateSessionObject($session); 301 | 302 | // grant_type == authorization_code. 303 | if (!$session && $this->getVariable('code')) { 304 | $access_token = $this->getAccessTokenFromAuthorizationCode($this->getVariable('code')); 305 | $session = $this->getSessionObject($access_token); 306 | $session = $this->validateSessionObject($session); 307 | } 308 | 309 | // grant_type == password. 310 | if (!$session && $this->getVariable('username') && $this->getVariable('password')) { 311 | $access_token = $this->getAccessTokenFromPassword( 312 | $this->getVariable('username'), 313 | $this->getVariable('password') 314 | ); 315 | $session = $this->getSessionObject($access_token); 316 | $session = $this->validateSessionObject($session); 317 | } 318 | 319 | // Try loading session from cookie if necessary. 320 | if (!$session && $this->getVariable('cookie_support')) { 321 | $cookie_name = $this->getSessionCookieName(); 322 | if (isset($_COOKIE[$cookie_name])) { 323 | $session = array(); 324 | parse_str( 325 | trim( 326 | get_magic_quotes_gpc() 327 | ? stripslashes($_COOKIE[$cookie_name]) 328 | : $_COOKIE[$cookie_name], 329 | '"' 330 | ), 331 | $session 332 | ); 333 | $session = $this->validateSessionObject($session); 334 | // Write only if we need to delete a invalid session cookie. 335 | $write_cookie = empty($session); 336 | } 337 | } 338 | 339 | $this->setSession($session, $write_cookie); 340 | } 341 | 342 | return $this->getVariable('_session'); 343 | } 344 | 345 | /** 346 | * Gets an OAuth2.0 access token from session. 347 | * 348 | * This will trigger getSession() and so we MUST initialize with required 349 | * configuration. 350 | * 351 | * @return string|null The valid OAuth2.0 access token, and null if not exists in session. 352 | */ 353 | public function getAccessToken() 354 | { 355 | $session = $this->getSession(); 356 | 357 | return isset($session['access_token']) ? $session['access_token'] : null; 358 | } 359 | 360 | /** 361 | * Get access token from OAuth2.0 token endpoint with authorization code. 362 | * 363 | * This function will only be activated if both access token URI, client 364 | * identifier and client secret are setup correctly. 365 | * 366 | * @param string $code Authorization code issued by authorization server's authorization endpoint. 367 | * 368 | * @return string A valid OAuth2.0 JSON decoded access token in associative array, and null if not enough parameters or JSON decode failed. 369 | */ 370 | private function getAccessTokenFromAuthorizationCode($code) 371 | { 372 | if ($this->getVariable('access_token_uri') && $this->getVariable('client_id') && $this->getVariable( 373 | 'client_secret' 374 | ) 375 | ) { 376 | return json_decode( 377 | $this->makeRequest( 378 | $this->getVariable('access_token_uri'), 379 | 'POST', 380 | array( 381 | 'grant_type' => 'authorization_code', 382 | 'client_id' => $this->getVariable('client_id'), 383 | 'client_secret' => $this->getVariable('client_secret'), 384 | 'code' => $code, 385 | 'redirect_uri' => $this->getCurrentUri(), 386 | ) 387 | ), 388 | true 389 | ); 390 | } 391 | 392 | return null; 393 | } 394 | 395 | /** 396 | * Get access token from OAuth2.0 token endpoint with basic user credentials. 397 | * 398 | * This function will only be activated if both username and password are setup correctly. 399 | * 400 | * @param string $username Username to be check with. 401 | * @param string $password Password to be check with. 402 | * 403 | * @return array|null A valid OAuth2.0 JSON decoded access token in associative array, and null if not enough parameters or JSON decode failed. 404 | */ 405 | private function getAccessTokenFromPassword($username, $password) 406 | { 407 | if ($this->getVariable('access_token_uri') && $this->getVariable('client_id') && $this->getVariable( 408 | 'client_secret' 409 | ) 410 | ) { 411 | return json_decode( 412 | $this->makeRequest( 413 | $this->getVariable('access_token_uri'), 414 | 'POST', 415 | array( 416 | 'grant_type' => 'password', 417 | 'client_id' => $this->getVariable('client_id'), 418 | 'client_secret' => $this->getVariable('client_secret'), 419 | 'username' => $username, 420 | 'password' => $password, 421 | ) 422 | ), 423 | true 424 | ); 425 | } 426 | 427 | return null; 428 | } 429 | 430 | /** 431 | * Make an OAuth2.0 Request. 432 | * 433 | * Automatically append "oauth_token" in query parameters if not yet 434 | * exists and able to discover a valid access token from session. Otherwise 435 | * just ignore setup with "oauth_token" and handle the API call AS-IS, and 436 | * so may issue a plain API call without OAuth2.0 protection. 437 | * 438 | * @param string $path The target path, relative to base_path/service_uri or an absolute URI. 439 | * @param string $method (optional) The HTTP method (default 'GET'). 440 | * @param array $params (optional The GET/POST parameters. 441 | * 442 | * @return string The JSON decoded response object. 443 | * 444 | * @throws OAuth2Exception 445 | */ 446 | protected function makeOAuth2Request($path, $method = 'GET', $params = array()) 447 | { 448 | if ((!isset($params['oauth_token']) || empty($params['oauth_token'])) && $oauth_token = $this->getAccessToken() 449 | ) { 450 | $params['oauth_token'] = $oauth_token; 451 | } 452 | 453 | return $this->makeRequest($path, $method, $params); 454 | } 455 | 456 | /** 457 | * Makes an HTTP request. 458 | * 459 | * This method can be overriden by subclasses if developers want to do 460 | * fancier things or use something other than cURL to make the request. 461 | * 462 | * @param string $path The target path, relative to base_path/service_uri or an absolute URI. 463 | * @param string $method (optional) The HTTP method (default 'GET'). 464 | * @param array $params (optional The GET/POST parameters. 465 | * @param resource|null $ch (optional) An initialized curl handle 466 | * 467 | * @throws OAuth2Exception 468 | * @return string The JSON decoded response object. 469 | */ 470 | protected function makeRequest($path, $method = 'GET', $params = array(), $ch = null) 471 | { 472 | if (!$ch) { 473 | $ch = curl_init(); 474 | } 475 | 476 | $opts = self::$CURL_OPTS; 477 | if ($params) { 478 | switch ($method) { 479 | case 'GET': 480 | $path .= '?' . http_build_query($params, null, '&'); 481 | break; 482 | // Method override as we always do a POST. 483 | default: 484 | if ($this->getVariable('file_upload_support')) { 485 | $opts[CURLOPT_POSTFIELDS] = $params; 486 | } else { 487 | $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&'); 488 | } 489 | } 490 | } 491 | $opts[CURLOPT_URL] = $path; 492 | 493 | // Disable the 'Expect: 100-continue' behaviour. This causes CURL to wait 494 | // for 2 seconds if the server does not support this header. 495 | if (isset($opts[CURLOPT_HTTPHEADER])) { 496 | $existing_headers = $opts[CURLOPT_HTTPHEADER]; 497 | $existing_headers[] = 'Expect:'; 498 | $opts[CURLOPT_HTTPHEADER] = $existing_headers; 499 | } else { 500 | $opts[CURLOPT_HTTPHEADER] = array('Expect:'); 501 | } 502 | 503 | curl_setopt_array($ch, $opts); 504 | $result = curl_exec($ch); 505 | 506 | if (curl_errno($ch) == 60) { // CURLE_SSL_CACERT 507 | error_log('Invalid or no certificate authority found, using bundled information'); 508 | curl_setopt( 509 | $ch, 510 | CURLOPT_CAINFO, 511 | dirname(__FILE__) . '/fb_ca_chain_bundle.crt' 512 | ); 513 | $result = curl_exec($ch); 514 | } 515 | 516 | if ($result === false) { 517 | $e = new OAuth2Exception(array( 518 | 'code' => curl_errno($ch), 519 | 'message' => curl_error($ch), 520 | )); 521 | curl_close($ch); 522 | throw $e; 523 | } 524 | curl_close($ch); 525 | 526 | // Split the HTTP response into header and body. 527 | list($headers, $body) = explode("\r\n\r\n", $result); 528 | $headers = explode("\r\n", $headers); 529 | 530 | // We catch HTTP/1.1 4xx or HTTP/1.1 5xx error response. 531 | if (strpos($headers[0], 'HTTP/1.1 4') !== false || strpos($headers[0], 'HTTP/1.1 5') !== false) { 532 | $result = array( 533 | 'code' => 0, 534 | 'message' => '', 535 | ); 536 | 537 | if (preg_match('/^HTTP\/1.1 ([0-9]{3,3}) (.*)$/', $headers[0], $matches)) { 538 | $result['code'] = $matches[1]; 539 | $result['message'] = $matches[2]; 540 | } 541 | 542 | // In case retrun with WWW-Authenticate replace the description. 543 | foreach ($headers as $header) { 544 | if (preg_match("/^WWW-Authenticate:.*error='(.*)'/", $header, $matches)) { 545 | $result['error'] = $matches[1]; 546 | } 547 | } 548 | 549 | return json_encode($result); 550 | } 551 | 552 | return $body; 553 | } 554 | 555 | /** 556 | * The name of the cookie that contains the session object. 557 | * 558 | * @return string The cookie name. 559 | */ 560 | private function getSessionCookieName() 561 | { 562 | return 'oauth2_' . $this->getVariable('client_id'); 563 | } 564 | 565 | /** 566 | * Set a JS Cookie based on the _passed in_ session. 567 | * 568 | * It does not use the currently stored session - you need to explicitly 569 | * pass it in. 570 | * 571 | * @param array|null $session The session to use for setting the cookie. 572 | */ 573 | protected function setCookieFromSession($session = null) 574 | { 575 | if (!$this->getVariable('cookie_support')) { 576 | return; 577 | } 578 | 579 | $cookie_name = $this->getSessionCookieName(); 580 | $value = 'deleted'; 581 | $expires = time() - 3600; 582 | $base_domain = $this->getVariable('base_domain', self::DEFAULT_BASE_DOMAIN); 583 | if ($session) { 584 | $value = '"' . http_build_query($session, null, '&') . '"'; 585 | $base_domain = isset($session['base_domain']) ? $session['base_domain'] : $base_domain; 586 | $expires = isset($session['expires']) ? $session['expires'] : time() + $this->getVariable( 587 | 'expires_in', 588 | self::DEFAULT_EXPIRES_IN 589 | ); 590 | } 591 | 592 | // Prepend dot if a domain is found. 593 | if ($base_domain) { 594 | $base_domain = '.' . $base_domain; 595 | } 596 | 597 | // If an existing cookie is not set, we dont need to delete it. 598 | if ($value == 'deleted' && empty($_COOKIE[$cookie_name])) { 599 | return; 600 | } 601 | 602 | if (headers_sent()) { 603 | error_log('Could not set cookie. Headers already sent.'); 604 | } else { 605 | setcookie($cookie_name, $value, $expires, '/', $base_domain); 606 | } 607 | } 608 | 609 | /** 610 | * Validates a session_version = 3 style session object. 611 | * 612 | * @param array $session The session object. 613 | * 614 | * @return array|null The session object if it validates, null otherwise. 615 | */ 616 | protected function validateSessionObject($session) 617 | { 618 | // Make sure some essential fields exist. 619 | if (is_array($session) && isset($session['access_token']) && isset($session['sig'])) { 620 | // Validate the signature. 621 | $sessionWithoutSig = $session; 622 | unset($sessionWithoutSig['sig']); 623 | 624 | $expected_sig = self::generateSignature( 625 | $sessionWithoutSig, 626 | $this->getVariable('client_secret') 627 | ); 628 | 629 | if ($session['sig'] != $expected_sig) { 630 | error_log('Got invalid session signature in cookie.'); 631 | $session = null; 632 | } 633 | } else { 634 | $session = null; 635 | } 636 | 637 | return $session; 638 | } 639 | 640 | /** 641 | * Since $_SERVER['REQUEST_URI'] is only available on Apache, we 642 | * generate an equivalent using other environment variables. 643 | */ 644 | public function getRequestUri() 645 | { 646 | if (isset($_SERVER['REQUEST_URI'])) { 647 | $uri = $_SERVER['REQUEST_URI']; 648 | } else { 649 | if (isset($_SERVER['argv'])) { 650 | $uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['argv'][0]; 651 | } elseif (isset($_SERVER['QUERY_STRING'])) { 652 | $uri = $_SERVER['SCRIPT_NAME'] . '?' . $_SERVER['QUERY_STRING']; 653 | } else { 654 | $uri = $_SERVER['SCRIPT_NAME']; 655 | } 656 | } 657 | // Prevent multiple slashes to avoid cross site requests via the Form API. 658 | $uri = '/' . ltrim($uri, '/'); 659 | 660 | return $uri; 661 | } 662 | 663 | /** 664 | * Returns the Current URL. 665 | * 666 | * @return string The current URL. 667 | */ 668 | protected function getCurrentUri() 669 | { 670 | $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' 671 | ? 'https://' 672 | : 'http://'; 673 | $current_uri = $protocol . $_SERVER['HTTP_HOST'] . $this->getRequestUri(); 674 | $parts = parse_url($current_uri); 675 | 676 | $query = ''; 677 | if (!empty($parts['query'])) { 678 | $params = array(); 679 | parse_str($parts['query'], $params); 680 | $params = array_filter($params); 681 | if (!empty($params)) { 682 | $query = '?' . http_build_query($params, null, '&'); 683 | } 684 | } 685 | 686 | // Use port if non default. 687 | $port = isset($parts['port']) && 688 | (($protocol === 'http://' && $parts['port'] !== 80) || ($protocol === 'https://' && $parts['port'] !== 443)) 689 | ? ':' . $parts['port'] : ''; 690 | 691 | // Rebuild. 692 | return $protocol . $parts['host'] . $port . $parts['path'] . $query; 693 | } 694 | 695 | /** 696 | * Build the URL for given path and parameters. 697 | * 698 | * @param $path (optional) The path. 699 | * @param $params (optional) The query parameters in associative array. 700 | * 701 | * @return string The URL for the given parameters. 702 | */ 703 | protected function getUri($path = '', $params = array()) 704 | { 705 | $url = $this->getVariable('services_uri') ? $this->getVariable('services_uri') : $this->getVariable('base_uri'); 706 | 707 | if (!empty($path)) { 708 | if (substr($path, 0, 4) == "http") { 709 | $url = $path; 710 | } else { 711 | $url = rtrim($url, '/') . '/' . ltrim($path, '/'); 712 | } 713 | } 714 | 715 | if (!empty($params)) { 716 | $url .= '?' . http_build_query($params, null, '&'); 717 | } 718 | 719 | return $url; 720 | } 721 | 722 | /** 723 | * Generate a signature for the given params and secret. 724 | * 725 | * @param array $params The parameters to sign. 726 | * @param string $secret The secret to sign with. 727 | * 728 | * @return string The generated signature 729 | */ 730 | protected function generateSignature($params, $secret) 731 | { 732 | // Work with sorted data. 733 | ksort($params); 734 | 735 | // Generate the base string. 736 | $base_string = ''; 737 | foreach ($params as $key => $value) { 738 | $base_string .= $key . '=' . $value; 739 | } 740 | $base_string .= $secret; 741 | 742 | return md5($base_string); 743 | } 744 | } 745 | -------------------------------------------------------------------------------- /lib/OAuth2Exception.php: -------------------------------------------------------------------------------- 1 | . 9 | * @author Update to draft v10 by Edison Wong . 10 | * 11 | * @sa Facebook PHP SDK. 12 | */ 13 | class OAuth2Exception extends \Exception 14 | { 15 | /** 16 | * The result from the API server that represents the exception information. 17 | * 18 | * @var array 19 | */ 20 | protected $result; 21 | 22 | /** 23 | * Make a new API Exception with the given result. 24 | * 25 | * @param array $result The result from the API server. 26 | */ 27 | public function __construct($result) 28 | { 29 | $this->result = $result; 30 | 31 | $code = isset($result['code']) ? $result['code'] : 0; 32 | 33 | if (isset($result['error'])) { 34 | // OAuth 2.0 Draft 10 style 35 | $message = $result['error']; 36 | } elseif (isset($result['message'])) { 37 | // cURL style 38 | $message = $result['message']; 39 | } else { 40 | $message = 'Unknown Error. Check getResult()'; 41 | } 42 | 43 | parent::__construct($message, $code); 44 | } 45 | 46 | /** 47 | * Return the associated result object returned by the API server. 48 | * 49 | * @return array The result from the API server. 50 | */ 51 | public function getResult() 52 | { 53 | return $this->result; 54 | } 55 | 56 | /** 57 | * Returns the associated type for the error. This will default to 58 | * 'Exception' when a type is not available. 59 | * 60 | * @return string The type for the error. 61 | */ 62 | public function getType() 63 | { 64 | if (isset($this->result['error'])) { 65 | $message = $this->result['error']; 66 | if (is_string($message)) { 67 | // OAuth 2.0 Draft 10 style 68 | return $message; 69 | } 70 | } 71 | 72 | return 'Exception'; 73 | } 74 | 75 | /** 76 | * To make debugging easier. 77 | * 78 | * @return string The string representation of the error. 79 | */ 80 | public function __toString() 81 | { 82 | $str = $this->getType() . ': '; 83 | if ($this->code != 0) { 84 | $str .= $this->code . ': '; 85 | } 86 | 87 | return $str . $this->message; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/OAuth2RedirectException.php: -------------------------------------------------------------------------------- 1 | method = $method; 45 | $this->redirectUri = $redirectUri; 46 | if ($state) { 47 | $this->errorData['state'] = $state; 48 | } 49 | } 50 | 51 | /** 52 | * Redirect the user agent. 53 | * 54 | * @return array 55 | * 56 | * @ingroup oauth2_section_4 57 | */ 58 | public function getResponseHeaders() 59 | { 60 | $params = array($this->method => $this->errorData); 61 | 62 | return array( 63 | 'Location' => $this->buildUri($this->redirectUri, $params), 64 | ); 65 | } 66 | 67 | /** 68 | * Build the absolute URI based on supplied URI and parameters. 69 | * 70 | * @param string $uri An absolute URI. 71 | * @param array $params Parameters to be append as GET. 72 | * 73 | * @return string An absolute URI with supplied parameters. 74 | * 75 | * @ingroup oauth2_section_4 76 | */ 77 | protected function buildUri($uri, $params) 78 | { 79 | $parse_url = parse_url($uri); 80 | 81 | // Add our params to the parsed uri 82 | foreach ($params as $k => $v) { 83 | if (isset($parse_url[$k])) { 84 | $parse_url[$k] .= "&" . http_build_query($v); 85 | } else { 86 | $parse_url[$k] = http_build_query($v); 87 | } 88 | } 89 | 90 | // Put humpty dumpty back together 91 | return 92 | ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "") 93 | . ((isset($parse_url["user"])) ? $parse_url["user"] . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") . "@" : "") 94 | . ((isset($parse_url["host"])) ? $parse_url["host"] : "") 95 | . ((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "") 96 | . ((isset($parse_url["path"])) ? $parse_url["path"] : "") 97 | . ((isset($parse_url["query"])) ? "?" . $parse_url["query"] : "") 98 | . ((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : ""); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/OAuth2ServerException.php: -------------------------------------------------------------------------------- 1 | httpCode = $httpStatusCode; 32 | 33 | $this->errorData['error'] = $error; 34 | $this->errorData['error_description'] = $errorDescription; 35 | } 36 | 37 | /** 38 | * Get error description 39 | * 40 | * @return string 41 | */ 42 | public function getDescription() 43 | { 44 | return $this->errorData['error_description']; 45 | } 46 | 47 | /** 48 | * Get HTTP code 49 | * 50 | * @return string 51 | */ 52 | public function getHttpCode() 53 | { 54 | return $this->httpCode; 55 | } 56 | 57 | /** 58 | * Get HTTP Error Response 59 | * 60 | * @return Response 61 | * 62 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-5.1 63 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-5.2 64 | * 65 | * @ingroup oauth2_error 66 | */ 67 | public function getHttpResponse() 68 | { 69 | return new Response( 70 | $this->getResponseBody(), 71 | $this->getHttpCode(), 72 | $this->getResponseHeaders() 73 | ); 74 | } 75 | 76 | /** 77 | * Get HTTP Error Response headers 78 | * 79 | * @return array 80 | * 81 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-5.2 82 | * 83 | * @ingroup oauth2_error 84 | */ 85 | public function getResponseHeaders() 86 | { 87 | return array( 88 | 'Content-Type' => 'application/json', 89 | 'Cache-Control' => 'no-store', 90 | 'Pragma' => 'no-cache', 91 | ); 92 | } 93 | 94 | /** 95 | * Get response body as JSON string 96 | * 97 | * @return string 98 | */ 99 | public function getResponseBody() 100 | { 101 | return json_encode($this->errorData); 102 | } 103 | 104 | /** 105 | * Outputs response 106 | */ 107 | public function sendHttpResponse() 108 | { 109 | $this->getHttpResponse()->send(); 110 | exit; // TODO: refactor out this piece of code 111 | } 112 | 113 | /** 114 | * @see \Exception::__toString() 115 | */ 116 | public function __toString() 117 | { 118 | return $this->getResponseBody(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests 17 | 18 | 19 | 20 | 21 | 22 | ./lib/ 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /server/examples/mongo/addclient.php: -------------------------------------------------------------------------------- 1 | addClient($_POST["client_id"], $_POST["client_secret"], $_POST["redirect_uri"]); 15 | } 16 | 17 | ?> 18 | 19 | 20 | Add Client 21 | 22 |
23 |

24 | 25 | 26 |

27 |

28 | 29 | 30 |

31 |

32 | 33 | 34 |

35 | 36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /server/examples/mongo/authorize.php: -------------------------------------------------------------------------------- 1 | finishClientAuthorization($_POST["accept"] == "Yep", $_POST); 18 | } 19 | 20 | try { 21 | $auth_params = $oauth->getAuthorizeParams(); 22 | } catch (OAuth2ServerException $oauthError) { 23 | $oauthError->sendHttpResponse(); 24 | } 25 | 26 | ?> 27 | 28 | Authorize 29 | 30 |
31 | $v) { ?> 32 | 33 | 34 | Do you authorize the app to do its thing? 35 |

36 | 37 | 38 |

39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /server/examples/mongo/lib/OAuth2StorageMongo.php: -------------------------------------------------------------------------------- 1 | db = $mongo->selectDB(self::DB); 40 | } 41 | 42 | /** 43 | * Release DB connection during destruct. 44 | */ 45 | public function __destruct() 46 | { 47 | $this->db = null; // Release db connection 48 | } 49 | 50 | /** 51 | * Handle PDO exceptional cases. 52 | */ 53 | private function handleException($e) 54 | { 55 | echo 'Database error: '. $e->getMessage(); 56 | exit; 57 | } 58 | 59 | /** 60 | * Little helper function to add a new client to the database. 61 | * 62 | * @param $client_id 63 | * Client identifier to be stored. 64 | * @param $client_secret 65 | * Client secret to be stored. 66 | * @param $redirect_uri 67 | * Redirect URI to be stored. 68 | */ 69 | public function addClient($client_id, $client_secret, $redirect_uri) 70 | { 71 | $this->db->clients->insert(array( 72 | "_id" => $client_id, 73 | "pw" => $this->hash($client_secret, $client_id), 74 | "redirect_uri" => $redirect_uri 75 | )); 76 | } 77 | 78 | /** 79 | * Implements IOAuth2Storage::checkClientCredentials(). 80 | * 81 | */ 82 | public function checkClientCredentials($client_id, $client_secret = null) 83 | { 84 | $client = $this->db->clients->findOne(array("_id" => $client_id, "pw" => $client_secret)); 85 | 86 | return $this->checkPassword($client_secret, $result['client_secret'], $client_id); 87 | } 88 | 89 | /** 90 | * Implements IOAuth2Storage::getRedirectUri(). 91 | */ 92 | public function getClientDetails($client_id) 93 | { 94 | $result = $this->db->clients->findOne(array("_id" => $client_id), array("redirect_uri")); 95 | } 96 | 97 | /** 98 | * Implements IOAuth2Storage::getAccessToken(). 99 | */ 100 | public function getAccessToken($oauth_token) 101 | { 102 | return $this->db->tokens->findOne(array("_id" => $oauth_token)); 103 | } 104 | 105 | /** 106 | * Implements IOAuth2Storage::setAccessToken(). 107 | */ 108 | public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null) 109 | { 110 | $this->db->tokens->insert(array( 111 | "_id" => $oauth_token, 112 | "client_id" => $client_id, 113 | "expires" => $expires, 114 | "scope" => $scope 115 | )); 116 | } 117 | 118 | /** 119 | * @see IOAuth2Storage::getRefreshToken() 120 | */ 121 | public function getRefreshToken($refresh_token) 122 | { 123 | return $this->getToken($refresh_token, true); 124 | } 125 | 126 | /** 127 | * @see IOAuth2Storage::setRefreshToken() 128 | */ 129 | public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) 130 | { 131 | return $this->setToken($refresh_token, $client_id, $user_id, $expires, $scope, true); 132 | } 133 | 134 | /** 135 | * @see IOAuth2Storage::unsetRefreshToken() 136 | */ 137 | public function unsetRefreshToken($refresh_token) 138 | { 139 | try { 140 | $sql = 'DELETE FROM '.self::TABLE_TOKENS.' WHERE refresh_token = :refresh_token'; 141 | $stmt = $this->db->prepare($sql); 142 | $stmt->bindParam(':refresh_token', $refresh_token, PDO::PARAM_STR); 143 | $stmt->execute(); 144 | } catch (PDOException $e) { 145 | $this->handleException($e); 146 | } 147 | } 148 | 149 | /** 150 | * Implements IOAuth2Storage::getAuthCode(). 151 | */ 152 | public function getAuthCode($code) 153 | { 154 | $stored_code = $this->db->auth_codes->findOne(array("_id" => $code)); 155 | 156 | return $stored_code !== null ? $stored_code : false; 157 | } 158 | 159 | /** 160 | * Implements IOAuth2Storage::setAuthCode(). 161 | */ 162 | public function setAuthCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null) 163 | { 164 | $this->db->auth_codes->insert(array( 165 | "_id" => $code, 166 | "client_id" => $client_id, 167 | "redirect_uri" => $redirect_uri, 168 | "expires" => $expires, 169 | "scope" => $scope 170 | )); 171 | } 172 | 173 | /** 174 | * @see IOAuth2Storage::checkRestrictedGrantType() 175 | */ 176 | public function checkRestrictedGrantType($client_id, $grant_type) 177 | { 178 | return true; // Not implemented 179 | } 180 | 181 | /** 182 | * Change/override this to whatever your own password hashing method is. 183 | * 184 | * @param string $secret 185 | * @return string 186 | */ 187 | protected function hash($client_secret, $client_id) 188 | { 189 | return hash('blowfish', $client_id.$client_secret.self::SALT); 190 | } 191 | 192 | /** 193 | * Checks the password. 194 | * Override this if you need to 195 | * 196 | * @param string $client_id 197 | * @param string $client_secret 198 | * @param string $actualPassword 199 | */ 200 | protected function checkPassword($try, $client_secret, $client_id) 201 | { 202 | return $try == $this->hash($client_secret, $client_id); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /server/examples/mongo/protected_resource.php: -------------------------------------------------------------------------------- 1 | verifyAccessToken($token); 19 | } catch (OAuth2ServerException $oauthError) { 20 | $oauthError->sendHttpResponse(); 21 | } 22 | 23 | // With a particular scope, you'd do: 24 | // $oauth->verifyAccessToken("scope_name"); 25 | 26 | ?> 27 | 28 | 29 | 30 | Hello! 31 | 32 | 33 |

This is a secret.

34 | 35 | 36 | -------------------------------------------------------------------------------- /server/examples/mongo/token.php: -------------------------------------------------------------------------------- 1 | grantAccessToken(); 17 | } catch (OAuth2ServerException $oauthError) { 18 | $oauthError->sendHttpResponse(); 19 | } 20 | -------------------------------------------------------------------------------- /server/examples/pdo/addclient.php: -------------------------------------------------------------------------------- 1 | addClient($_POST["client_id"], $_POST["client_secret"], $_POST["redirect_uri"]); 15 | } 16 | 17 | ?> 18 | 19 | 20 | 21 | Add Client 22 | 23 | 24 |
25 |

26 | 27 | 28 |

29 |

30 | 31 | 32 |

33 |

34 | 35 | 36 |

37 | 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /server/examples/pdo/authorize.php: -------------------------------------------------------------------------------- 1 | finishClientAuthorization($_POST["accept"] == "Yep", $userId); 37 | $response->send(); 38 | } catch (OAuth2ServerException $e) { 39 | $e->getHttpResponse()->send(); 40 | } 41 | exit; 42 | } 43 | 44 | try { 45 | $auth_params = $oauth->getAuthorizeParams(); 46 | } catch (OAuth2ServerException $oauthError) { 47 | $oauthError->sendHttpResponse(); 48 | } 49 | 50 | ?> 51 | 52 | 53 | Authorize 54 | 60 | 61 | 62 |
63 | $value) : ?> 64 | 65 | 66 | Do you authorize the app to do its thing? 67 |

68 | 69 | 70 |

71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /server/examples/pdo/lib/OAuth2StoragePdo.php: -------------------------------------------------------------------------------- 1 | db = $db; 47 | } 48 | 49 | /** 50 | * Handle PDO exceptional cases. 51 | */ 52 | private function handleException($e) 53 | { 54 | throw $e; 55 | } 56 | 57 | /** 58 | * Little helper function to add a new client to the database. 59 | * 60 | * Do NOT use this in production! This sample code stores the secret 61 | * in plaintext! 62 | * 63 | * @param string $clientId Client identifier to be stored. 64 | * @param string $clientSecret Client secret to be stored. 65 | * @param string $redirectUri Redirect URI to be stored. 66 | */ 67 | public function addClient($clientId, $clientSecret, $redirectUri) 68 | { 69 | try { 70 | $clientSecret = $this->hash($clientSecret, $clientId); 71 | 72 | $sql = 'INSERT INTO '.self::TABLE_CLIENTS.' (client_id, client_secret, redirect_uri) VALUES (:client_id, :client_secret, :redirect_uri)'; 73 | $stmt = $this->db->prepare($sql); 74 | $stmt->bindParam(':client_id', $clientId, PDO::PARAM_STR); 75 | $stmt->bindParam(':client_secret', $clientSecret, PDO::PARAM_STR); 76 | $stmt->bindParam(':redirect_uri', $redirectUri, PDO::PARAM_STR); 77 | $stmt->execute(); 78 | } catch (PDOException $e) { 79 | $this->handleException($e); 80 | } 81 | } 82 | 83 | /** 84 | * Implements IOAuth2Storage::checkClientCredentials(). 85 | * 86 | */ 87 | public function checkClientCredentials(IOAuth2Client $clientId, $clientSecret = null) 88 | { 89 | try { 90 | $sql = 'SELECT client_secret FROM '.self::TABLE_CLIENTS.' WHERE client_id = :client_id'; 91 | $stmt = $this->db->prepare($sql); 92 | $stmt->bindParam(':client_id', $clientId, PDO::PARAM_STR); 93 | $stmt->execute(); 94 | 95 | $result = $stmt->fetch(PDO::FETCH_ASSOC); 96 | 97 | if ($clientSecret === null) { 98 | return $result !== false; 99 | } 100 | 101 | return $this->checkPassword($clientSecret, $result['client_secret'], $clientId); 102 | } catch (PDOException $e) { 103 | $this->handleException($e); 104 | } 105 | } 106 | 107 | /** 108 | * Implements IOAuth2Storage::getRedirectUri(). 109 | */ 110 | public function getClientDetails($clientId) 111 | { 112 | try { 113 | $sql = 'SELECT redirect_uri FROM '.self::TABLE_CLIENTS.' WHERE client_id = :client_id'; 114 | $stmt = $this->db->prepare($sql); 115 | $stmt->bindParam(':client_id', $clientId, PDO::PARAM_STR); 116 | $stmt->execute(); 117 | 118 | $result = $stmt->fetch(PDO::FETCH_ASSOC); 119 | 120 | if ($result === false) 121 | return false; 122 | 123 | return isset($result['redirect_uri']) && $result['redirect_uri'] ? $result : null; 124 | } catch (PDOException $e) { 125 | $this->handleException($e); 126 | } 127 | } 128 | 129 | /** 130 | * Implements IOAuth2Storage::getAccessToken(). 131 | */ 132 | public function getAccessToken($oauth_token) 133 | { 134 | return $this->getToken($oauth_token, false); 135 | } 136 | 137 | /** 138 | * Implements IOAuth2Storage::setAccessToken(). 139 | */ 140 | public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null) 141 | { 142 | $this->setToken($oauth_token, $client_id, $user_id, $expires, $scope, false); 143 | } 144 | 145 | /** 146 | * @see IOAuth2Storage::getRefreshToken() 147 | */ 148 | public function getRefreshToken($refreshToken) 149 | { 150 | return $this->getToken($refreshToken, true); 151 | } 152 | 153 | /** 154 | * @see IOAuth2Storage::setRefreshToken() 155 | */ 156 | public function setRefreshToken($refreshToken, $clientId, $userId, $expires, $scope = null) 157 | { 158 | return $this->setToken($refreshToken, $clientId, $userId, $expires, $scope, true); 159 | } 160 | 161 | /** 162 | * @see IOAuth2Storage::unsetRefreshToken() 163 | */ 164 | public function unsetRefreshToken($refreshToken) 165 | { 166 | try { 167 | $sql = 'DELETE FROM '.self::TABLE_TOKENS.' WHERE refresh_token = :refresh_token'; 168 | $stmt = $this->db->prepare($sql); 169 | $stmt->bindParam(':refresh_token', $refreshToken, PDO::PARAM_STR); 170 | $stmt->execute(); 171 | } catch (PDOException $e) { 172 | $this->handleException($e); 173 | } 174 | } 175 | 176 | /** 177 | * Implements IOAuth2Storage::getAuthCode(). 178 | */ 179 | public function getAuthCode($code) 180 | { 181 | try { 182 | $sql = 'SELECT code, client_id, user_id, redirect_uri, expires, scope FROM '.self::TABLE_CODES.' auth_codes WHERE code = :code'; 183 | $stmt = $this->db->prepare($sql); 184 | $stmt->bindParam(':code', $code, PDO::PARAM_STR); 185 | $stmt->execute(); 186 | 187 | $result = $stmt->fetch(PDO::FETCH_ASSOC); 188 | 189 | return $result !== false ? $result : null; 190 | } catch (PDOException $e) { 191 | $this->handleException($e); 192 | } 193 | } 194 | 195 | /** 196 | * Implements IOAuth2Storage::setAuthCode(). 197 | */ 198 | public function setAuthCode($code, $clientId, $userId, $redirectUri, $expires, $scope = null) 199 | { 200 | try { 201 | $sql = 'INSERT INTO '.self::TABLE_CODES.' (code, client_id, user_id, redirect_uri, expires, scope) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope)'; 202 | $stmt = $this->db->prepare($sql); 203 | $stmt->bindParam(':code', $code, PDO::PARAM_STR); 204 | $stmt->bindParam(':client_id', $clientId, PDO::PARAM_STR); 205 | $stmt->bindParam(':user_id', $userId, PDO::PARAM_STR); 206 | $stmt->bindParam(':redirect_uri', $redirectUri, PDO::PARAM_STR); 207 | $stmt->bindParam(':expires', $expires, PDO::PARAM_INT); 208 | $stmt->bindParam(':scope', $scope, PDO::PARAM_STR); 209 | 210 | $stmt->execute(); 211 | } catch (PDOException $e) { 212 | $this->handleException($e); 213 | } 214 | } 215 | 216 | /** 217 | * @see IOAuth2Storage::checkRestrictedGrantType() 218 | */ 219 | public function checkRestrictedGrantType(IOAuth2Client $client, $grantType) 220 | { 221 | return true; // Not implemented 222 | } 223 | 224 | /** 225 | * Creates a refresh or access token 226 | * 227 | * @param string $token Access or refresh token id 228 | * @param string $clientId 229 | * @param mixed $userId 230 | * @param int $expires 231 | * @param string $scope 232 | * @param bool $isRefresh 233 | */ 234 | protected function setToken($token, $clientId, $userId, $expires, $scope, $isRefresh = true) 235 | { 236 | try { 237 | $tableName = $isRefresh ? self::TABLE_REFRESH : self::TABLE_TOKENS; 238 | 239 | $sql = "INSERT INTO $tableName (oauth_token, client_id, user_id, expires, scope) VALUES (:token, :client_id, :user_id, :expires, :scope)"; 240 | $stmt = $this->db->prepare($sql); 241 | $stmt->bindParam(':token', $token, PDO::PARAM_STR); 242 | $stmt->bindParam(':client_id', $clientId, PDO::PARAM_STR); 243 | $stmt->bindParam(':user_id', $userId, PDO::PARAM_STR); 244 | $stmt->bindParam(':expires', $expires, PDO::PARAM_INT); 245 | $stmt->bindParam(':scope', $scope, PDO::PARAM_STR); 246 | 247 | $stmt->execute(); 248 | } catch (PDOException $e) { 249 | $this->handleException($e); 250 | } 251 | } 252 | 253 | /** 254 | * Retrieves an access or refresh token. 255 | * 256 | * @param string $token 257 | * @param bool $isRefresh 258 | * 259 | * @return array|null 260 | */ 261 | protected function getToken($token, $isRefresh = true) 262 | { 263 | try { 264 | $tableName = $isRefresh ? self::TABLE_REFRESH : self::TABLE_TOKENS; 265 | $tokenName = $isRefresh ? 'refresh_token' : 'oauth_token'; 266 | 267 | $sql = "SELECT oauth_token AS $tokenName, client_id, expires, scope, user_id FROM $tableName WHERE oauth_token = :token"; 268 | $stmt = $this->db->prepare($sql); 269 | $stmt->bindParam(':token', $token, PDO::PARAM_STR); 270 | $stmt->execute(); 271 | 272 | $result = $stmt->fetch(PDO::FETCH_ASSOC); 273 | 274 | return $result !== false ? $result : null; 275 | } catch (PDOException $e) { 276 | $this->handleException($e); 277 | } 278 | } 279 | 280 | /** 281 | * Change/override this to whatever your own password hashing method is. 282 | * 283 | * @param string $clientSecret 284 | * @param string $clientId 285 | * 286 | * @return string 287 | */ 288 | protected function hash($clientSecret, $clientId) 289 | { 290 | return hash('sha1', $clientId.$clientSecret.$this->salt); 291 | } 292 | 293 | /** 294 | * Checks the password. 295 | * Override this if you need to 296 | * 297 | * @param string $try 298 | * @param string $clientSecret 299 | * @param string $clientId 300 | * 301 | * @return bool 302 | */ 303 | protected function checkPassword($try, $clientSecret, $clientId) 304 | { 305 | return $clientSecret == $this->hash($try, $clientId); 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /server/examples/pdo/lib/bootstrap.php: -------------------------------------------------------------------------------- 1 | registerNamespaces(array( 11 | 'OAuth2' => $root.'lib', 12 | 'Symfony' => $root.'vendor', 13 | )); 14 | 15 | $loader->register(); 16 | 17 | require_once __DIR__ . '/OAuth2StoragePdo.php'; 18 | 19 | function newPDO() 20 | { 21 | $settings = parse_ini_file(__DIR__ . '/settings.ini'); 22 | $pdo = new PDO($settings['dsn'], $settings['user'], $settings['password']); 23 | $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 24 | 25 | return $pdo; 26 | } 27 | -------------------------------------------------------------------------------- /server/examples/pdo/mysql_create_tables.sql: -------------------------------------------------------------------------------- 1 | SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; 2 | 3 | DROP TABLE IF EXISTS auth_codes; 4 | DROP TABLE IF EXISTS clients; 5 | DROP TABLE IF EXISTS access_tokens; 6 | DROP TABLE IF EXISTS refresh_tokens; 7 | 8 | CREATE TABLE `auth_codes` ( 9 | `code` varchar(40) NOT NULL, 10 | `client_id` varchar(20) NOT NULL, 11 | `user_id` varchar(20) NOT NULL, 12 | `redirect_uri` varchar(200) NOT NULL, 13 | `expires` int(11) NOT NULL, 14 | `scope` varchar(250) DEFAULT NULL, 15 | PRIMARY KEY (`code`) 16 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 17 | 18 | CREATE TABLE `clients` ( 19 | `client_id` varchar(20) NOT NULL, 20 | `client_secret` varchar(200) NOT NULL, 21 | `redirect_uri` varchar(200) NOT NULL, 22 | PRIMARY KEY (`client_id`) 23 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 24 | 25 | CREATE TABLE `access_tokens` ( 26 | `oauth_token` varchar(40) NOT NULL, 27 | `client_id` varchar(20) NOT NULL, 28 | `user_id` int(11) UNSIGNED NOT NULL, 29 | `expires` int(11) NOT NULL, 30 | `scope` varchar(200) DEFAULT NULL, 31 | PRIMARY KEY (`oauth_token`) 32 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 33 | 34 | CREATE TABLE `refresh_tokens` ( 35 | `oauth_token` varchar(40) NOT NULL, 36 | `refresh_token` varchar(40) NOT NULL, 37 | `client_id` varchar(20) NOT NULL, 38 | `user_id` int(11) UNSIGNED NOT NULL, 39 | `expires` int(11) NOT NULL, 40 | `scope` varchar(200) DEFAULT NULL, 41 | PRIMARY KEY (`oauth_token`) 42 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 43 | -------------------------------------------------------------------------------- /server/examples/pdo/protected_resource.php: -------------------------------------------------------------------------------- 1 | getBearerToken(); 21 | $oauth->verifyAccessToken($token); 22 | } catch (OAuth2ServerException $oauthError) { 23 | $oauthError->sendHttpResponse(); 24 | } 25 | 26 | // With a particular scope, you'd do: 27 | // $oauth->verifyAccessToken("scope_name"); 28 | 29 | ?> 30 | 31 | 32 | 33 | Hello! 34 | 35 | 36 |

This is a secret.

37 | 38 | 39 | -------------------------------------------------------------------------------- /server/examples/pdo/token.php: -------------------------------------------------------------------------------- 1 | grantAccessToken(); 20 | $response->send(); 21 | } catch (OAuth2ServerException $oauthError) { 22 | $oauthError->getHttpResponse()->send(); 23 | } 24 | -------------------------------------------------------------------------------- /tests/ExtraHeadersTest.php: -------------------------------------------------------------------------------- 1 | array( 17 | "Access-Control-Allow-Origin" => "http://www.foo.com", 18 | "X-Extra-Header-1" => "Foo-Bar", 19 | ), 20 | ); 21 | $stub = new OAuth2GrantUserStub(); 22 | $stub->addClient(new OAuth2Client('cid', 'cpass')); 23 | $stub->addUser('foo', 'bar'); 24 | $stub->setAllowedGrantTypes(array('authorization_code', 'password')); 25 | 26 | $oauth2 = new OAuth2($stub, $config); 27 | 28 | $response = $oauth2->grantAccessToken(new Request(array( 29 | 'grant_type' => 'password', 30 | 'client_id' => 'cid', 31 | 'client_secret' => 'cpass', 32 | 'username' => 'foo', 33 | 'password' => 'bar', 34 | ))); 35 | $this->assertSame("http://www.foo.com", $response->headers->get("Access-Control-Allow-Origin")); 36 | $this->assertSame("Foo-Bar", $response->headers->get("X-Extra-Header-1")); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Fixtures/OAuth2GrantCodeStub.php: -------------------------------------------------------------------------------- 1 | authCodes[$code])) { 17 | return $this->authCodes[$code]; 18 | } 19 | } 20 | 21 | public function getAuthCodes() 22 | { 23 | return $this->authCodes; 24 | } 25 | 26 | public function getLastAuthCode() 27 | { 28 | return end($this->authCodes); 29 | } 30 | 31 | public function createAuthCode($code, IOAuth2Client $client, $data, $redirectUri, $expires, $scope = null) 32 | { 33 | $token = new OAuth2AuthCode($client->getPublicId(), $code, $expires, $scope, $data, $redirectUri); 34 | $this->authCodes[$code] = $token; 35 | } 36 | 37 | public function markAuthCodeAsUsed($code) 38 | { 39 | if (isset($this->authCodes[$code])) { 40 | unset($this->authCodes[$code]); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Fixtures/OAuth2GrantExtensionJwtBearer.php: -------------------------------------------------------------------------------- 1 | sub !== $decodedJwtStruct['sub']) { 36 | return false; 37 | } 38 | 39 | return array( 40 | 'data' => $decodedJwtStruct, 41 | ); 42 | } 43 | 44 | public function setExpectedSubject($sub) 45 | { 46 | $this->sub = $sub; 47 | } 48 | 49 | /** 50 | * Let's pretend a JWT is endoded and signed by wrapping it in -ENCODED-JWT- 51 | * 52 | * In real life, we would verify the JWT is valid, and get the subject from it after decoding 53 | * 54 | * @param string An encoded JWT string 55 | * @return array The decoded JWT struct 56 | */ 57 | public static function decodeJwt($encodedJwt) 58 | { 59 | $decodedJwt = str_replace('-ENCODED-JWT-', '', $encodedJwt); 60 | return json_decode($decodedJwt, true); 61 | } 62 | 63 | /** 64 | * Let's pretend a JWT is endoded and signed by wrapping it in -ENCODED-JWT- 65 | * 66 | * In real life, we would verify the JWT is valid, and get the subject from it after decoding 67 | * 68 | * @param array A struct to encode as a JWT 69 | * @return string The encoded JWT 70 | */ 71 | public static function encodeJwt($decodedStruct) 72 | { 73 | $decodedJwt = json_encode($decodedStruct); 74 | $wrapper = '-ENCODED-JWT-'; 75 | $encodedJwt = sprintf( 76 | '%s%s%s', 77 | $wrapper, 78 | $decodedJwt, 79 | $wrapper 80 | ); 81 | return $encodedJwt; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Fixtures/OAuth2GrantExtensionLifetimeStub.php: -------------------------------------------------------------------------------- 1 | getFacebookIdFromFacebookAccessToken($fbAccessToken); 27 | 28 | if (!isset($this->facebookIds[$fbId])) { 29 | return false; 30 | } 31 | 32 | return array( 33 | 'data' => array('user_id' => $fbId), 34 | 'access_token_lifetime' => 86400, 35 | 'issue_refresh_token' => false 36 | ); 37 | } 38 | 39 | public function addFacebookId($id) 40 | { 41 | $this->facebookIds[$id] = $id; 42 | } 43 | 44 | /** 45 | * Let's assume a fb access token looks like "something_fbid" 46 | * 47 | * In real life, we would verify the access token is valid, and get the facebook_id of the 48 | * user via GET http://graph.facebook.com/me 49 | */ 50 | protected function getFacebookIdFromFacebookAccessToken($fbAccessToken) 51 | { 52 | return substr($fbAccessToken, strpos($fbAccessToken, '_') + 1); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Fixtures/OAuth2GrantExtensionStub.php: -------------------------------------------------------------------------------- 1 | getFacebookIdFromFacebookAccessToken($fbAccessToken); 28 | 29 | return isset($this->facebookIds[$fbId]); 30 | } 31 | 32 | public function addFacebookId($id) 33 | { 34 | $this->facebookIds[$id] = $id; 35 | } 36 | 37 | /** 38 | * Let's assume a fb access token looks like "something_fbid" 39 | * 40 | * In real life, we would verify the access token is valid, and get the facebook_id of the 41 | * user via GET http://graph.facebook.com/me 42 | */ 43 | protected function getFacebookIdFromFacebookAccessToken($fbAccessToken) 44 | { 45 | return substr($fbAccessToken, strpos($fbAccessToken, '_') + 1); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Fixtures/OAuth2GrantUserStub.php: -------------------------------------------------------------------------------- 1 | users[$username] = array( 16 | 'password' => $password, 17 | 'scope' => $scope, 18 | 'data' => $data, 19 | ); 20 | } 21 | 22 | public function checkUserCredentials(IOAuth2Client $client, $username, $password) 23 | { 24 | if (!isset($this->users[$username])) { 25 | return false; 26 | } 27 | if ($this->users[$username]['password'] === $password) { 28 | return array( 29 | 'scope' => $this->users[$username]['scope'], 30 | 'data' => $this->users[$username]['data'], 31 | ); 32 | } 33 | 34 | return false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Fixtures/OAuth2ImplicitStub.php: -------------------------------------------------------------------------------- 1 | clients[$client->getPublicId()] = $client; 26 | } 27 | 28 | public function getClient($client_id) 29 | { 30 | if (isset($this->clients[$client_id])) { 31 | return $this->clients[$client_id]; 32 | } 33 | } 34 | 35 | public function getClients() 36 | { 37 | return $this->clients; 38 | } 39 | 40 | public function checkClientCredentials(IOAuth2Client $client, $clientSecret = null) 41 | { 42 | return $client->checkSecret($clientSecret); 43 | } 44 | 45 | public function checkClientCredentialsGrant(IOAuth2Client $client, $clientSecret) 46 | { 47 | return $this->checkClientCredentials($client, $clientSecret); 48 | } 49 | 50 | public function createAccessToken($oauthToken, IOAuth2Client $client, $data, $expires, $scope = null) 51 | { 52 | $token = new OAuth2AccessToken($client->getPublicId(), $oauthToken, $expires, $scope, $data); 53 | 54 | $this->accessTokens[$oauthToken] = $token; 55 | } 56 | 57 | public function getAccessToken($oauth_token) 58 | { 59 | if (isset($this->accessTokens[$oauth_token])) { 60 | return $this->accessTokens[$oauth_token]; 61 | } 62 | } 63 | 64 | public function getAccessTokens() 65 | { 66 | return $this->accessTokens; 67 | } 68 | 69 | public function getLastAccessToken() 70 | { 71 | return end($this->accessTokens); 72 | } 73 | 74 | public function setAllowedGrantTypes(array $types) 75 | { 76 | $this->allowedGrantTypes = $types; 77 | } 78 | 79 | public function checkRestrictedGrantType(IOAuth2Client $client, $grantType) 80 | { 81 | return in_array($grantType, $this->allowedGrantTypes); 82 | } 83 | 84 | public function getRefreshToken($refreshToken) 85 | { 86 | if (isset($this->refreshTokens[$refreshToken])) { 87 | return $this->refreshTokens[$refreshToken]; 88 | } 89 | } 90 | 91 | public function createRefreshToken($refreshToken, IOAuth2Client $client, $data, $expires, $scope = null) 92 | { 93 | $token = new OAuth2RefreshToken($client->getPublicId(), $refreshToken, $expires, $scope, $data); 94 | 95 | $this->refreshToken[$refreshToken] = $token; 96 | } 97 | 98 | public function unsetRefreshToken($refreshToken) 99 | { 100 | if (isset($this->refreshTokens[$refreshToken])) { 101 | unset($this->refreshTokens[$refreshToken]); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Model/OAuth2TokenTest.php: -------------------------------------------------------------------------------- 1 | assertSame('foo', $token->getClientId()); 17 | $this->assertSame('bar', $token->getToken()); 18 | $this->assertFalse($token->hasExpired()); 19 | $this->assertLessThan(43, $token->getExpiresIn()); 20 | $this->assertGreaterThan(40, $token->getExpiresIn()); 21 | $this->assertSame('foo bar baz', $token->getScope()); 22 | $this->assertSame($data, $token->getData()); 23 | } 24 | 25 | /** @dataProvider getTestExpiresData */ 26 | public function testExpires($offset, $expired) 27 | { 28 | $token = new OAuth2Token('foo', 'bar', time() + $offset); 29 | 30 | $this->assertSame($expired, $token->hasExpired()); 31 | } 32 | 33 | public function getTestExpiresData() 34 | { 35 | return array( 36 | array(-10, true), 37 | array(-5, true), 38 | array(+5, false), 39 | array(+10, false), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/OAuth2ImplicitGrantTypeTest.php: -------------------------------------------------------------------------------- 1 | grantAccessToken() with implicit 16 | * 17 | */ 18 | public function testGrantAccessTokenWithGrantImplicit() 19 | { 20 | $stub = new OAuth2ImplicitStub(); 21 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); 22 | $oauth2 = new OAuth2($stub); 23 | 24 | $data = new \stdClass(); 25 | 26 | $response = $oauth2->finishClientAuthorization(true, $data, new Request(array( 27 | 'client_id' => 'blah', 28 | 'redirect_uri' => 'http://www.example.com/?foo=bar', 29 | 'response_type' => 'token', 30 | 'state' => '42', 31 | ))); 32 | 33 | $this->assertRegExp('/^http:\/\/www.example.com\/\?foo=bar#state=42&access_token=[^"]+&expires_in=3600&token_type=bearer$/', $response->headers->get('Location')); 34 | } 35 | 36 | /** 37 | * Tests OAuth2->grantAccessToken() with implicit 38 | * 39 | */ 40 | public function testRejectedAccessTokenWithGrantImplicit() 41 | { 42 | //$this->fixture->grantAccessToken(/* parameters */); 43 | 44 | $stub = new OAuth2ImplicitStub(); 45 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); 46 | $oauth2 = new OAuth2($stub); 47 | 48 | $data = new \stdClass(); 49 | 50 | try { 51 | $oauth2->finishClientAuthorization(false, $data, new Request(array( 52 | 'client_id' => 'blah', 53 | 'redirect_uri' => 'http://www.example.com/?foo=bar', 54 | 'state' => '42', 55 | 'response_type' => 'token', 56 | ))); 57 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 58 | } catch (OAuth2ServerException $e) { 59 | $this->assertSame('access_denied', $e->getMessage()); 60 | $this->assertSame('The user denied access to your application', $e->getDescription()); 61 | $this->assertSame(array( 62 | 'Location' => 'http://www.example.com/?foo=bar#error=access_denied&error_description=The+user+denied+access+to+your+application&state=42', 63 | ), $e->getResponseHeaders()); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/OAuth2OutputTest.php: -------------------------------------------------------------------------------- 1 | grantAccessToken() with successful Auth code grant 20 | * 21 | */ 22 | public function testGrantAccessTokenWithGrantAuthCodeSuccess() 23 | { 24 | $request = new Request( 25 | array('grant_type' => OAuth2::GRANT_TYPE_AUTH_CODE, 'redirect_uri' => 'http://www.example.com/my/subdir', 'client_id' => 'my_little_app', 'client_secret' => 'b', 'code'=> 'foo') 26 | ); 27 | $storedToken = new OAuth2AuthCode('my_little_app', '', time() + 60, null, null, 'http://www.example.com'); 28 | 29 | $mockStorage = $this->createBaseMock('OAuth2\IOAuth2GrantCode'); 30 | $mockStorage->expects($this->any()) 31 | ->method('getAuthCode') 32 | ->will($this->returnValue($storedToken)); 33 | 34 | $this->fixture = new OAuth2($mockStorage); 35 | $response = $this->fixture->grantAccessToken($request); 36 | 37 | // Successful token grant will return a JSON encoded token: 38 | $this->assertRegExp('/{"access_token":".*","expires_in":\d+,"token_type":"bearer"/', $response->getContent()); 39 | } 40 | 41 | /** 42 | * Tests OAuth2->grantAccessToken() with successful Auth code grant, but without redreict_uri in the input 43 | */ 44 | public function testGrantAccessTokenWithGrantAuthCodeSuccessWithoutRedirect() 45 | { 46 | $request = new Request( 47 | array('grant_type' => OAuth2::GRANT_TYPE_AUTH_CODE, 'client_id' => 'my_little_app', 'client_secret' => 'b', 'code'=> 'foo') 48 | ); 49 | $storedToken = new OAuth2AuthCode('my_little_app', '', time() + 60, null, null, 'http://www.example.com'); 50 | 51 | $mockStorage = $this->createBaseMock('OAuth2\IOAuth2GrantCode'); 52 | $mockStorage->expects($this->any()) 53 | ->method('getAuthCode') 54 | ->will($this->returnValue($storedToken)); 55 | 56 | $this->fixture = new OAuth2($mockStorage); 57 | $this->fixture->setVariable(OAuth2::CONFIG_ENFORCE_INPUT_REDIRECT, false); 58 | $response = $this->fixture->grantAccessToken($request); 59 | 60 | // Successful token grant will return a JSON encoded token: 61 | $this->assertRegExp('/{"access_token":".*","expires_in":\d+,"token_type":"bearer"/', $response->getContent()); 62 | } 63 | 64 | // Utility methods 65 | 66 | /** 67 | * 68 | * @param string $interfaceName 69 | */ 70 | protected function createBaseMock($interfaceName) 71 | { 72 | $client = new OAuth2Client('my_little_app'); 73 | 74 | $mockStorage = $this->createMock($interfaceName); 75 | $mockStorage->expects($this->any()) 76 | ->method('getClient') 77 | ->will($this->returnCallback(function ($id) use ($client) { 78 | if ('my_little_app' === $id) { 79 | return $client; 80 | } 81 | })); 82 | $mockStorage->expects($this->any()) 83 | ->method('checkClientCredentials') 84 | ->will($this->returnValue(true)); // Always return true for any combination of user/pass 85 | $mockStorage->expects($this->any()) 86 | ->method('checkRestrictedGrantType') 87 | ->will($this->returnValue(true)); // Always return true for any combination of user/pass 88 | 89 | return $mockStorage; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/OAuth2Test.php: -------------------------------------------------------------------------------- 1 | verifyAccessToken() with a missing token 32 | */ 33 | public function testVerifyAccessTokenWithNoParam() 34 | { 35 | $mockStorage = $this->createMock('OAuth2\IOAuth2Storage'); 36 | $this->fixture = new OAuth2($mockStorage); 37 | 38 | $scope = null; 39 | $this->expectException('OAuth2\OAuth2AuthenticateException'); 40 | $this->fixture->verifyAccessToken('', $scope); 41 | } 42 | 43 | /** 44 | * Tests OAuth2->verifyAccessToken() with a invalid token 45 | */ 46 | public function testVerifyAccessTokenInvalidToken() 47 | { 48 | // Set up the mock storage to say this token does not exist 49 | $mockStorage = $this->createMock('OAuth2\IOAuth2Storage'); 50 | $mockStorage->expects($this->once()) 51 | ->method('getAccessToken') 52 | ->will($this->returnValue(false)); 53 | 54 | $this->fixture = new OAuth2($mockStorage); 55 | 56 | $scope = null; 57 | $this->expectException('OAuth2\OAuth2AuthenticateException'); 58 | $this->fixture->verifyAccessToken($this->tokenId, $scope); 59 | } 60 | 61 | /** 62 | * Tests OAuth2->verifyAccessToken() with a malformed token 63 | * 64 | * @dataProvider generateMalformedTokens 65 | */ 66 | public function testVerifyAccessTokenMalformedToken(IOAuth2AccessToken $token) 67 | { 68 | // Set up the mock storage to say this token does not exist 69 | $mockStorage = $this->createMock('OAuth2\IOAuth2Storage'); 70 | $mockStorage->expects($this->once()) 71 | ->method('getAccessToken') 72 | ->will($this->returnValue($token)); 73 | 74 | $this->fixture = new OAuth2($mockStorage); 75 | 76 | $scope = null; 77 | $this->expectException('OAuth2\OAuth2AuthenticateException'); 78 | $this->fixture->verifyAccessToken($this->tokenId, $scope); 79 | } 80 | 81 | /** 82 | * Tests OAuth2->verifyAccessToken() with different expiry dates 83 | * 84 | * @dataProvider generateExpiryTokens 85 | */ 86 | public function testVerifyAccessTokenCheckExpiry(IOAuth2AccessToken $token, $expectedToPass) 87 | { 88 | // Set up the mock storage to say this token does not exist 89 | $mockStorage = $this->createMock('OAuth2\IOAuth2Storage'); 90 | $mockStorage->expects($this->once()) 91 | ->method('getAccessToken') 92 | ->will($this->returnValue($token)); 93 | 94 | $this->fixture = new OAuth2($mockStorage); 95 | 96 | $scope = null; 97 | 98 | // When valid, we just want any sort of token 99 | if ($expectedToPass) { 100 | $actual = $this->fixture->verifyAccessToken($this->tokenId, $scope); 101 | $this->assertNotEmpty($actual, "verifyAccessToken() was expected to PASS, but it failed"); 102 | $this->assertInstanceOf('OAuth2\Model\IOAuth2AccessToken', $actual); 103 | } else { 104 | $this->expectException('OAuth2\OAuth2AuthenticateException'); 105 | $this->fixture->verifyAccessToken($this->tokenId, $scope); 106 | } 107 | } 108 | 109 | /** 110 | * Tests OAuth2->verifyAccessToken() with different scopes 111 | * 112 | * @dataProvider generateScopes 113 | */ 114 | public function testVerifyAccessTokenCheckScope($scopeRequired, IOAuth2AccessToken $token, $expectedToPass) 115 | { 116 | // Set up the mock storage to say this token does not exist 117 | $mockStorage = $this->createMock('OAuth2\IOAuth2Storage'); 118 | $mockStorage->expects($this->once()) 119 | ->method('getAccessToken') 120 | ->will($this->returnValue($token)); 121 | 122 | $this->fixture = new OAuth2($mockStorage); 123 | 124 | // When valid, we just want any sort of token 125 | if ($expectedToPass) { 126 | $actual = $this->fixture->verifyAccessToken($this->tokenId, $scopeRequired); 127 | $this->assertNotEmpty($actual, "verifyAccessToken() was expected to PASS, but it failed"); 128 | $this->assertInstanceOf('OAuth2\Model\IOAuth2AccessToken', $actual); 129 | } else { 130 | $this->expectException('OAuth2\OAuth2AuthenticateException'); 131 | $this->fixture->verifyAccessToken($this->tokenId, $scopeRequired); 132 | } 133 | } 134 | 135 | /** 136 | * Tests OAuth2->grantAccessToken() for missing data 137 | * 138 | * @dataProvider generateEmptyDataForGrant 139 | */ 140 | public function testGrantAccessTokenMissingData($request) 141 | { 142 | $mockStorage = $this->createMock('OAuth2\IOAuth2Storage'); 143 | $this->fixture = new OAuth2($mockStorage); 144 | 145 | $this->expectException('OAuth2\OAuth2ServerException'); 146 | $this->fixture->grantAccessToken($request); 147 | } 148 | 149 | /** 150 | * Tests OAuth2->grantAccessToken() 151 | * 152 | * Tests the different ways client credentials can be provided. 153 | */ 154 | public function testGrantAccessTokenCheckClientCredentials() 155 | { 156 | $mockStorage = $this->createMock('OAuth2\IOAuth2Storage'); 157 | $mockStorage->expects($this->any()) 158 | ->method('getClient') 159 | ->will($this->returnValue(new OAuth2Client('dev-abc'))); 160 | $mockStorage->expects($this->any()) 161 | ->method('checkClientCredentials') 162 | ->will($this->returnValue(true)); // Always return true for any combination of user/pass 163 | $this->fixture = new OAuth2($mockStorage); 164 | 165 | $inputData = array('grant_type' => OAuth2::GRANT_TYPE_AUTH_CODE); 166 | $request = $this->createRequest($inputData); 167 | 168 | // First, confirm that an non-client related error is thrown: 169 | try { 170 | $this->fixture->grantAccessToken($request); 171 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 172 | } catch ( OAuth2ServerException $e ) { 173 | $this->assertEquals(OAuth2::ERROR_INVALID_CLIENT, $e->getMessage()); 174 | } 175 | 176 | // Confirm Auth header 177 | $authHeaders = array('PHP_AUTH_USER' => 'dev-abc', 'PHP_AUTH_PW' => 'pass'); 178 | $inputData = array('grant_type' => OAuth2::GRANT_TYPE_AUTH_CODE, 'client_id' => 'dev-abc'); // When using auth, client_id must match 179 | $request = $this->createRequest($inputData, $authHeaders); 180 | try { 181 | $this->fixture->grantAccessToken($request); 182 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 183 | } catch ( OAuth2ServerException $e ) { 184 | $this->assertNotEquals(OAuth2::ERROR_INVALID_CLIENT, $e->getMessage()); 185 | } 186 | 187 | // Confirm GET/POST 188 | $authHeaders = array(); 189 | $inputData = array('grant_type' => OAuth2::GRANT_TYPE_AUTH_CODE, 'client_id' => 'dev-abc', 'client_secret' => 'foo'); // When using auth, client_id must match 190 | $request = $this->createRequest($inputData, $authHeaders); 191 | try { 192 | $this->fixture->grantAccessToken($request); 193 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 194 | } catch ( OAuth2ServerException $e ) { 195 | $this->assertNotEquals(OAuth2::ERROR_INVALID_CLIENT, $e->getMessage()); 196 | } 197 | } 198 | 199 | /** 200 | * Tests OAuth2->grantAccessToken() with successful Client Credentials grant 201 | * 202 | */ 203 | public function testGrantAccessTokenWithClientCredentialsSuccess() 204 | { 205 | $request = new Request( 206 | array('grant_type' => OAuth2::GRANT_TYPE_CLIENT_CREDENTIALS, 'client_id' => 'my_little_app', 'client_secret' => 'b') 207 | ); 208 | 209 | $storage = new OAuth2StorageStub; 210 | $storage->addClient(new OAuth2Client('my_little_app', 'b')); 211 | $storage->setAllowedGrantTypes(array(OAuth2::GRANT_TYPE_CLIENT_CREDENTIALS)); 212 | 213 | $this->fixture = new OAuth2($storage); 214 | 215 | $response = $this->fixture->grantAccessToken($request); 216 | 217 | // Successful token grant will return a JSON encoded token WITHOUT a refresh token: 218 | $this->assertRegExp('/^{"access_token":"[^"]+","expires_in":[^"]+,"token_type":"bearer","scope":null}$/', $response->getContent()); 219 | } 220 | 221 | /** 222 | * Tests OAuth2->grantAccessToken() with Auth code grant 223 | * 224 | */ 225 | public function testGrantAccessTokenWithGrantAuthCodeMandatoryParams() 226 | { 227 | $mockStorage = $this->createBaseMock('OAuth2\IOAuth2GrantCode'); 228 | $mockStorage->expects($this->any()) 229 | ->method('getClient') 230 | ->will($this->returnValue(new OAuth2Client('dev-abc'))); 231 | $mockStorage->expects($this->any()) 232 | ->method('checkClientCredentials') 233 | ->will($this->returnValue(true)); // Always return true for any combination of user/pass 234 | 235 | $inputData = array('grant_type' => OAuth2::GRANT_TYPE_AUTH_CODE, 'client_id' => 'a', 'client_secret' => 'b'); 236 | 237 | // Ensure redirect URI and auth-code is mandatory 238 | try { 239 | $this->fixture = new OAuth2($mockStorage); 240 | $this->fixture->setVariable(OAuth2::CONFIG_ENFORCE_INPUT_REDIRECT, true); // Only required when this is set 241 | $request = $this->createRequest($inputData + array('code' => 'foo')); 242 | $this->fixture->grantAccessToken($request); 243 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 244 | } catch ( OAuth2ServerException $e ) { 245 | $this->assertEquals(OAuth2::ERROR_INVALID_REQUEST, $e->getMessage()); 246 | } 247 | try { 248 | $this->fixture = new OAuth2($mockStorage); 249 | $request = $this->createRequest($inputData + array('redirect_uri' => 'foo')); 250 | $this->fixture->grantAccessToken($request); 251 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 252 | } catch ( OAuth2ServerException $e ) { 253 | $this->assertEquals(OAuth2::ERROR_INVALID_REQUEST, $e->getMessage()); 254 | } 255 | } 256 | 257 | /** 258 | * Tests OAuth2->grantAccessToken() with Auth code grant 259 | * 260 | */ 261 | public function testGrantAccessTokenWithGrantAuthCodeNoToken() 262 | { 263 | $mockStorage = $this->createBaseMock('OAuth2\IOAuth2GrantCode'); 264 | $mockStorage->expects($this->any()) 265 | ->method('getClient') 266 | ->will($this->returnValue(new OAuth2Client('dev-abc'))); 267 | $mockStorage->expects($this->any()) 268 | ->method('checkClientCredentials') 269 | ->will($this->returnValue(true)); // Always return true for any combination of user/pass 270 | 271 | $inputData = array('grant_type' => OAuth2::GRANT_TYPE_AUTH_CODE, 'client_id' => 'a', 'client_secret' => 'b', 'redirect_uri' => 'foo', 'code'=> 'foo'); 272 | 273 | // Ensure missing auth code raises an error 274 | try { 275 | $this->fixture = new OAuth2($mockStorage); 276 | $request = $this->createRequest($inputData); 277 | $this->fixture->grantAccessToken($request); 278 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 279 | } catch ( OAuth2ServerException $e ) { 280 | $this->assertEquals(OAuth2::ERROR_INVALID_GRANT, $e->getMessage()); 281 | } 282 | } 283 | 284 | /** 285 | * Tests OAuth2->grantAccessToken() with checks the redirect URI 286 | * 287 | */ 288 | public function testGrantAccessTokenWithGrantAuthCodeRedirectChecked() 289 | { 290 | $inputData = array('redirect_uri' => 'http://www.crossdomain.com/my/subdir', 'grant_type' => OAuth2::GRANT_TYPE_AUTH_CODE, 'client_id' => 'my_little_app', 'client_secret' => 'b', 'code'=> 'foo'); 291 | $storedToken = new OAuth2AuthCode('my_little_app', '', time() + 60, null, null, 'http://www.example.com'); 292 | 293 | $mockStorage = $this->createBaseMock('Oauth2\IOAuth2GrantCode'); 294 | $mockStorage->expects($this->any()) 295 | ->method('getClient') 296 | ->will($this->returnValue(new OAuth2Client('my_little_app'))); 297 | $mockStorage->expects($this->any()) 298 | ->method('checkClientCredentials') 299 | ->will($this->returnValue(true)); // Always return true for any combination of user/pass 300 | $mockStorage->expects($this->any()) 301 | ->method('getAuthCode') 302 | ->will($this->returnValue($storedToken)); 303 | 304 | // Ensure that the redirect_uri is checked 305 | try { 306 | $this->fixture = new OAuth2($mockStorage); 307 | $request = $this->createRequest($inputData); 308 | $this->fixture->grantAccessToken($request); 309 | 310 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 311 | } catch ( OAuth2ServerException $e ) { 312 | $this->assertEquals(OAuth2::ERROR_REDIRECT_URI_MISMATCH, $e->getMessage()); 313 | } 314 | } 315 | 316 | /** 317 | * Tests OAuth2->grantAccessToken() with checks the client ID is matched 318 | * 319 | */ 320 | public function testGrantAccessTokenWithGrantAuthCodeClientIdChecked() 321 | { 322 | $inputData = array('client_id' => 'another_app', 'grant_type' => OAuth2::GRANT_TYPE_AUTH_CODE, 'redirect_uri' => 'http://www.example.com/my/subdir', 'client_secret' => 'b', 'code'=> 'foo'); 323 | $storedToken = new OAuth2AuthCode('my_little_app', '', time() + 60, null, null, 'http://www.example.com'); 324 | 325 | $mockStorage = $this->createBaseMock('OAuth2\IOAuth2GrantCode'); 326 | $mockStorage->expects($this->any()) 327 | ->method('getClient') 328 | ->will($this->returnValue(new OAuth2Client('x'))); 329 | $mockStorage->expects($this->any()) 330 | ->method('checkClientCredentials') 331 | ->will($this->returnValue(true)); // Always return true for any combination of user/pass 332 | $mockStorage->expects($this->any()) 333 | ->method('getAuthCode') 334 | ->will($this->returnValue($storedToken)); 335 | 336 | // Ensure the client ID is checked 337 | try { 338 | $this->fixture = new OAuth2($mockStorage); 339 | $request = $this->createRequest($inputData); 340 | $this->fixture->grantAccessToken($request); 341 | 342 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 343 | } catch ( OAuth2ServerException $e ) { 344 | $this->assertEquals(OAuth2::ERROR_INVALID_GRANT, $e->getMessage()); 345 | } 346 | } 347 | 348 | /** 349 | * Tests OAuth2->grantAccessToken() with same Auth code grant 350 | * 351 | */ 352 | public function testGrantAccessTokenWithSameGrantAuthCode() 353 | { 354 | $stub = new OAuth2GrantCodeStub; 355 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); 356 | $oauth2 = new OAuth2($stub); 357 | 358 | $data = new \stdClass; 359 | 360 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 361 | 'client_id' => 'blah', 362 | 'redirect_uri' => 'http://www.example.com/?foo=bar', 363 | 'response_type' => 'code', 364 | 'state' => '42', 365 | ))); 366 | 367 | $code = $stub->getLastAuthCode(); 368 | 369 | $inputData = array('grant_type' => OAuth2::GRANT_TYPE_AUTH_CODE, 'client_id' => 'blah', 'client_secret' => 'foo', 'redirect_uri' => 'http://www.example.com/?foo=bars', 'code'=> $code->getToken()); 370 | $request = $this->createRequest($inputData); 371 | 372 | // Grant an access token with the auth code 373 | $oauth2->grantAccessToken($request); 374 | 375 | try { 376 | // Grant an access token with the same auth code. 377 | $oauth2->grantAccessToken($request); 378 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 379 | } catch ( OAuth2ServerException $e ) { 380 | $this->assertEquals(OAuth2::ERROR_INVALID_GRANT, $e->getMessage()); 381 | } 382 | } 383 | 384 | /** 385 | * Tests OAuth2->grantAccessToken() with implicit 386 | * 387 | * @doesNotPerformAssertions 388 | */ 389 | public function testGrantAccessTokenWithGrantImplicit() 390 | { 391 | $this->markTestIncomplete ( "grantAccessToken test not implemented" ); 392 | 393 | $this->fixture->grantAccessToken(/* parameters */); 394 | } 395 | 396 | /** 397 | * Tests OAuth2->grantAccessToken() with user credentials 398 | * 399 | */ 400 | public function testGrantAccessTokenWithGrantUser() 401 | { 402 | $data = new \stdClass; 403 | 404 | $stub = new OAuth2GrantUserStub; 405 | $stub->addClient(new OAuth2Client('cid', 'cpass')); 406 | $stub->addUser('foo', 'bar', null, $data); 407 | $stub->setAllowedGrantTypes(array('authorization_code', 'password')); 408 | 409 | $oauth2 = new OAuth2($stub); 410 | 411 | $response = $oauth2->grantAccessToken(new Request(array( 412 | 'grant_type' => 'password', 413 | 'client_id' => 'cid', 414 | 'client_secret' => 'cpass', 415 | 'username' => 'foo', 416 | 'password' => 'bar', 417 | ))); 418 | 419 | $this->assertSame(array( 420 | 'content-type' => array('application/json'), 421 | 'cache-control' => array('no-store, private'), 422 | 'pragma' => array('no-cache'), 423 | ), array_diff_key( 424 | $response->headers->all(), 425 | array('date' => null) 426 | )); 427 | 428 | $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":null}', $response->getContent()); 429 | 430 | $token = $stub->getLastAccessToken(); 431 | $this->assertSame('cid', $token->getClientId()); 432 | $this->assertSame($data, $token->getData()); 433 | $this->assertNull($token->getScope()); 434 | } 435 | 436 | public function testGrantAccessTokenWithGrantUserWithAddScopeThrowsError() 437 | { 438 | $stub = new OAuth2GrantUserStub; 439 | $stub->addClient(new OAuth2Client('cid', 'cpass')); 440 | $stub->addUser('foo', 'bar'); 441 | $stub->setAllowedGrantTypes(array('authorization_code', 'password')); 442 | 443 | $oauth2 = new OAuth2($stub); 444 | 445 | try { 446 | $oauth2->grantAccessToken(new Request(array( 447 | 'grant_type' => 'password', 448 | 'client_id' => 'cid', 449 | 'client_secret' => 'cpass', 450 | 'username' => 'foo', 451 | 'password' => 'bar', 452 | 'scope' => 'scope1 scope2', 453 | ))); 454 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 455 | } catch (OAuth2ServerException $e) { 456 | $this->assertSame('invalid_scope', $e->getMessage()); 457 | $this->assertSame(array( 458 | 'Content-Type' => 'application/json', 459 | 'Cache-Control' => 'no-store', 460 | 'Pragma' => 'no-cache', 461 | ), $e->getResponseHeaders()); 462 | $this->assertSame('{"error":"invalid_scope","error_description":"An unsupported scope was requested."}', $e->getResponseBody()); 463 | } 464 | } 465 | 466 | public function testGrantAccessTokenWithGrantUserWithScope() 467 | { 468 | $stub = new OAuth2GrantUserStub; 469 | $stub->addClient(new OAuth2Client('cid', 'cpass')); 470 | $stub->addUser('foo', 'bar', 'scope1 scope2'); 471 | $stub->setAllowedGrantTypes(array('authorization_code', 'password')); 472 | 473 | $oauth2 = new OAuth2($stub); 474 | 475 | $response = $oauth2->grantAccessToken(new Request(array( 476 | 'grant_type' => 'password', 477 | 'client_id' => 'cid', 478 | 'client_secret' => 'cpass', 479 | 'username' => 'foo', 480 | 'password' => 'bar', 481 | 'scope' => 'scope1 scope2', 482 | ))); 483 | 484 | $this->assertSame(array( 485 | 'content-type' => array('application/json'), 486 | 'cache-control' => array('no-store, private'), 487 | 'pragma' => array('no-cache'), 488 | ), array_diff_key( 489 | $response->headers->all(), 490 | array('date' => null) 491 | )); 492 | 493 | $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":"scope1 scope2"}', $response->getContent()); 494 | 495 | $token = $stub->getLastAccessToken(); 496 | $this->assertSame('cid', $token->getClientId()); 497 | $this->assertSame('scope1 scope2', $token->getScope()); 498 | } 499 | 500 | public function testGrantAccessTokenWithGrantUserWithReducedScope() 501 | { 502 | $stub = new OAuth2GrantUserStub; 503 | $stub->addClient(new OAuth2Client('cid', 'cpass')); 504 | $stub->addUser('foo', 'bar', 'scope1 scope2'); 505 | $stub->setAllowedGrantTypes(array('authorization_code', 'password')); 506 | 507 | $oauth2 = new OAuth2($stub); 508 | 509 | $response = $oauth2->grantAccessToken(new Request(array( 510 | 'grant_type' => 'password', 511 | 'client_id' => 'cid', 512 | 'client_secret' => 'cpass', 513 | 'username' => 'foo', 514 | 'password' => 'bar', 515 | 'scope' => 'scope1', 516 | ))); 517 | 518 | $this->assertSame(array( 519 | 'content-type' => array('application/json'), 520 | 'cache-control' => array('no-store, private'), 521 | 'pragma' => array('no-cache'), 522 | ), array_diff_key( 523 | $response->headers->all(), 524 | array('date' => null) 525 | )); 526 | 527 | $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":"scope1"}', $response->getContent()); 528 | 529 | $token = $stub->getLastAccessToken(); 530 | $this->assertSame('cid', $token->getClientId()); 531 | $this->assertSame('scope1', $token->getScope()); 532 | } 533 | 534 | public function testGrantAccessTokenWithGrantUserWithNoScope() 535 | { 536 | $stub = new OAuth2GrantUserStub; 537 | $stub->addClient(new OAuth2Client('cid', 'cpass')); 538 | $stub->addUser('foo', 'bar', 'scope1 scope2'); 539 | $stub->setAllowedGrantTypes(array('authorization_code', 'password')); 540 | 541 | $oauth2 = new OAuth2($stub); 542 | 543 | $response = $oauth2->grantAccessToken(new Request(array( 544 | 'grant_type' => 'password', 545 | 'client_id' => 'cid', 546 | 'client_secret' => 'cpass', 547 | 'username' => 'foo', 548 | 'password' => 'bar', 549 | ))); 550 | 551 | $this->assertSame(array( 552 | 'content-type' => array('application/json'), 553 | 'cache-control' => array('no-store, private'), 554 | 'pragma' => array('no-cache'), 555 | ), array_diff_key( 556 | $response->headers->all(), 557 | array('date' => null) 558 | )); 559 | 560 | $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":"scope1 scope2"}', $response->getContent()); 561 | 562 | $token = $stub->getLastAccessToken(); 563 | $this->assertSame('cid', $token->getClientId()); 564 | $this->assertSame('scope1 scope2', $token->getScope()); 565 | } 566 | 567 | public function testGrantAccessTokenWithGrantUserWithNewScopeThrowsError() 568 | { 569 | $stub = new OAuth2GrantUserStub; 570 | $stub->addClient(new OAuth2Client('cid', 'cpass')); 571 | $stub->addUser('foo', 'bar', 'scope1 scope2'); 572 | $stub->setAllowedGrantTypes(array('authorization_code', 'password')); 573 | 574 | $oauth2 = new OAuth2($stub); 575 | 576 | try { 577 | $oauth2->grantAccessToken(new Request(array( 578 | 'grant_type' => 'password', 579 | 'client_id' => 'cid', 580 | 'client_secret' => 'cpass', 581 | 'username' => 'foo', 582 | 'password' => 'bar', 583 | 'scope' => 'scope3', 584 | ))); 585 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 586 | } catch (OAuth2ServerException $e) { 587 | $this->assertSame('invalid_scope', $e->getMessage()); 588 | $this->assertSame('An unsupported scope was requested.', $e->getDescription()); 589 | } 590 | } 591 | 592 | /** 593 | * Tests OAuth2->grantAccessToken() with client credentials 594 | * 595 | * @doesNotPerformAssertions 596 | */ 597 | public function testGrantAccessTokenWithGrantClient() 598 | { 599 | $this->markTestIncomplete ( "grantAccessToken test not implemented" ); 600 | 601 | $this->fixture->grantAccessToken(/* parameters */); 602 | } 603 | 604 | /** 605 | * Tests OAuth2->grantAccessToken() with refresh token 606 | * 607 | * @doesNotPerformAssertions 608 | */ 609 | public function testGrantAccessTokenWithGrantRefresh() 610 | { 611 | $this->markTestIncomplete ( "grantAccessToken test not implemented" ); 612 | 613 | $this->fixture->grantAccessToken(/* parameters */); 614 | } 615 | 616 | /** 617 | * Tests OAuth2->grantAccessToken() with extension 618 | */ 619 | public function testGrantAccessTokenWithGrantExtension() 620 | { 621 | $clientId = 'cid'; 622 | $clientSecret = 'csecret'; 623 | $grantType = 'http://company.com/fb_access_token'; 624 | $fbId = '35'; 625 | $fbAccessToken = 'da4b9237bacccd_35'; 626 | 627 | $stub = new \OAuth2\Tests\Fixtures\OAuth2GrantExtensionStub(); 628 | $stub->addClient(new OAuth2Client($clientId, $clientSecret)); 629 | $stub->setAllowedGrantTypes(array($grantType)); 630 | $stub->addFacebookId($fbId); 631 | $oauth2 = new OAuth2($stub); 632 | 633 | $response = $oauth2->grantAccessToken(new Request(array( 634 | 'grant_type' => $grantType, 635 | 'client_id' => $clientId, 636 | 'client_secret' => $clientSecret, 637 | 'fb_access_token' => $fbAccessToken, 638 | ))); 639 | 640 | $this->assertSame(array( 641 | 'content-type' => array('application/json'), 642 | 'cache-control' => array('no-store, private'), 643 | 'pragma' => array('no-cache'), 644 | ), array_diff_key( 645 | $response->headers->all(), 646 | array('date' => null) 647 | )); 648 | 649 | $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer"}', $response->getContent()); 650 | } 651 | 652 | /** 653 | * Tests OAuth2->grantAccessToken() with extension with limited timeframe 654 | */ 655 | public function testGrantAccessTokenWithGrantExtensionLimitedLifetime() 656 | { 657 | $clientId = 'cid'; 658 | $clientSecret = 'csecret'; 659 | $grantType = 'http://company.com/fb_access_token_time_limited'; 660 | $fbId = '35'; 661 | $fbAccessToken = 'da4b9237bacccd_35'; 662 | 663 | $stub = new \OAuth2\Tests\Fixtures\OAuth2GrantExtensionLifetimeStub(); 664 | $stub->addClient(new OAuth2Client($clientId, $clientSecret)); 665 | $stub->setAllowedGrantTypes(array($grantType)); 666 | $stub->addFacebookId($fbId); 667 | $oauth2 = new OAuth2($stub); 668 | 669 | $response = $oauth2->grantAccessToken(new Request(array( 670 | 'grant_type' => $grantType, 671 | 'client_id' => $clientId, 672 | 'client_secret' => $clientSecret, 673 | 'fb_access_token' => $fbAccessToken, 674 | ))); 675 | 676 | $this->assertSame(array( 677 | 'content-type' => array('application/json'), 678 | 'cache-control' => array('no-store, private'), 679 | 'pragma' => array('no-cache'), 680 | ), array_diff_key( 681 | $response->headers->all(), 682 | array('date' => null) 683 | )); 684 | 685 | $this->assertRegExp('{"access_token":"[^"]+","expires_in":86400,"token_type":"bearer"}', $response->getContent()); 686 | } 687 | 688 | /** 689 | * Tests OAuth2->grantAccessToken() with urn: extension 690 | */ 691 | public function testGrantAccessTokenWithGrantExtensionJwtBearer() 692 | { 693 | $clientId = 'cid'; 694 | $clientSecret = 'csecret'; 695 | $grantType = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; 696 | $subject = 1234; 697 | 698 | $stub = new \OAuth2\Tests\Fixtures\OAuth2GrantExtensionJwtBearer(); 699 | $stub->addClient(new OAuth2Client($clientId, $clientSecret)); 700 | $stub->setAllowedGrantTypes(array($grantType)); 701 | $stub->setExpectedSubject($subject); 702 | $oauth2 = new OAuth2($stub); 703 | 704 | $response = $oauth2->grantAccessToken(new Request(array( 705 | 'grant_type' => $grantType, 706 | 'client_id' => $clientId, 707 | 'client_secret' => $clientSecret, 708 | 'jwt' => \OAuth2\Tests\Fixtures\OAuth2GrantExtensionJwtBearer::encodeJwt(array( 709 | 'sub' => $subject, 710 | )), 711 | ))); 712 | 713 | $this->assertSame(array( 714 | 'content-type' => array('application/json'), 715 | 'cache-control' => array('no-store, private'), 716 | 'pragma' => array('no-cache'), 717 | ), array_diff_key( 718 | $response->headers->all(), 719 | array('date' => null) 720 | )); 721 | 722 | $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":null,"refresh_token":"[^"]+"}', $response->getContent()); 723 | 724 | $token = $stub->getLastAccessToken(); 725 | $this->assertSame('cid', $token->getClientId()); 726 | $data = $token->getData(); 727 | $this->assertSame($subject, $data['sub']); 728 | } 729 | 730 | 731 | /** 732 | * Tests OAuth2->getAuthorizeParams() 733 | * @doesNotPerformAssertions 734 | */ 735 | public function testGetAuthorizeParams() 736 | { 737 | // TODO Auto-generated OAuth2Test->testGetAuthorizeParams() 738 | $this->markTestIncomplete ( "getAuthorizeParams test not implemented" ); 739 | 740 | $this->fixture->getAuthorizeParams(/* parameters */); 741 | 742 | } 743 | 744 | /** 745 | * Tests OAuth2->finishClientAuthorization() 746 | */ 747 | public function testFinishClientAuthorization() 748 | { 749 | $stub = new OAuth2GrantCodeStub; 750 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); 751 | $oauth2 = new OAuth2($stub); 752 | 753 | $data = new \stdClass; 754 | 755 | $response = $oauth2->finishClientAuthorization(true, $data, new Request(array( 756 | 'client_id' => 'blah', 757 | 'redirect_uri' => 'http://www.example.com/?foo=bar', 758 | 'response_type' => 'code', 759 | 'state' => '42', 760 | ))); 761 | 762 | $this->assertSame(302, $response->getStatusCode()); 763 | $this->assertRegExp('#^http://www\.example\.com/\?foo=bar&state=42&code=#', $response->headers->get('location')); 764 | 765 | $code = $stub->getLastAuthCode(); 766 | $this->assertSame('blah', $code->getClientId()); 767 | $this->assertNull($code->getScope()); 768 | $this->assertSame($data, $code->getData()); 769 | } 770 | 771 | public function testFinishClientAuthorizationThrowsErrorIfClientIdMissing() 772 | { 773 | $stub = new OAuth2GrantCodeStub; 774 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); 775 | $oauth2 = new OAuth2($stub); 776 | 777 | $data = new \stdClass; 778 | 779 | try { 780 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 781 | 'redirect_uri' => 'http://www.example.com/?foo=bar', 782 | 'response_type' => 'code', 783 | 'state' => '42', 784 | ))); 785 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 786 | } catch (OAuth2ServerException $e) { 787 | $this->assertSame('invalid_request', $e->getMessage()); 788 | $this->assertSame('No client id supplied', $e->getDescription()); 789 | } 790 | } 791 | 792 | public function testFinishClientAuthorizationThrowsErrorIfClientUnkown() 793 | { 794 | $stub = new OAuth2GrantCodeStub; 795 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); 796 | $oauth2 = new OAuth2($stub); 797 | 798 | $data = new \stdClass; 799 | 800 | try { 801 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 802 | 'client_id' => 'foo', 803 | 'redirect_uri' => 'http://www.example.com/?foo=bar', 804 | 'response_type' => 'code', 805 | 'state' => '42', 806 | ))); 807 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 808 | } catch (OAuth2ServerException $e) { 809 | $this->assertSame('invalid_client', $e->getMessage()); 810 | $this->assertSame('Unknown client', $e->getDescription()); 811 | } 812 | } 813 | 814 | public function testFinishClientAuthorizationThrowsErrorIfNoAvailUri() 815 | { 816 | $stub = new OAuth2GrantCodeStub; 817 | $stub->addClient(new OAuth2Client('blah', 'foo', array())); 818 | $oauth2 = new OAuth2($stub); 819 | 820 | $data = new \stdClass; 821 | 822 | try { 823 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 824 | 'client_id' => 'blah', 825 | 'response_type' => 'code', 826 | 'state' => '42', 827 | ))); 828 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 829 | } catch (OAuth2ServerException $e) { 830 | $this->assertSame('redirect_uri_mismatch', $e->getMessage()); 831 | $this->assertSame('No redirect URL was supplied or registered.', $e->getDescription()); 832 | } 833 | } 834 | 835 | public function testFinishClientAuthorizationThrowsErrorIfMoreThanOneRegisterdUriAndNoSupplied() 836 | { 837 | $stub = new OAuth2GrantCodeStub; 838 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://a.example.com', 'http://b.example.com'))); 839 | $oauth2 = new OAuth2($stub); 840 | 841 | $data = new \stdClass; 842 | 843 | try { 844 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 845 | 'client_id' => 'blah', 846 | 'response_type' => 'code', 847 | 'state' => '42', 848 | ))); 849 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 850 | } catch (OAuth2ServerException $e) { 851 | $this->assertSame('redirect_uri_mismatch', $e->getMessage()); 852 | $this->assertSame('No redirect URL was supplied and more than one is registered.', $e->getDescription()); 853 | } 854 | } 855 | 856 | public function testFinishClientAuthorizationThrowsErrorIfNoSuppliedUri() 857 | { 858 | $stub = new OAuth2GrantCodeStub; 859 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://a.example.com'))); 860 | $oauth2 = new OAuth2($stub); 861 | 862 | $data = new \stdClass; 863 | 864 | try { 865 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 866 | 'client_id' => 'blah', 867 | 'response_type' => 'code', 868 | 'state' => '42', 869 | ))); 870 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 871 | } catch (OAuth2ServerException $e) { 872 | $this->assertSame('redirect_uri_mismatch', $e->getMessage()); 873 | $this->assertSame('The redirect URI is mandatory and was not supplied.', $e->getDescription()); 874 | } 875 | } 876 | 877 | public function testFinishClientAuthorizationThrowsErrorIfNoMatchingUri() 878 | { 879 | $stub = new OAuth2GrantCodeStub; 880 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://a.example.com'))); 881 | $oauth2 = new OAuth2($stub); 882 | 883 | $data = new \stdClass; 884 | 885 | try { 886 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 887 | 'client_id' => 'blah', 888 | 'response_type' => 'code', 889 | 'state' => '42', 890 | 'redirect_uri' => 'http://www.example.com/', 891 | ))); 892 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 893 | } catch (OAuth2ServerException $e) { 894 | $this->assertSame('redirect_uri_mismatch', $e->getMessage()); 895 | $this->assertSame('The redirect URI provided does not match registered URI(s).', $e->getDescription()); 896 | } 897 | } 898 | 899 | public function testFinishClientAuthorizationThrowsErrorIfNoMatchingDomain() 900 | { 901 | $stub = new OAuth2GrantCodeStub; 902 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://a.example.com'))); 903 | $oauth2 = new OAuth2($stub); 904 | 905 | $data = new \stdClass; 906 | 907 | try { 908 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 909 | 'client_id' => 'blah', 910 | 'response_type' => 'code', 911 | 'state' => '42', 912 | 'redirect_uri' => 'http://a.example.com.test.com/', 913 | ))); 914 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 915 | } catch (OAuth2ServerException $e) { 916 | $this->assertSame('redirect_uri_mismatch', $e->getMessage()); 917 | $this->assertSame('The redirect URI provided does not match registered URI(s).', $e->getDescription()); 918 | } 919 | } 920 | 921 | public function testFinishClientAuthorizationThrowsErrorIfNoMatchingPort() 922 | { 923 | $stub = new OAuth2GrantCodeStub; 924 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://a.example.com:80'))); 925 | $oauth2 = new OAuth2($stub); 926 | 927 | $data = new \stdClass; 928 | 929 | try { 930 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 931 | 'client_id' => 'blah', 932 | 'response_type' => 'code', 933 | 'state' => '42', 934 | 'redirect_uri' => 'http://a.example.com:8080/', 935 | ))); 936 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 937 | } catch (OAuth2ServerException $e) { 938 | $this->assertSame('redirect_uri_mismatch', $e->getMessage()); 939 | $this->assertSame('The redirect URI provided does not match registered URI(s).', $e->getDescription()); 940 | } 941 | } 942 | 943 | public function testFinishClientAuthorizationThrowsErrorIfRedirectUriAttemptsPathTraversal() 944 | { 945 | $stub = new OAuth2GrantCodeStub; 946 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://a.example.com/callback'))); 947 | $oauth2 = new OAuth2($stub); 948 | 949 | $data = new \stdClass; 950 | 951 | try { 952 | $oauth2->finishClientAuthorization( 953 | true, 954 | $data, 955 | new Request(array( 956 | 'client_id' => 'blah', 957 | 'response_type' => 'code', 958 | 'state' => '42', 959 | 'redirect_uri' => 'http://a.example.com/callback/../insecure', 960 | )) 961 | ); 962 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 963 | } catch (OAuth2ServerException $e) { 964 | $this->assertSame('redirect_uri_mismatch', $e->getMessage()); 965 | $this->assertSame('The redirect URI provided does not match registered URI(s).', $e->getDescription()); 966 | } 967 | } 968 | 969 | public function testFinishClientAuthorizationThrowsErrorIfResponseTypeIsMissing() 970 | { 971 | $stub = new OAuth2GrantCodeStub; 972 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); 973 | $oauth2 = new OAuth2($stub); 974 | 975 | $data = new \stdClass; 976 | 977 | try { 978 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 979 | 'client_id' => 'blah', 980 | 'redirect_uri' => 'http://www.example.com/?foo=bar', 981 | 'state' => '42', 982 | ))); 983 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 984 | } catch (OAuth2ServerException $e) { 985 | $this->assertSame('invalid_request', $e->getMessage()); 986 | $this->assertSame('Invalid response type.', $e->getDescription()); 987 | } 988 | } 989 | 990 | public function testFinishClientAuthorizationThrowsErrorIfResponseTypeNotSupported() 991 | { 992 | $stub = new OAuth2GrantCodeStub; 993 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); 994 | $oauth2 = new OAuth2($stub); 995 | 996 | $data = new \stdClass; 997 | 998 | try { 999 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 1000 | 'client_id' => 'blah', 1001 | 'redirect_uri' => 'http://www.example.com/?foo=bar', 1002 | 'state' => '42', 1003 | 'response_type' => 'token', 1004 | ))); 1005 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 1006 | } catch (OAuth2ServerException $e) { 1007 | $this->assertSame('unsupported_response_type', $e->getMessage()); 1008 | } 1009 | } 1010 | 1011 | public function testFinishClientAuthorizationThrowsErrorIfResponseTypeUnknown() 1012 | { 1013 | $stub = new OAuth2GrantCodeStub; 1014 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); 1015 | $oauth2 = new OAuth2($stub); 1016 | 1017 | $data = new \stdClass; 1018 | 1019 | try { 1020 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 1021 | 'client_id' => 'blah', 1022 | 'redirect_uri' => 'http://www.example.com/?foo=bar', 1023 | 'state' => '42', 1024 | 'response_type' => 'foo', 1025 | ))); 1026 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 1027 | } catch (OAuth2ServerException $e) { 1028 | $this->assertSame('unsupported_response_type', $e->getMessage()); 1029 | } 1030 | } 1031 | 1032 | public function testFinishClientAuthorizationThrowsErrorIfScopeUnkown() 1033 | { 1034 | $stub = new OAuth2GrantCodeStub; 1035 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); 1036 | $oauth2 = new OAuth2($stub); 1037 | 1038 | $data = new \stdClass; 1039 | 1040 | try { 1041 | $oauth2->finishClientAuthorization(true, $data, new Request(array( 1042 | 'client_id' => 'blah', 1043 | 'redirect_uri' => 'http://www.example.com/?foo=bar', 1044 | 'state' => '42', 1045 | 'response_type' => 'code', 1046 | 'scope' => 'x', 1047 | ))); 1048 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 1049 | } catch (OAuth2ServerException $e) { 1050 | $this->assertSame('invalid_scope', $e->getMessage()); 1051 | } 1052 | } 1053 | 1054 | public function testFinishClientAuthorizationThrowsErrorIfUnauthorized() 1055 | { 1056 | $stub = new OAuth2GrantCodeStub; 1057 | $stub->addClient(new OAuth2Client('blah', 'foo', array('http://www.example.com/'))); 1058 | $oauth2 = new OAuth2($stub); 1059 | 1060 | $data = new \stdClass; 1061 | 1062 | try { 1063 | $oauth2->finishClientAuthorization(false, $data, new Request(array( 1064 | 'client_id' => 'blah', 1065 | 'redirect_uri' => 'http://www.example.com/?foo=bar', 1066 | 'state' => '42', 1067 | 'response_type' => 'code', 1068 | ))); 1069 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 1070 | } catch (OAuth2ServerException $e) { 1071 | $this->assertSame('access_denied', $e->getMessage()); 1072 | $this->assertSame('The user denied access to your application', $e->getDescription()); 1073 | $this->assertSame(array( 1074 | 'Location' => 'http://www.example.com/?foo=bar&error=access_denied&error_description=The+user+denied+access+to+your+application&state=42', 1075 | ), $e->getResponseHeaders()); 1076 | } 1077 | } 1078 | 1079 | /** 1080 | * @dataProvider getTestGetBearerTokenData 1081 | */ 1082 | public function testGetBearerToken(Request $request, $token, $remove = false, $exception = null, $exceptionMessage = null, $headers = null, $body = null) 1083 | { 1084 | $mock = $this->createMock('OAuth2\IOAuth2Storage'); 1085 | $oauth2 = new OAuth2($mock); 1086 | 1087 | try { 1088 | $this->assertSame($token, $oauth2->getBearerToken($request, $remove)); 1089 | if ($exception) { 1090 | $this->fail('The expected exception OAuth2ServerException was not thrown'); 1091 | } 1092 | if ($remove) { 1093 | $this->assertNull($request->headers->get('AUTHORIZATION')); 1094 | $this->assertNull($request->query->get('access_token')); 1095 | $this->assertNull($request->request->get('access_token')); 1096 | } 1097 | } catch (\Exception $e) { 1098 | if (!$exception || !($e instanceof $exception)) { 1099 | throw $e; 1100 | } 1101 | $this->assertSame($headers, $e->getResponseHeaders()); 1102 | $this->assertSame($body, $e->getResponseBody()); 1103 | } 1104 | } 1105 | 1106 | public function getTestGetBearerTokenData() 1107 | { 1108 | $data = array(); 1109 | 1110 | // Authorization header 1111 | $request = new Request; 1112 | $request->headers->set('AUTHORIZATION', 'Bearer foo'); 1113 | $data[] = array($request, 'foo'); 1114 | 1115 | // Authorization header with remove 1116 | $request = new Request; 1117 | $request->headers->set('AUTHORIZATION', 'Bearer foo'); 1118 | $data[] = array($request, 'foo', true); 1119 | 1120 | // GET 1121 | $data[] = array(new Request(array('access_token' => 'foo')), 'foo'); 1122 | 1123 | // GET with remove 1124 | $data[] = array(new Request(array('access_token' => 'foo')), 'foo', true); 1125 | 1126 | foreach (array('POST', 'PUT', 'DELETE', 'FOOBAR') as $method) { 1127 | 1128 | // $method without remove 1129 | $request = Request::create('/', $method, array(), array(), array(), array('CONTENT_TYPE' => 'application/x-www-form-urlencoded'), 'access_token=foo'); 1130 | $data[] = array($request, 'foo'); 1131 | 1132 | // $method without remove and charset 1133 | $request = Request::create('/', $method, array(), array(), array(), array('CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8'), 'access_token=foo'); 1134 | $data[] = array($request, 'foo'); 1135 | 1136 | // $method without remove and additional information 1137 | $request = Request::create('/', $method, array(), array(), array(), array('CONTENT_TYPE' => 'application/x-www-form-urlencoded mode=baz'), 'access_token=foo'); 1138 | $data[] = array($request, 'foo'); 1139 | 1140 | // $method without remove and wrong Content-Type 1141 | $request = Request::create('/', $method, array(), array(), array(), array('CONTENT_TYPE' => 'application/x-www-form-urlencoded-foo'), 'access_token=foo'); 1142 | $data[] = array($request, null); 1143 | 1144 | // $method with remove 1145 | $request = Request::create('/', $method, array(), array(), array(), array('CONTENT_TYPE' => 'application/x-www-form-urlencoded'), 'access_token=foo'); 1146 | $data[] = array($request, 'foo', true); 1147 | 1148 | // $method with remove and charset 1149 | $request = Request::create('/', $method, array(), array(), array(), array('CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8'), 'access_token=foo'); 1150 | $data[] = array($request, 'foo', true); 1151 | 1152 | // $method without remove and additional information 1153 | $request = Request::create('/', $method, array(), array(), array(), array('CONTENT_TYPE' => 'application/x-www-form-urlencoded mode=baz'), 'access_token=foo'); 1154 | $data[] = array($request, 'foo', true); 1155 | 1156 | // $method with remove and wrong Content-Type 1157 | $request = Request::create('/', $method, array(), array(), array(), array('CONTENT_TYPE' => 'application/x-www-form-urlencoded-foo'), 'access_token=foo'); 1158 | $data[] = array($request, null, true); 1159 | } 1160 | 1161 | // No access token provided returns null 1162 | $data[] = array(new Request, null); 1163 | 1164 | // More than one method throws exception 1165 | $request = new Request(array('access_token' => 'foo')); 1166 | $request->headers->set('AUTHORIZATION', 'Bearer foo'); 1167 | $data[] = array( 1168 | $request, 1169 | null, 1170 | null, 1171 | 'OAuth2\OAuth2ServerException', 1172 | 'invalid_request', 1173 | array( 1174 | 'WWW-Authenticate' => 'Bearer realm="Service", error="invalid_request", error_description="Only one method may be used to authenticate at a time (Auth header, GET or POST)."', 1175 | 'Content-Type' => 'application/json', 1176 | 'Cache-Control' => 'no-store', 1177 | 'Pragma' => 'no-cache', 1178 | ), 1179 | '{"error":"invalid_request","error_description":"Only one method may be used to authenticate at a time (Auth header, GET or POST)."}' 1180 | ); 1181 | 1182 | // POST with incorrect Content-Type ignores POST vars 1183 | $request = new Request; 1184 | $request->setMethod('POST'); 1185 | $request->server->set('CONTENT_TYPE', 'multipart/form-data'); 1186 | $request->request->set('access_token', 'foo'); 1187 | $data[] = array( 1188 | $request, 1189 | null, 1190 | false, 1191 | ); 1192 | 1193 | // POST with incorrect Content-Type ignores POST vars and takes GET var 1194 | $request = new Request(array('access_token' => 'foo')); 1195 | $request->setMethod('POST'); 1196 | $request->server->set('CONTENT_TYPE', 'multipart/form-data'); 1197 | $request->request->set('access_token', 'foo'); 1198 | $data[] = array( 1199 | $request, 1200 | 'foo', 1201 | false, 1202 | ); 1203 | 1204 | // POST with request_token in headers 1205 | $request = new Request; 1206 | $request->headers->set('AUTHORIZATION', 'Bearer foo'); 1207 | $request->setMethod('POST'); 1208 | $request->server->set('CONTENT_TYPE', 'application/x-www-form-urlencoded'); 1209 | $data[] = array( 1210 | $request, 1211 | 'foo', 1212 | false, 1213 | ); 1214 | 1215 | // non-Bearer Authorization header 1216 | $request = new Request(array('access_token' => 'foo')); 1217 | $request->headers->set('AUTHORIZATION', 'Basic Zm9vOmJhcg=='); 1218 | $data[] = array($request, 'foo'); 1219 | 1220 | return $data; 1221 | 1222 | } 1223 | 1224 | // Utility methods 1225 | 1226 | /** 1227 | * 1228 | * @param string $interfaceName 1229 | */ 1230 | protected function createBaseMock($interfaceName) 1231 | { 1232 | $mockStorage = $this->createMock($interfaceName); 1233 | $mockStorage->expects($this->any()) 1234 | ->method('checkClientCredentials') 1235 | ->will($this->returnValue(true)); // Always return true for any combination of user/pass 1236 | $mockStorage->expects($this->any()) 1237 | ->method('checkRestrictedGrantType') 1238 | ->will($this->returnValue(true)); // Always return true for any combination of user/pass 1239 | 1240 | return $mockStorage; 1241 | } 1242 | 1243 | // Data Providers below: 1244 | 1245 | /** 1246 | * Dataprovider for testVerifyAccessTokenMalformedToken(). 1247 | * 1248 | * Produces malformed access tokens 1249 | */ 1250 | public function generateMalformedTokens() 1251 | { 1252 | return array( 1253 | array(new OAuth2AccessToken(null, null, null)), 1254 | ); 1255 | } 1256 | 1257 | /** 1258 | * Dataprovider for testVerifyAccessTokenCheckExpiry(). 1259 | * 1260 | * Produces malformed access tokens 1261 | */ 1262 | public function generateExpiryTokens() 1263 | { 1264 | return array( 1265 | array(new OAuth2AccessToken('blah', '', time() - 30), false), // 30 seconds ago should fail 1266 | array(new OAuth2AccessToken('blah', '', time() - 1), false), // now-ish should fail 1267 | array(new OAuth2AccessToken('blah', '', 0), false), // 1970 should fail 1268 | array(new OAuth2AccessToken('blah', '', time() + 30), true), // 30 seconds in the future should be valid 1269 | array(new OAuth2AccessToken('blah', '', time() + 86400), true), // 1 day in the future should be valid 1270 | array(new OAuth2AccessToken('blah', '', time() + (365 * 86400)), true), // 1 year should be valid 1271 | array(new OAuth2AccessToken('blah', '', time() + (10 * 365 * 86400)), true), // 10 years should be valid 1272 | ); 1273 | } 1274 | 1275 | /** 1276 | * Dataprovider for testVerifyAccessTokenCheckExpiry(). 1277 | * 1278 | * Produces malformed access tokens 1279 | */ 1280 | public function generateScopes() 1281 | { 1282 | $token = function ($scope) { 1283 | return new OAuth2AccessToken('blah', '', time() + 60, $scope); 1284 | }; 1285 | 1286 | return array( 1287 | array(null, $token(null), true), // null scope is valid 1288 | array('', $token(''), true), // empty scope is valid 1289 | array('read', $token('read'), true), // exact same scope is valid 1290 | array('read', $token(' read '), true), // exact same scope is valid 1291 | array(' read ', $token('read'), true), // exact same scope is valid 1292 | array('read', $token('read write delete'), true), // contains scope 1293 | array('read', $token('write read delete'), true), // contains scope 1294 | array('read', $token('delete write read'), true), // contains scope 1295 | 1296 | // Invalid combinations 1297 | array('read', $token('write'), false), 1298 | array('read', $token('apple banana'), false), 1299 | array('read', $token('apple read-write'), false), 1300 | array('read', $token('apple read,write'), false), 1301 | array('read', $token(null), false), 1302 | array('read', $token(''), false), 1303 | ); 1304 | } 1305 | 1306 | /** 1307 | * Provider for OAuth2->grantAccessToken() 1308 | */ 1309 | public function generateEmptyDataForGrant() 1310 | { 1311 | return array( 1312 | array( 1313 | $this->createRequest(array(), array()) 1314 | ), 1315 | array( 1316 | $this->createRequest(array(), array('grant_type' => OAuth2::GRANT_TYPE_AUTH_CODE)) // grant_type in auth headers should be ignored 1317 | ), 1318 | array( 1319 | $this->createRequest(array('not_grant_type' => 5), array()) 1320 | ), 1321 | ); 1322 | } 1323 | 1324 | public function createRequest(array $query = array(), array $headers = array()) 1325 | { 1326 | $request = new Request( 1327 | $query // _GET 1328 | , array() // _REQUEST 1329 | , array() // attributes 1330 | , array() // _COOKIES 1331 | , array() // _FILES 1332 | , $headers // _SERVER 1333 | ); 1334 | 1335 | return $request; 1336 | } 1337 | } 1338 | --------------------------------------------------------------------------------