├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Auth0.php ├── Auth0ResourceOwner.php └── Exception │ ├── AccountNotProvidedException.php │ ├── Auth0IdentityProviderException.php │ └── InvalidRegionException.php └── tests ├── Auth0ResourceOwnerTest.php └── Auth0Test.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | .phpunit.result.cache 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.3 5 | - 7.4 6 | - 8.0 7 | 8 | cache: 9 | directories: 10 | - $HOME/.composer/cache 11 | 12 | before_install: 13 | - composer self-update 14 | 15 | install: 16 | - travis_retry composer install --no-interaction --prefer-dist 17 | 18 | script: 19 | - ./vendor/bin/phpunit 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nicolas Eeckeloo 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auth0 Provider for OAuth 2.0 Client 2 | 3 | [![Build Status](https://img.shields.io/travis/RiskioFr/oauth2-auth0.svg)](https://travis-ci.org/RiskioFr/oauth2-auth0) 4 | [![License](https://img.shields.io/packagist/l/riskio/oauth2-auth0.svg)](https://github.com/RiskioFr/oauth2-auth0/blob/master/LICENSE) 5 | [![Latest Stable Version](https://img.shields.io/packagist/v/riskio/oauth2-auth0.svg)](https://packagist.org/packages/riskio/oauth2-auth0) 6 | 7 | This package provides Auth0 OAuth 2.0 support for the PHP League's [OAuth 2.0 Client](https://github.com/thephpleague/oauth2-client). 8 | 9 | ## Installation 10 | 11 | To install, use composer: 12 | 13 | ``` 14 | composer require riskio/oauth2-auth0 15 | ``` 16 | 17 | ## Usage 18 | 19 | Usage is the same as The League's OAuth client, using `Riskio\OAuth2\Client\Provider\Auth0` as the provider. 20 | 21 | ### Authorization Code Flow 22 | 23 | You have to provide some parameters to the provider: 24 | 25 | - customDomain (optional): 26 | - description: Custom domain used for the Auth0 login - https://auth0.com/docs/custom-domains 27 | (I.e.: login.custom-domain.tld - It will be prefixed with https:// automatically. If this is set, the region and account parameters will be ignored.) 28 | - region (optional): 29 | - description: Auth0 region 30 | - values: 31 | - Riskio\OAuth2\Client\Provider\Auth0::REGION_US (default value) 32 | - Riskio\OAuth2\Client\Provider\Auth0::REGION_EU 33 | - Riskio\OAuth2\Client\Provider\Auth0::REGION_AU 34 | - Riskio\OAuth2\Client\Provider\Auth0::REGION_JP 35 | - account (required if customDomain is not set): 36 | - description: Auth0 account name 37 | - clientId 38 | - description: The client ID assigned to you by the provider 39 | - clientSecret 40 | - description: The client password assigned to you by the provider 41 | - redirectUri 42 | 43 | ```php 44 | 45 | session_start(); 46 | 47 | $provider = new Riskio\OAuth2\Client\Provider\Auth0([ 48 | 'region' => '{region}', 49 | 'account' => '{account}', 50 | 'clientId' => '{auth0-client-id}', 51 | 'clientSecret' => '{auth0-client-secret}', 52 | 'redirectUri' => 'https://example.com/callback-url' 53 | ]); 54 | 55 | if (!isset($_GET['code'])) { 56 | 57 | // If we don't have an authorization code then get one 58 | $authUrl = $provider->getAuthorizationUrl(); 59 | $_SESSION['oauth2state'] = $provider->getState(); 60 | header('Location: ' . $authUrl); 61 | exit; 62 | 63 | // Check given state against previously stored one to mitigate CSRF attack 64 | } elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) { 65 | 66 | unset($_SESSION['oauth2state']); 67 | exit('Invalid state'); 68 | 69 | } else { 70 | 71 | // Try to get an access token (using the authorization code grant) 72 | $token = $provider->getAccessToken('authorization_code', [ 73 | 'code' => $_GET['code'] 74 | ]); 75 | 76 | // Optional: Now you have a token you can look up a users profile data 77 | try { 78 | 79 | // We got an access token, let's now get the user's details 80 | $user = $provider->getResourceOwner($token); 81 | 82 | // Use these details to create a new profile 83 | printf('Hello %s!', $user->getName()); 84 | 85 | } catch (Exception $e) { 86 | 87 | // Failed to get user details 88 | exit('Oh dear...'); 89 | } 90 | 91 | // Use this to interact with an API on the users behalf 92 | echo $token->getToken(); 93 | } 94 | ``` 95 | 96 | ## Refreshing a Token 97 | 98 | Auth0's OAuth implementation does not use refresh tokens. 99 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "riskio/oauth2-auth0", 3 | "description": "Auth0 OAuth 2.0 Client Provider for The PHP League OAuth2-Client", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Nicolas Eeckeloo", 8 | "email": "neeckeloo@gmail.com", 9 | "homepage": "https://github.com/neeckeloo" 10 | } 11 | ], 12 | "keywords": [ 13 | "oauth", 14 | "oauth2", 15 | "client", 16 | "authorization", 17 | "authorisation", 18 | "auth0" 19 | ], 20 | "require": { 21 | "php": "^7.3|^8.0", 22 | "league/oauth2-client": "^2.2.1" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^9.3" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Riskio\\OAuth2\\Client\\Provider\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Riskio\\OAuth2\\Client\\Test\\Provider\\": "tests/" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Auth0.php: -------------------------------------------------------------------------------- 1 | customDomain !== null) { 32 | return $this->customDomain; 33 | } 34 | 35 | if (empty($this->account)) { 36 | throw new AccountNotProvidedException(); 37 | } 38 | if (!in_array($this->region, $this->availableRegions)) { 39 | throw new InvalidRegionException(); 40 | } 41 | 42 | $domain = 'auth0.com'; 43 | 44 | if ($this->region !== self::REGION_US) { 45 | $domain = $this->region . '.' . $domain; 46 | } 47 | 48 | return $this->account . '.' . $domain; 49 | } 50 | 51 | protected function baseUrl() 52 | { 53 | return 'https://' . $this->domain(); 54 | } 55 | 56 | public function getBaseAuthorizationUrl() 57 | { 58 | return $this->baseUrl() . '/authorize'; 59 | } 60 | 61 | public function getBaseAccessTokenUrl(array $params = []) 62 | { 63 | return $this->baseUrl() . '/oauth/token'; 64 | } 65 | 66 | public function getResourceOwnerDetailsUrl(AccessToken $token) 67 | { 68 | return $this->baseUrl() . '/userinfo'; 69 | } 70 | 71 | public function getDefaultScopes() 72 | { 73 | return ['openid', 'email']; 74 | } 75 | 76 | protected function checkResponse(ResponseInterface $response, $data) 77 | { 78 | if ($response->getStatusCode() >= 400) { 79 | throw Auth0IdentityProviderException::fromResponse( 80 | $response, 81 | $data['error'] ?: $response->getReasonPhrase() 82 | ); 83 | } 84 | } 85 | 86 | protected function createResourceOwner(array $response, AccessToken $token) 87 | { 88 | return new Auth0ResourceOwner($response); 89 | } 90 | 91 | protected function getScopeSeparator() 92 | { 93 | return ' '; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Auth0ResourceOwner.php: -------------------------------------------------------------------------------- 1 | response = $response; 19 | } 20 | 21 | /** 22 | * @inheritdoc 23 | */ 24 | public function getId() 25 | { 26 | return $this->getValueByKey($this->response, 'user_id'); 27 | } 28 | 29 | /** 30 | * Returns email address of the resource owner 31 | * 32 | * @return string|null 33 | */ 34 | public function getEmail() 35 | { 36 | return $this->getValueByKey($this->response, 'email'); 37 | } 38 | 39 | /** 40 | * Returns full name of the resource owner 41 | * 42 | * @return string|null 43 | */ 44 | public function getName() 45 | { 46 | return $this->getValueByKey($this->response, 'name'); 47 | } 48 | 49 | /** 50 | * Returns nickname of the resource owner 51 | * 52 | * @return string|null 53 | */ 54 | public function getNickname() 55 | { 56 | return $this->getValueByKey($this->response, 'nickname'); 57 | } 58 | 59 | /** 60 | * Returns identities of the resource owner 61 | * 62 | * @see https://auth0.com/docs/user-profile/user-profile-structure 63 | * @return array|null 64 | */ 65 | public function getIdentities() 66 | { 67 | return $this->getValueByKey($this->response, 'identities'); 68 | } 69 | 70 | /** 71 | * Returns picture url of the resource owner 72 | * 73 | * @return string|null 74 | */ 75 | public function getPictureUrl() 76 | { 77 | return $this->getValueByKey($this->response, 'picture'); 78 | } 79 | 80 | /** 81 | * @inheritdoc 82 | */ 83 | public function toArray() 84 | { 85 | return $this->response; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Exception/AccountNotProvidedException.php: -------------------------------------------------------------------------------- 1 | getStatusCode(), (string) $response->getBody()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/InvalidRegionException.php: -------------------------------------------------------------------------------- 1 | 'testuser@gmail.com', 11 | 'email_verified' => true, 12 | 'name' => 'Test User', 13 | 'given_name' => 'Test', 14 | 'family_name' => 'User', 15 | 'picture' => 'https://lh5.googleusercontent.com/-NNasdfdfasdf/asfadfdf/photo.jpg', 16 | 'gender' => 'male', 17 | 'locale' => 'en-GB', 18 | 'clientID' => 'U_DUMmyClientIdhere', 19 | 'updated_at' => '2017-08-25T10:54:21.326Z', 20 | 'user_id' => 'google-oauth2|11204527450454', 21 | 'nickname' => ' testuser', 22 | 'identities' => [ 23 | [ 24 | 'provider' => 'google-oauth2', 25 | 'user_id' => '11204527450454', 26 | 'connection' => 'google-oauth2', 27 | 'isSocial' => true, 28 | ], 29 | ], 30 | 'created_at' => '2017-08-14T13:22:29.753Z', 31 | 'sub' => 'google-oauth2|113974520365241488704', 32 | ]; 33 | 34 | public function testGetUserDetails() 35 | { 36 | $user = new Auth0ResourceOwner($this->response); 37 | 38 | $this->assertEquals($this->response['name'], $user->getName()); 39 | $this->assertEquals($this->response['user_id'], $user->getId()); 40 | $this->assertEquals($this->response['email'], $user->getEmail()); 41 | $this->assertEquals($this->response['identities'], $user->getIdentities()); 42 | $this->assertEquals($this->response, $user->toArray()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Auth0Test.php: -------------------------------------------------------------------------------- 1 | self::DEFAULT_ACCOUNT, 17 | 'clientId' => 'mock_client_id', 18 | 'clientSecret' => 'mock_secret', 19 | 'redirectUri' => 'none', 20 | ]; 21 | 22 | /** 23 | * @dataProvider regionDataProvider 24 | */ 25 | public function testGetAuthorizationUrl($region, $expectedHost) 26 | { 27 | $provider = new OauthProvider(array_merge($this->config, ['region' => $region])); 28 | $url = $provider->getAuthorizationUrl(); 29 | $parsedUrl = parse_url($url); 30 | 31 | $this->assertEquals($expectedHost, $parsedUrl['host']); 32 | $this->assertEquals('/authorize', $parsedUrl['path']); 33 | } 34 | 35 | public function testGetAuthorizationUrlWhenAccountIsNotSpecifiedShouldThrowException() 36 | { 37 | unset($this->config['account']); 38 | 39 | $provider = new OauthProvider($this->config); 40 | 41 | $this->expectException(RuntimeException::class); 42 | $provider->getAuthorizationUrl(); 43 | } 44 | 45 | /** 46 | * @dataProvider regionDataProvider 47 | */ 48 | public function testGetUrlAccessToken($region, $expectedHost) 49 | { 50 | $provider = new OauthProvider(array_merge($this->config, ['region' => $region])); 51 | $url = $provider->getBaseAccessTokenUrl(); 52 | $parsedUrl = parse_url($url); 53 | 54 | $this->assertEquals($expectedHost, $parsedUrl['host']); 55 | $this->assertEquals('/oauth/token', $parsedUrl['path']); 56 | } 57 | 58 | public function testGetAccessTokenUrlWhenAccountIsNotSpecifiedShouldThrowException() 59 | { 60 | unset($this->config['account']); 61 | 62 | $provider = new OauthProvider($this->config); 63 | 64 | $this->expectException(RuntimeException::class); 65 | $provider->getBaseAccessTokenUrl(); 66 | } 67 | 68 | /** 69 | * @dataProvider regionDataProvider 70 | */ 71 | public function testGetUrlUserDetails($region, $expectedHost) 72 | { 73 | $provider = new OauthProvider(array_merge($this->config, ['region' => $region])); 74 | 75 | $accessTokenDummy = $this->getAccessToken(); 76 | 77 | $url = $provider->getResourceOwnerDetailsUrl($accessTokenDummy); 78 | $parsedUrl = parse_url($url); 79 | 80 | $this->assertEquals($expectedHost, $parsedUrl['host']); 81 | $this->assertEquals('/userinfo', $parsedUrl['path']); 82 | } 83 | 84 | public function testGetUserDetailsUrlWhenAccountIsNotSpecifiedShouldThrowException() 85 | { 86 | unset($this->config['account']); 87 | 88 | $provider = new OauthProvider($this->config); 89 | 90 | $accessTokenDummy = $this->getAccessToken(); 91 | 92 | $this->expectException(AccountNotProvidedException::class); 93 | 94 | $provider->getResourceOwner($accessTokenDummy); 95 | } 96 | 97 | public function testGetUserDetailsUrlWhenInvalidRegionIsProvidedShouldThrowException() 98 | { 99 | $this->config['region'] = 'invalid_region'; 100 | 101 | $provider = new OauthProvider($this->config); 102 | 103 | $accessTokenDummy = $this->getAccessToken(); 104 | 105 | $this->expectException(InvalidRegionException::class); 106 | 107 | $provider->getResourceOwner($accessTokenDummy); 108 | } 109 | 110 | public function regionDataProvider() 111 | { 112 | return [ 113 | [ 114 | OauthProvider::REGION_US, 115 | sprintf('%s.auth0.com', self::DEFAULT_ACCOUNT), 116 | ], 117 | [ 118 | OauthProvider::REGION_EU, 119 | sprintf('%s.%s.auth0.com', self::DEFAULT_ACCOUNT, OauthProvider::REGION_EU), 120 | ], 121 | [ 122 | OauthProvider::REGION_AU, 123 | sprintf('%s.%s.auth0.com', self::DEFAULT_ACCOUNT, OauthProvider::REGION_AU), 124 | ], 125 | ]; 126 | } 127 | 128 | /** 129 | * @return \PHPUnit\Framework\MockObject\MockObject|AccessToken 130 | */ 131 | private function getAccessToken() 132 | { 133 | return $this->getMockBuilder(AccessToken::class) 134 | ->disableOriginalConstructor() 135 | ->getMock(); 136 | } 137 | 138 | /** 139 | * @dataProvider scopeDataProvider 140 | */ 141 | public function testGetAuthorizationUrlWithScopes($scopes, $expectedScopeParameter) 142 | { 143 | $provider = new OauthProvider($this->config); 144 | 145 | $url = $provider->getAuthorizationUrl(['scope' => $scopes]); 146 | $queryString = parse_url($url, PHP_URL_QUERY); 147 | 148 | parse_str($queryString, $queryParameters); 149 | 150 | $this->assertArrayHasKey('scope', $queryParameters); 151 | $this->assertSame($expectedScopeParameter, $queryParameters['scope']); 152 | } 153 | 154 | public function scopeDataProvider() 155 | { 156 | return [ 157 | [['openid'], 'openid'], 158 | [['openid', 'email'], 'openid email'], 159 | ]; 160 | } 161 | 162 | public function testGetAuthorizationUrlWithCustomDomain() 163 | { 164 | $customDomain = 'login.custom-domain.tld'; 165 | $provider = new OauthProvider(array_merge($this->config, ['customDomain' => $customDomain])); 166 | $url = $provider->getAuthorizationUrl(); 167 | $expectedBaseUrl = 'https://' . $customDomain; 168 | 169 | $this->assertStringStartsWith($expectedBaseUrl, $url); 170 | } 171 | 172 | /** 173 | * Test that URL getters work as expected with custom domain set, and account not set. 174 | * They should not throw AccountNotProvidedException (or any exception), 175 | * and have to return an url starting with the custom domain. 176 | */ 177 | public function testCustomDomain() 178 | { 179 | $customDomain = 'login.custom-domain.tld'; 180 | $this->config['customDomain'] = $customDomain; 181 | unset($this->config['account']); 182 | $expectedBaseUrl = 'https://' . $customDomain; 183 | 184 | $provider = new OauthProvider($this->config); 185 | $accessTokenDummy = $this->getAccessToken(); 186 | 187 | $url = $provider->getBaseAuthorizationUrl(); 188 | $this->assertStringStartsWith($expectedBaseUrl, $url); 189 | 190 | $url = $provider->getBaseAccessTokenUrl(); 191 | $this->assertStringStartsWith($expectedBaseUrl, $url); 192 | 193 | $url = $provider->getResourceOwnerDetailsUrl($accessTokenDummy); 194 | $this->assertStringStartsWith($expectedBaseUrl, $url); 195 | } 196 | } 197 | --------------------------------------------------------------------------------