├── LICENSE.md ├── README.md ├── composer.json └── src ├── AbstractUser.php ├── Contracts ├── Factory.php ├── Provider.php └── User.php ├── Exceptions └── DriverMissingConfigurationException.php ├── Facades └── Socialite.php ├── One ├── AbstractProvider.php ├── MissingTemporaryCredentialsException.php ├── MissingVerifierException.php ├── TwitterProvider.php └── User.php ├── SocialiteManager.php ├── SocialiteServiceProvider.php └── Two ├── AbstractProvider.php ├── BitbucketProvider.php ├── FacebookProvider.php ├── GithubProvider.php ├── GitlabProvider.php ├── GoogleProvider.php ├── InvalidStateException.php ├── LinkedInOpenIdProvider.php ├── LinkedInProvider.php ├── ProviderInterface.php ├── SlackOpenIdProvider.php ├── SlackProvider.php ├── Token.php ├── TwitchProvider.php ├── TwitterProvider.php ├── User.php └── XProvider.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 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 |

Logo Laravel Socialite

2 | 3 |

4 | Build Status 5 | Total Downloads 6 | Latest Stable Version 7 | License 8 |

9 | 10 | ## Introduction 11 | 12 | Laravel Socialite provides an expressive, fluent interface to OAuth authentication with Bitbucket, Facebook, GitHub, GitLab, Google, LinkedIn, Slack, Twitch, and X. It handles almost all of the boilerplate social authentication code you are dreading writing. 13 | 14 | **We are not accepting new adapters.** 15 | 16 | Adapters for other platforms are listed at the community driven [Socialite Providers](https://socialiteproviders.com/) website. 17 | 18 | ## Official Documentation 19 | 20 | Documentation for Socialite can be found on the [Laravel website](https://laravel.com/docs/socialite). 21 | 22 | ## Contributing 23 | 24 | Thank you for considering contributing to Socialite! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). 25 | 26 | ## Code of Conduct 27 | 28 | In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). 29 | 30 | ## Security Vulnerabilities 31 | 32 | Please review [our security policy](https://github.com/laravel/socialite/security/policy) on how to report security vulnerabilities. 33 | 34 | ## License 35 | 36 | Laravel Socialite is open-sourced software licensed under the [MIT license](LICENSE.md). 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/socialite", 3 | "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", 4 | "keywords": ["oauth", "laravel"], 5 | "license": "MIT", 6 | "homepage": "https://laravel.com", 7 | "support": { 8 | "issues": "https://github.com/laravel/socialite/issues", 9 | "source": "https://github.com/laravel/socialite" 10 | }, 11 | "authors": [ 12 | { 13 | "name": "Taylor Otwell", 14 | "email": "taylor@laravel.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "^7.2|^8.0", 19 | "ext-json": "*", 20 | "firebase/php-jwt": "^6.4", 21 | "guzzlehttp/guzzle": "^6.0|^7.0", 22 | "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 23 | "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 24 | "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 25 | "league/oauth1-client": "^1.11", 26 | "phpseclib/phpseclib": "^3.0" 27 | }, 28 | "require-dev": { 29 | "mockery/mockery": "^1.0", 30 | "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", 31 | "phpstan/phpstan": "^1.12.23", 32 | "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Laravel\\Socialite\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Laravel\\Socialite\\Tests\\": "tests/" 42 | } 43 | }, 44 | "extra": { 45 | "branch-alias": { 46 | "dev-master": "5.x-dev" 47 | }, 48 | "laravel": { 49 | "providers": [ 50 | "Laravel\\Socialite\\SocialiteServiceProvider" 51 | ], 52 | "aliases": { 53 | "Socialite": "Laravel\\Socialite\\Facades\\Socialite" 54 | } 55 | } 56 | }, 57 | "config": { 58 | "sort-packages": true 59 | }, 60 | "minimum-stability": "dev", 61 | "prefer-stable": true 62 | } 63 | -------------------------------------------------------------------------------- /src/AbstractUser.php: -------------------------------------------------------------------------------- 1 | id; 67 | } 68 | 69 | /** 70 | * Get the nickname / username for the user. 71 | * 72 | * @return string|null 73 | */ 74 | public function getNickname() 75 | { 76 | return $this->nickname; 77 | } 78 | 79 | /** 80 | * Get the full name of the user. 81 | * 82 | * @return string|null 83 | */ 84 | public function getName() 85 | { 86 | return $this->name; 87 | } 88 | 89 | /** 90 | * Get the e-mail address of the user. 91 | * 92 | * @return string|null 93 | */ 94 | public function getEmail() 95 | { 96 | return $this->email; 97 | } 98 | 99 | /** 100 | * Get the avatar / image URL for the user. 101 | * 102 | * @return string|null 103 | */ 104 | public function getAvatar() 105 | { 106 | return $this->avatar; 107 | } 108 | 109 | /** 110 | * Get the raw user array. 111 | * 112 | * @return array 113 | */ 114 | public function getRaw() 115 | { 116 | return $this->user; 117 | } 118 | 119 | /** 120 | * Set the raw user array from the provider. 121 | * 122 | * @param array $user 123 | * @return $this 124 | */ 125 | public function setRaw(array $user) 126 | { 127 | $this->user = $user; 128 | 129 | return $this; 130 | } 131 | 132 | /** 133 | * Map the given array onto the user's properties. 134 | * 135 | * @param array $attributes 136 | * @return $this 137 | */ 138 | public function map(array $attributes) 139 | { 140 | $this->attributes = $attributes; 141 | 142 | foreach ($attributes as $key => $value) { 143 | if (property_exists($this, $key)) { 144 | $this->{$key} = $value; 145 | } 146 | } 147 | 148 | return $this; 149 | } 150 | 151 | /** 152 | * Determine if the given raw user attribute exists. 153 | * 154 | * @param string $offset 155 | * @return bool 156 | */ 157 | #[\ReturnTypeWillChange] 158 | public function offsetExists($offset) 159 | { 160 | return array_key_exists($offset, $this->user); 161 | } 162 | 163 | /** 164 | * Get the given key from the raw user. 165 | * 166 | * @param string $offset 167 | * @return mixed 168 | */ 169 | #[\ReturnTypeWillChange] 170 | public function offsetGet($offset) 171 | { 172 | return $this->user[$offset]; 173 | } 174 | 175 | /** 176 | * Set the given attribute on the raw user array. 177 | * 178 | * @param string $offset 179 | * @param mixed $value 180 | * @return void 181 | */ 182 | #[\ReturnTypeWillChange] 183 | public function offsetSet($offset, $value) 184 | { 185 | $this->user[$offset] = $value; 186 | } 187 | 188 | /** 189 | * Unset the given value from the raw user array. 190 | * 191 | * @param string $offset 192 | * @return void 193 | */ 194 | #[\ReturnTypeWillChange] 195 | public function offsetUnset($offset) 196 | { 197 | unset($this->user[$offset]); 198 | } 199 | 200 | /** 201 | * Get a user attribute value dynamically. 202 | * 203 | * @param string $key 204 | * @return void 205 | */ 206 | public function __get($key) 207 | { 208 | return $this->attributes[$key] ?? null; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/Contracts/Factory.php: -------------------------------------------------------------------------------- 1 | $keys 14 | * @return static 15 | */ 16 | public static function make($provider, $keys) 17 | { 18 | /** @phpstan-ignore new.static */ 19 | return new static('Missing required configuration keys ['.implode(', ', $keys)."] for [{$provider}] OAuth provider."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Facades/Socialite.php: -------------------------------------------------------------------------------- 1 | server = $server; 44 | $this->request = $request; 45 | } 46 | 47 | /** 48 | * Redirect the user to the authentication page for the provider. 49 | * 50 | * @return \Illuminate\Http\RedirectResponse 51 | */ 52 | public function redirect() 53 | { 54 | $this->request->session()->put( 55 | 'oauth.temp', $temp = $this->server->getTemporaryCredentials() 56 | ); 57 | 58 | return new RedirectResponse($this->server->getAuthorizationUrl($temp)); 59 | } 60 | 61 | /** 62 | * Get the User instance for the authenticated user. 63 | * 64 | * @return \Laravel\Socialite\One\User 65 | * 66 | * @throws \Laravel\Socialite\One\MissingVerifierException 67 | */ 68 | public function user() 69 | { 70 | if (! $this->hasNecessaryVerifier()) { 71 | throw new MissingVerifierException('Invalid request. Missing OAuth verifier.'); 72 | } 73 | 74 | $token = $this->getToken(); 75 | 76 | $user = $this->server->getUserDetails( 77 | $token, $this->shouldBypassCache($token->getIdentifier(), $token->getSecret()) 78 | ); 79 | 80 | $instance = (new User)->setRaw($user->extra) 81 | ->setToken($token->getIdentifier(), $token->getSecret()); 82 | 83 | return $instance->map([ 84 | 'id' => $user->uid, 85 | 'nickname' => $user->nickname, 86 | 'name' => $user->name, 87 | 'email' => $user->email, 88 | 'avatar' => $user->imageUrl, 89 | ]); 90 | } 91 | 92 | /** 93 | * Get a Social User instance from a known access token and secret. 94 | * 95 | * @param string $token 96 | * @param string $secret 97 | * @return \Laravel\Socialite\One\User 98 | */ 99 | public function userFromTokenAndSecret($token, $secret) 100 | { 101 | $tokenCredentials = new TokenCredentials(); 102 | 103 | $tokenCredentials->setIdentifier($token); 104 | $tokenCredentials->setSecret($secret); 105 | 106 | $user = $this->server->getUserDetails( 107 | $tokenCredentials, $this->shouldBypassCache($token, $secret) 108 | ); 109 | 110 | $instance = (new User)->setRaw($user->extra) 111 | ->setToken($tokenCredentials->getIdentifier(), $tokenCredentials->getSecret()); 112 | 113 | return $instance->map([ 114 | 'id' => $user->uid, 115 | 'nickname' => $user->nickname, 116 | 'name' => $user->name, 117 | 'email' => $user->email, 118 | 'avatar' => $user->imageUrl, 119 | ]); 120 | } 121 | 122 | /** 123 | * Get the token credentials for the request. 124 | * 125 | * @return \League\OAuth1\Client\Credentials\TokenCredentials 126 | */ 127 | protected function getToken() 128 | { 129 | $temp = $this->request->session()->get('oauth.temp'); 130 | 131 | if (! $temp) { 132 | throw new MissingTemporaryCredentialsException('Missing temporary OAuth credentials.'); 133 | } 134 | 135 | return $this->server->getTokenCredentials( 136 | $temp, $this->request->get('oauth_token'), $this->request->get('oauth_verifier') 137 | ); 138 | } 139 | 140 | /** 141 | * Determine if the request has the necessary OAuth verifier. 142 | * 143 | * @return bool 144 | */ 145 | protected function hasNecessaryVerifier() 146 | { 147 | return $this->request->has(['oauth_token', 'oauth_verifier']); 148 | } 149 | 150 | /** 151 | * Determine if the user information cache should be bypassed. 152 | * 153 | * @param string $token 154 | * @param string $secret 155 | * @return bool 156 | */ 157 | protected function shouldBypassCache($token, $secret) 158 | { 159 | $newHash = sha1($token.'_'.$secret); 160 | 161 | if (! empty($this->userHash) && $newHash !== $this->userHash) { 162 | $this->userHash = $newHash; 163 | 164 | return true; 165 | } 166 | 167 | $this->userHash = $this->userHash ?: $newHash; 168 | 169 | return false; 170 | } 171 | 172 | /** 173 | * Set the request instance. 174 | * 175 | * @param \Illuminate\Http\Request $request 176 | * @return $this 177 | */ 178 | public function setRequest(Request $request) 179 | { 180 | $this->request = $request; 181 | 182 | return $this; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/One/MissingTemporaryCredentialsException.php: -------------------------------------------------------------------------------- 1 | hasNecessaryVerifier()) { 13 | throw new MissingVerifierException('Invalid request. Missing OAuth verifier.'); 14 | } 15 | 16 | $user = $this->server->getUserDetails($token = $this->getToken(), $this->shouldBypassCache($token->getIdentifier(), $token->getSecret())); 17 | 18 | $extraDetails = [ 19 | 'location' => $user->location, 20 | 'description' => $user->description, 21 | ]; 22 | 23 | $instance = (new User)->setRaw(array_merge($user->extra, $user->urls, $extraDetails)) 24 | ->setToken($token->getIdentifier(), $token->getSecret()); 25 | 26 | return $instance->map([ 27 | 'id' => $user->uid, 28 | 'nickname' => $user->nickname, 29 | 'name' => $user->name, 30 | 'email' => $user->email, 31 | 'avatar' => $user->imageUrl, 32 | 'avatar_original' => str_replace('_normal', '', $user->imageUrl), 33 | ]); 34 | } 35 | 36 | /** 37 | * Set the access level the application should request to the user account. 38 | * 39 | * @param string $scope 40 | * @return void 41 | */ 42 | public function scope(string $scope) 43 | { 44 | $this->server->setApplicationScope($scope); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/One/User.php: -------------------------------------------------------------------------------- 1 | token = $token; 33 | $this->tokenSecret = $tokenSecret; 34 | 35 | return $this; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/SocialiteManager.php: -------------------------------------------------------------------------------- 1 | driver($driver); 45 | } 46 | 47 | /** 48 | * Create an instance of the specified driver. 49 | * 50 | * @return \Laravel\Socialite\Two\AbstractProvider 51 | */ 52 | protected function createGithubDriver() 53 | { 54 | $config = $this->config->get('services.github'); 55 | 56 | return $this->buildProvider( 57 | GithubProvider::class, $config 58 | ); 59 | } 60 | 61 | /** 62 | * Create an instance of the specified driver. 63 | * 64 | * @return \Laravel\Socialite\Two\AbstractProvider 65 | */ 66 | protected function createFacebookDriver() 67 | { 68 | $config = $this->config->get('services.facebook'); 69 | 70 | return $this->buildProvider( 71 | FacebookProvider::class, $config 72 | ); 73 | } 74 | 75 | /** 76 | * Create an instance of the specified driver. 77 | * 78 | * @return \Laravel\Socialite\Two\AbstractProvider 79 | */ 80 | protected function createGoogleDriver() 81 | { 82 | $config = $this->config->get('services.google'); 83 | 84 | return $this->buildProvider( 85 | GoogleProvider::class, $config 86 | ); 87 | } 88 | 89 | /** 90 | * Create an instance of the specified driver. 91 | * 92 | * @return \Laravel\Socialite\Two\AbstractProvider 93 | */ 94 | protected function createLinkedinDriver() 95 | { 96 | $config = $this->config->get('services.linkedin'); 97 | 98 | return $this->buildProvider( 99 | LinkedInProvider::class, $config 100 | ); 101 | } 102 | 103 | /** 104 | * Create an instance of the specified driver. 105 | * 106 | * @return \Laravel\Socialite\Two\AbstractProvider 107 | */ 108 | protected function createLinkedinOpenidDriver() 109 | { 110 | $config = $this->config->get('services.linkedin-openid'); 111 | 112 | return $this->buildProvider( 113 | LinkedInOpenIdProvider::class, $config 114 | ); 115 | } 116 | 117 | /** 118 | * Create an instance of the specified driver. 119 | * 120 | * @return \Laravel\Socialite\Two\AbstractProvider 121 | */ 122 | protected function createBitbucketDriver() 123 | { 124 | $config = $this->config->get('services.bitbucket'); 125 | 126 | return $this->buildProvider( 127 | BitbucketProvider::class, $config 128 | ); 129 | } 130 | 131 | /** 132 | * Create an instance of the specified driver. 133 | * 134 | * @return \Laravel\Socialite\Two\AbstractProvider 135 | */ 136 | protected function createGitlabDriver() 137 | { 138 | $config = $this->config->get('services.gitlab'); 139 | 140 | return $this->buildProvider( 141 | GitlabProvider::class, $config 142 | )->setHost($config['host'] ?? null); 143 | } 144 | 145 | /** 146 | * Create an instance of the specified driver. 147 | * 148 | * @return \Laravel\Socialite\One\AbstractProvider|\Laravel\Socialite\Two\AbstractProvider 149 | */ 150 | protected function createTwitterDriver() 151 | { 152 | $config = $this->config->get('services.twitter'); 153 | 154 | if (($config['oauth'] ?? null) === 2) { 155 | return $this->createTwitterOAuth2Driver(); 156 | } 157 | 158 | return new TwitterProvider( 159 | $this->container->make('request'), new TwitterServer($this->formatConfig($config)) 160 | ); 161 | } 162 | 163 | /** 164 | * Create an instance of the specified driver. 165 | * 166 | * @return \Laravel\Socialite\Two\AbstractProvider 167 | */ 168 | protected function createTwitterOAuth2Driver() 169 | { 170 | $config = $this->config->get('services.twitter') ?? $this->config->get('services.twitter-oauth-2'); 171 | 172 | return $this->buildProvider( 173 | TwitterOAuth2Provider::class, $config 174 | ); 175 | } 176 | 177 | /** 178 | * Create an instance of the specified driver. 179 | * 180 | * @return \Laravel\Socialite\Two\AbstractProvider 181 | */ 182 | protected function createXDriver() 183 | { 184 | $config = $this->config->get('services.x') ?? $this->config->get('services.x-oauth-2'); 185 | 186 | return $this->buildProvider( 187 | XProvider::class, $config 188 | ); 189 | } 190 | 191 | /** 192 | * Create an instance of the specified driver. 193 | * 194 | * @return \Laravel\Socialite\Two\AbstractProvider 195 | */ 196 | protected function createTwitchDriver() 197 | { 198 | $config = $this->config->get('services.twitch'); 199 | 200 | return $this->buildProvider( 201 | TwitchProvider::class, $config 202 | ); 203 | } 204 | 205 | /** 206 | * Create an instance of the specified driver. 207 | * 208 | * @return \Laravel\Socialite\Two\AbstractProvider 209 | */ 210 | protected function createSlackDriver() 211 | { 212 | $config = $this->config->get('services.slack'); 213 | 214 | return $this->buildProvider( 215 | SlackProvider::class, $config 216 | ); 217 | } 218 | 219 | /** 220 | * Create an instance of the specified driver. 221 | * 222 | * @return \Laravel\Socialite\Two\AbstractProvider 223 | */ 224 | protected function createSlackOpenidDriver() 225 | { 226 | $config = $this->config->get('services.slack-openid'); 227 | 228 | return $this->buildProvider( 229 | SlackOpenIdProvider::class, $config 230 | ); 231 | } 232 | 233 | /** 234 | * Build an OAuth 2 provider instance. 235 | * 236 | * @param string $provider 237 | * @param array $config 238 | * @return \Laravel\Socialite\Two\AbstractProvider 239 | */ 240 | public function buildProvider($provider, $config) 241 | { 242 | $requiredKeys = ['client_id', 'client_secret', 'redirect']; 243 | 244 | $missingKeys = array_diff($requiredKeys, array_keys($config ?? [])); 245 | 246 | if (! empty($missingKeys)) { 247 | throw DriverMissingConfigurationException::make($provider, $missingKeys); 248 | } 249 | 250 | return (new $provider( 251 | $this->container->make('request'), $config['client_id'], 252 | $config['client_secret'], $this->formatRedirectUrl($config), 253 | Arr::get($config, 'guzzle', []) 254 | ))->scopes($config['scopes'] ?? []); 255 | } 256 | 257 | /** 258 | * Format the server configuration. 259 | * 260 | * @param array $config 261 | * @return array 262 | */ 263 | public function formatConfig(array $config) 264 | { 265 | return array_merge([ 266 | 'identifier' => $config['client_id'], 267 | 'secret' => $config['client_secret'], 268 | 'callback_uri' => $this->formatRedirectUrl($config), 269 | ], $config); 270 | } 271 | 272 | /** 273 | * Format the callback URL, resolving a relative URI if needed. 274 | * 275 | * @param array $config 276 | * @return string 277 | */ 278 | protected function formatRedirectUrl(array $config) 279 | { 280 | $redirect = value($config['redirect']); 281 | 282 | return Str::startsWith($redirect ?? '', '/') 283 | ? $this->container->make('url')->to($redirect) 284 | : $redirect; 285 | } 286 | 287 | /** 288 | * Forget all of the resolved driver instances. 289 | * 290 | * @return $this 291 | */ 292 | public function forgetDrivers() 293 | { 294 | $this->drivers = []; 295 | 296 | return $this; 297 | } 298 | 299 | /** 300 | * Set the container instance used by the manager. 301 | * 302 | * @param \Illuminate\Contracts\Container\Container $container 303 | * @return $this 304 | */ 305 | public function setContainer($container) 306 | { 307 | $this->app = $container; 308 | $this->container = $container; 309 | $this->config = $container->make('config'); 310 | 311 | return $this; 312 | } 313 | 314 | /** 315 | * Get the default driver name. 316 | * 317 | * @return string 318 | * 319 | * @throws \InvalidArgumentException 320 | */ 321 | public function getDefaultDriver() 322 | { 323 | throw new InvalidArgumentException('No Socialite driver was specified.'); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/SocialiteServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(Factory::class, function ($app) { 19 | return new SocialiteManager($app); 20 | }); 21 | } 22 | 23 | /** 24 | * Get the services provided by the provider. 25 | * 26 | * @return array 27 | */ 28 | public function provides() 29 | { 30 | return [Factory::class]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Two/AbstractProvider.php: -------------------------------------------------------------------------------- 1 | guzzle = $guzzle; 119 | $this->request = $request; 120 | $this->clientId = $clientId; 121 | $this->redirectUrl = $redirectUrl; 122 | $this->clientSecret = $clientSecret; 123 | } 124 | 125 | /** 126 | * Get the authentication URL for the provider. 127 | * 128 | * @param string $state 129 | * @return string 130 | */ 131 | abstract protected function getAuthUrl($state); 132 | 133 | /** 134 | * Get the token URL for the provider. 135 | * 136 | * @return string 137 | */ 138 | abstract protected function getTokenUrl(); 139 | 140 | /** 141 | * Get the raw user for the given access token. 142 | * 143 | * @param string $token 144 | * @return mixed 145 | */ 146 | abstract protected function getUserByToken($token); 147 | 148 | /** 149 | * Map the raw user array to a Socialite User instance. 150 | * 151 | * @param array $user 152 | * @return \Laravel\Socialite\Two\User 153 | */ 154 | abstract protected function mapUserToObject(array $user); 155 | 156 | /** 157 | * Redirect the user of the application to the provider's authentication screen. 158 | * 159 | * @return \Illuminate\Http\RedirectResponse 160 | */ 161 | public function redirect() 162 | { 163 | $state = null; 164 | 165 | if ($this->usesState()) { 166 | $this->request->session()->put('state', $state = $this->getState()); 167 | } 168 | 169 | if ($this->usesPKCE()) { 170 | $this->request->session()->put('code_verifier', $this->getCodeVerifier()); 171 | } 172 | 173 | return new RedirectResponse($this->getAuthUrl($state)); 174 | } 175 | 176 | /** 177 | * Build the authentication URL for the provider from the given base URL. 178 | * 179 | * @param string $url 180 | * @param string $state 181 | * @return string 182 | */ 183 | protected function buildAuthUrlFromBase($url, $state) 184 | { 185 | return $url.'?'.http_build_query($this->getCodeFields($state), '', '&', $this->encodingType); 186 | } 187 | 188 | /** 189 | * Get the GET parameters for the code request. 190 | * 191 | * @param string|null $state 192 | * @return array 193 | */ 194 | protected function getCodeFields($state = null) 195 | { 196 | $fields = [ 197 | 'client_id' => $this->clientId, 198 | 'redirect_uri' => $this->redirectUrl, 199 | 'scope' => $this->formatScopes($this->getScopes(), $this->scopeSeparator), 200 | 'response_type' => 'code', 201 | ]; 202 | 203 | if ($this->usesState()) { 204 | $fields['state'] = $state; 205 | } 206 | 207 | if ($this->usesPKCE()) { 208 | $fields['code_challenge'] = $this->getCodeChallenge(); 209 | $fields['code_challenge_method'] = $this->getCodeChallengeMethod(); 210 | } 211 | 212 | return array_merge($fields, $this->parameters); 213 | } 214 | 215 | /** 216 | * Format the given scopes. 217 | * 218 | * @param array $scopes 219 | * @param string $scopeSeparator 220 | * @return string 221 | */ 222 | protected function formatScopes(array $scopes, $scopeSeparator) 223 | { 224 | return implode($scopeSeparator, $scopes); 225 | } 226 | 227 | /** 228 | * {@inheritdoc} 229 | */ 230 | public function user() 231 | { 232 | if ($this->user) { 233 | return $this->user; 234 | } 235 | 236 | if ($this->hasInvalidState()) { 237 | throw new InvalidStateException; 238 | } 239 | 240 | $response = $this->getAccessTokenResponse($this->getCode()); 241 | 242 | $user = $this->getUserByToken(Arr::get($response, 'access_token')); 243 | 244 | return $this->userInstance($response, $user); 245 | } 246 | 247 | /** 248 | * Create a user instance from the given data. 249 | * 250 | * @param array $response 251 | * @param array $user 252 | * @return \Laravel\Socialite\Two\User 253 | */ 254 | protected function userInstance(array $response, array $user) 255 | { 256 | $this->user = $this->mapUserToObject($user); 257 | 258 | return $this->user->setToken(Arr::get($response, 'access_token')) 259 | ->setRefreshToken(Arr::get($response, 'refresh_token')) 260 | ->setExpiresIn(Arr::get($response, 'expires_in')) 261 | ->setApprovedScopes(explode($this->scopeSeparator, Arr::get($response, 'scope', ''))); 262 | } 263 | 264 | /** 265 | * Get a Social User instance from a known access token. 266 | * 267 | * @param string $token 268 | * @return \Laravel\Socialite\Two\User 269 | */ 270 | public function userFromToken($token) 271 | { 272 | $user = $this->mapUserToObject($this->getUserByToken($token)); 273 | 274 | return $user->setToken($token); 275 | } 276 | 277 | /** 278 | * Determine if the current request / session has a mismatching "state". 279 | * 280 | * @return bool 281 | */ 282 | protected function hasInvalidState() 283 | { 284 | if ($this->isStateless()) { 285 | return false; 286 | } 287 | 288 | $state = $this->request->session()->pull('state'); 289 | 290 | return empty($state) || $this->request->input('state') !== $state; 291 | } 292 | 293 | /** 294 | * Get the access token response for the given code. 295 | * 296 | * @param string $code 297 | * @return mixed 298 | */ 299 | public function getAccessTokenResponse($code) 300 | { 301 | $response = $this->getHttpClient()->post($this->getTokenUrl(), [ 302 | RequestOptions::HEADERS => $this->getTokenHeaders($code), 303 | RequestOptions::FORM_PARAMS => $this->getTokenFields($code), 304 | ]); 305 | 306 | return json_decode($response->getBody(), true); 307 | } 308 | 309 | /** 310 | * Get the headers for the access token request. 311 | * 312 | * @param string $code 313 | * @return array 314 | */ 315 | protected function getTokenHeaders($code) 316 | { 317 | return ['Accept' => 'application/json']; 318 | } 319 | 320 | /** 321 | * Get the POST fields for the token request. 322 | * 323 | * @param string $code 324 | * @return array 325 | */ 326 | protected function getTokenFields($code) 327 | { 328 | $fields = [ 329 | 'grant_type' => 'authorization_code', 330 | 'client_id' => $this->clientId, 331 | 'client_secret' => $this->clientSecret, 332 | 'code' => $code, 333 | 'redirect_uri' => $this->redirectUrl, 334 | ]; 335 | 336 | if ($this->usesPKCE()) { 337 | $fields['code_verifier'] = $this->request->session()->pull('code_verifier'); 338 | } 339 | 340 | return array_merge($fields, $this->parameters); 341 | } 342 | 343 | /** 344 | * Refresh a user's access token with a refresh token. 345 | * 346 | * @param string $refreshToken 347 | * @return \Laravel\Socialite\Two\Token 348 | */ 349 | public function refreshToken($refreshToken) 350 | { 351 | $response = $this->getRefreshTokenResponse($refreshToken); 352 | 353 | return new Token( 354 | Arr::get($response, 'access_token'), 355 | Arr::get($response, 'refresh_token'), 356 | Arr::get($response, 'expires_in'), 357 | explode($this->scopeSeparator, Arr::get($response, 'scope', '')) 358 | ); 359 | } 360 | 361 | /** 362 | * Get the refresh token response for the given refresh token. 363 | * 364 | * @param string $refreshToken 365 | * @return mixed 366 | */ 367 | protected function getRefreshTokenResponse($refreshToken) 368 | { 369 | return json_decode($this->getHttpClient()->post($this->getTokenUrl(), [ 370 | RequestOptions::HEADERS => ['Accept' => 'application/json'], 371 | RequestOptions::FORM_PARAMS => [ 372 | 'grant_type' => 'refresh_token', 373 | 'refresh_token' => $refreshToken, 374 | 'client_id' => $this->clientId, 375 | 'client_secret' => $this->clientSecret, 376 | ], 377 | ])->getBody(), true); 378 | } 379 | 380 | /** 381 | * Get the code from the request. 382 | * 383 | * @return string 384 | */ 385 | protected function getCode() 386 | { 387 | return $this->request->input('code'); 388 | } 389 | 390 | /** 391 | * Merge the scopes of the requested access. 392 | * 393 | * @param array|string $scopes 394 | * @return $this 395 | */ 396 | public function scopes($scopes) 397 | { 398 | $this->scopes = array_values(array_unique(array_merge($this->scopes, (array) $scopes))); 399 | 400 | return $this; 401 | } 402 | 403 | /** 404 | * Set the scopes of the requested access. 405 | * 406 | * @param array|string $scopes 407 | * @return $this 408 | */ 409 | public function setScopes($scopes) 410 | { 411 | $this->scopes = array_values(array_unique((array) $scopes)); 412 | 413 | return $this; 414 | } 415 | 416 | /** 417 | * Get the current scopes. 418 | * 419 | * @return array 420 | */ 421 | public function getScopes() 422 | { 423 | return $this->scopes; 424 | } 425 | 426 | /** 427 | * Set the redirect URL. 428 | * 429 | * @param string $url 430 | * @return $this 431 | */ 432 | public function redirectUrl($url) 433 | { 434 | $this->redirectUrl = $url; 435 | 436 | return $this; 437 | } 438 | 439 | /** 440 | * Get a instance of the Guzzle HTTP client. 441 | * 442 | * @return \GuzzleHttp\Client 443 | */ 444 | protected function getHttpClient() 445 | { 446 | if (is_null($this->httpClient)) { 447 | $this->httpClient = new Client($this->guzzle); 448 | } 449 | 450 | return $this->httpClient; 451 | } 452 | 453 | /** 454 | * Set the Guzzle HTTP client instance. 455 | * 456 | * @param \GuzzleHttp\Client $client 457 | * @return $this 458 | */ 459 | public function setHttpClient(Client $client) 460 | { 461 | $this->httpClient = $client; 462 | 463 | return $this; 464 | } 465 | 466 | /** 467 | * Set the request instance. 468 | * 469 | * @param \Illuminate\Http\Request $request 470 | * @return $this 471 | */ 472 | public function setRequest(Request $request) 473 | { 474 | $this->request = $request; 475 | 476 | return $this; 477 | } 478 | 479 | /** 480 | * Determine if the provider is operating with state. 481 | * 482 | * @return bool 483 | */ 484 | protected function usesState() 485 | { 486 | return ! $this->stateless; 487 | } 488 | 489 | /** 490 | * Determine if the provider is operating as stateless. 491 | * 492 | * @return bool 493 | */ 494 | protected function isStateless() 495 | { 496 | return $this->stateless; 497 | } 498 | 499 | /** 500 | * Indicates that the provider should operate as stateless. 501 | * 502 | * @return $this 503 | */ 504 | public function stateless() 505 | { 506 | $this->stateless = true; 507 | 508 | return $this; 509 | } 510 | 511 | /** 512 | * Get the string used for session state. 513 | * 514 | * @return string 515 | */ 516 | protected function getState() 517 | { 518 | return Str::random(40); 519 | } 520 | 521 | /** 522 | * Determine if the provider uses PKCE. 523 | * 524 | * @return bool 525 | */ 526 | protected function usesPKCE() 527 | { 528 | return $this->usesPKCE; 529 | } 530 | 531 | /** 532 | * Enables PKCE for the provider. 533 | * 534 | * @return $this 535 | */ 536 | public function enablePKCE() 537 | { 538 | $this->usesPKCE = true; 539 | 540 | return $this; 541 | } 542 | 543 | /** 544 | * Generates a random string of the right length for the PKCE code verifier. 545 | * 546 | * @return string 547 | */ 548 | protected function getCodeVerifier() 549 | { 550 | return Str::random(96); 551 | } 552 | 553 | /** 554 | * Generates the PKCE code challenge based on the PKCE code verifier in the session. 555 | * 556 | * @return string 557 | */ 558 | protected function getCodeChallenge() 559 | { 560 | $hashed = hash('sha256', $this->request->session()->get('code_verifier'), true); 561 | 562 | return rtrim(strtr(base64_encode($hashed), '+/', '-_'), '='); 563 | } 564 | 565 | /** 566 | * Returns the hash method used to calculate the PKCE code challenge. 567 | * 568 | * @return string 569 | */ 570 | protected function getCodeChallengeMethod() 571 | { 572 | return 'S256'; 573 | } 574 | 575 | /** 576 | * Set the custom parameters of the request. 577 | * 578 | * @param array $parameters 579 | * @return $this 580 | */ 581 | public function with(array $parameters) 582 | { 583 | $this->parameters = $parameters; 584 | 585 | return $this; 586 | } 587 | } 588 | -------------------------------------------------------------------------------- /src/Two/BitbucketProvider.php: -------------------------------------------------------------------------------- 1 | buildAuthUrlFromBase('https://bitbucket.org/site/oauth2/authorize', $state); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | protected function getTokenUrl() 37 | { 38 | return 'https://bitbucket.org/site/oauth2/access_token'; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function getUserByToken($token) 45 | { 46 | $response = $this->getHttpClient()->get('https://api.bitbucket.org/2.0/user', [ 47 | RequestOptions::QUERY => ['access_token' => $token], 48 | ]); 49 | 50 | $user = json_decode($response->getBody(), true); 51 | 52 | if (in_array('email', $this->scopes, true)) { 53 | $user['email'] = $this->getEmailByToken($token); 54 | } 55 | 56 | return $user; 57 | } 58 | 59 | /** 60 | * Get the email for the given access token. 61 | * 62 | * @param string $token 63 | * @return string|null 64 | */ 65 | protected function getEmailByToken($token) 66 | { 67 | $emailsUrl = 'https://api.bitbucket.org/2.0/user/emails?access_token='.$token; 68 | 69 | try { 70 | $response = $this->getHttpClient()->get($emailsUrl); 71 | } catch (Exception $e) { 72 | return; 73 | } 74 | 75 | $emails = json_decode($response->getBody(), true); 76 | 77 | foreach ($emails['values'] as $email) { 78 | if ($email['type'] === 'email' && $email['is_primary'] && $email['is_confirmed']) { 79 | return $email['email']; 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | protected function mapUserToObject(array $user) 88 | { 89 | return (new User)->setRaw($user)->map([ 90 | 'id' => $user['uuid'], 91 | 'nickname' => $user['username'], 92 | 'name' => Arr::get($user, 'display_name'), 93 | 'email' => Arr::get($user, 'email'), 94 | 'avatar' => Arr::get($user, 'links.avatar.href'), 95 | ]); 96 | } 97 | 98 | /** 99 | * Get the access token for the given code. 100 | * 101 | * @param string $code 102 | * @return string 103 | */ 104 | public function getAccessToken($code) 105 | { 106 | $response = $this->getHttpClient()->post($this->getTokenUrl(), [ 107 | RequestOptions::AUTH => [$this->clientId, $this->clientSecret], 108 | RequestOptions::HEADERS => ['Accept' => 'application/json'], 109 | RequestOptions::FORM_PARAMS => $this->getTokenFields($code), 110 | ]); 111 | 112 | return json_decode($response->getBody(), true)['access_token']; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Two/FacebookProvider.php: -------------------------------------------------------------------------------- 1 | buildAuthUrlFromBase('https://www.facebook.com/'.$this->version.'/dialog/oauth', $state); 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | protected function getTokenUrl() 76 | { 77 | return $this->graphUrl.'/'.$this->version.'/oauth/access_token'; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function getAccessTokenResponse($code) 84 | { 85 | $response = $this->getHttpClient()->post($this->getTokenUrl(), [ 86 | RequestOptions::FORM_PARAMS => $this->getTokenFields($code), 87 | ]); 88 | 89 | $data = json_decode($response->getBody(), true); 90 | 91 | return Arr::add($data, 'expires_in', Arr::pull($data, 'expires')); 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | protected function getUserByToken($token) 98 | { 99 | $this->lastToken = $token; 100 | 101 | return $this->getUserByOIDCToken($token) ?? 102 | $this->getUserFromAccessToken($token); 103 | } 104 | 105 | /** 106 | * Get user based on the OIDC token. 107 | * 108 | * @param string $token 109 | * @return array 110 | */ 111 | protected function getUserByOIDCToken($token) 112 | { 113 | $kid = json_decode(base64_decode(explode('.', $token)[0]), true)['kid'] ?? null; 114 | 115 | if ($kid === null) { 116 | return null; 117 | } 118 | 119 | $data = (array) JWT::decode($token, $this->getPublicKeyOfOIDCToken($kid)); 120 | 121 | throw_if($data['aud'] !== $this->clientId, new Exception('Token has incorrect audience.')); 122 | throw_if($data['iss'] !== 'https://www.facebook.com', new Exception('Token has incorrect issuer.')); 123 | 124 | $data['id'] = $data['sub']; 125 | 126 | if (isset($data['given_name'])) { 127 | $data['first_name'] = $data['given_name']; 128 | } 129 | 130 | if (isset($data['family_name'])) { 131 | $data['last_name'] = $data['family_name']; 132 | } 133 | 134 | return $data; 135 | } 136 | 137 | /** 138 | * Get the public key to verify the signature of OIDC token. 139 | * 140 | * @param string $id 141 | * @return \Firebase\JWT\Key 142 | */ 143 | protected function getPublicKeyOfOIDCToken(string $kid) 144 | { 145 | $response = $this->getHttpClient()->get('https://limited.facebook.com/.well-known/oauth/openid/jwks/'); 146 | 147 | $key = Arr::first(json_decode($response->getBody()->getContents(), true)['keys'], function ($key) use ($kid) { 148 | return $key['kid'] === $kid; 149 | }); 150 | 151 | $key['n'] = new BigInteger(JWT::urlsafeB64Decode($key['n']), 256); 152 | $key['e'] = new BigInteger(JWT::urlsafeB64Decode($key['e']), 256); 153 | 154 | return new Key((string) RSA::load($key), 'RS256'); 155 | } 156 | 157 | /** 158 | * Get user based on the access token. 159 | * 160 | * @param string $token 161 | * @return array 162 | */ 163 | protected function getUserFromAccessToken($token) 164 | { 165 | $params = [ 166 | 'access_token' => $token, 167 | 'fields' => implode(',', $this->fields), 168 | ]; 169 | 170 | if (! empty($this->clientSecret)) { 171 | $params['appsecret_proof'] = hash_hmac('sha256', $token, $this->clientSecret); 172 | } 173 | 174 | $response = $this->getHttpClient()->get($this->graphUrl.'/'.$this->version.'/me', [ 175 | RequestOptions::HEADERS => [ 176 | 'Accept' => 'application/json', 177 | ], 178 | RequestOptions::QUERY => $params, 179 | ]); 180 | 181 | return json_decode($response->getBody(), true); 182 | } 183 | 184 | /** 185 | * {@inheritdoc} 186 | */ 187 | protected function mapUserToObject(array $user) 188 | { 189 | if (! isset($user['sub'])) { 190 | $avatarUrl = $this->graphUrl.'/'.$this->version.'/'.$user['id'].'/picture'; 191 | 192 | $avatarOriginalUrl = $avatarUrl.'?width=1920'; 193 | } 194 | 195 | return (new User)->setRaw($user)->map([ 196 | 'id' => $user['id'], 197 | 'nickname' => null, 198 | 'name' => $user['name'] ?? null, 199 | 'email' => $user['email'] ?? null, 200 | 'avatar' => $avatarUrl ?? $user['picture'] ?? null, 201 | 'avatar_original' => $avatarOriginalUrl ?? $user['picture'] ?? null, 202 | 'profileUrl' => $user['link'] ?? null, 203 | ]); 204 | } 205 | 206 | /** 207 | * {@inheritdoc} 208 | */ 209 | protected function getCodeFields($state = null) 210 | { 211 | $fields = parent::getCodeFields($state); 212 | 213 | if ($this->popup) { 214 | $fields['display'] = 'popup'; 215 | } 216 | 217 | if ($this->reRequest) { 218 | $fields['auth_type'] = 'rerequest'; 219 | } 220 | 221 | return $fields; 222 | } 223 | 224 | /** 225 | * Set the user fields to request from Facebook. 226 | * 227 | * @param array $fields 228 | * @return $this 229 | */ 230 | public function fields(array $fields) 231 | { 232 | $this->fields = $fields; 233 | 234 | return $this; 235 | } 236 | 237 | /** 238 | * Set the dialog to be displayed as a popup. 239 | * 240 | * @return $this 241 | */ 242 | public function asPopup() 243 | { 244 | $this->popup = true; 245 | 246 | return $this; 247 | } 248 | 249 | /** 250 | * Re-request permissions which were previously declined. 251 | * 252 | * @return $this 253 | */ 254 | public function reRequest() 255 | { 256 | $this->reRequest = true; 257 | 258 | return $this; 259 | } 260 | 261 | /** 262 | * Get the last access token used. 263 | * 264 | * @return string|null 265 | */ 266 | public function lastToken() 267 | { 268 | return $this->lastToken; 269 | } 270 | 271 | /** 272 | * Specify which graph version should be used. 273 | * 274 | * @param string $version 275 | * @return $this 276 | */ 277 | public function usingGraphVersion(string $version) 278 | { 279 | $this->version = $version; 280 | 281 | return $this; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/Two/GithubProvider.php: -------------------------------------------------------------------------------- 1 | buildAuthUrlFromBase('https://github.com/login/oauth/authorize', $state); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | protected function getTokenUrl() 30 | { 31 | return 'https://github.com/login/oauth/access_token'; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function getUserByToken($token) 38 | { 39 | $userUrl = 'https://api.github.com/user'; 40 | 41 | $response = $this->getHttpClient()->get( 42 | $userUrl, $this->getRequestOptions($token) 43 | ); 44 | 45 | $user = json_decode($response->getBody(), true); 46 | 47 | if (in_array('user:email', $this->scopes, true)) { 48 | $user['email'] = $this->getEmailByToken($token); 49 | } 50 | 51 | return $user; 52 | } 53 | 54 | /** 55 | * Get the email for the given access token. 56 | * 57 | * @param string $token 58 | * @return string|null 59 | */ 60 | protected function getEmailByToken($token) 61 | { 62 | $emailsUrl = 'https://api.github.com/user/emails'; 63 | 64 | try { 65 | $response = $this->getHttpClient()->get( 66 | $emailsUrl, $this->getRequestOptions($token) 67 | ); 68 | } catch (Exception $e) { 69 | return; 70 | } 71 | 72 | foreach (json_decode($response->getBody(), true) as $email) { 73 | if ($email['primary'] && $email['verified']) { 74 | return $email['email']; 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | protected function mapUserToObject(array $user) 83 | { 84 | return (new User)->setRaw($user)->map([ 85 | 'id' => $user['id'], 86 | 'nodeId' => $user['node_id'], 87 | 'nickname' => $user['login'], 88 | 'name' => Arr::get($user, 'name'), 89 | 'email' => Arr::get($user, 'email'), 90 | 'avatar' => $user['avatar_url'], 91 | ]); 92 | } 93 | 94 | /** 95 | * Get the default options for an HTTP request. 96 | * 97 | * @param string $token 98 | * @return array 99 | */ 100 | protected function getRequestOptions($token) 101 | { 102 | return [ 103 | RequestOptions::HEADERS => [ 104 | 'Accept' => 'application/vnd.github.v3+json', 105 | 'Authorization' => 'token '.$token, 106 | ], 107 | ]; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Two/GitlabProvider.php: -------------------------------------------------------------------------------- 1 | host = rtrim($host, '/'); 40 | } 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | protected function getAuthUrl($state) 49 | { 50 | return $this->buildAuthUrlFromBase($this->host.'/oauth/authorize', $state); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | protected function getTokenUrl() 57 | { 58 | return $this->host.'/oauth/token'; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | protected function getUserByToken($token) 65 | { 66 | $response = $this->getHttpClient()->get($this->host.'/api/v3/user', [ 67 | RequestOptions::QUERY => ['access_token' => $token], 68 | ]); 69 | 70 | return json_decode($response->getBody(), true); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | protected function mapUserToObject(array $user) 77 | { 78 | return (new User)->setRaw($user)->map([ 79 | 'id' => $user['id'], 80 | 'nickname' => $user['username'], 81 | 'name' => $user['name'], 82 | 'email' => $user['email'], 83 | 'avatar' => $user['avatar_url'], 84 | ]); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Two/GoogleProvider.php: -------------------------------------------------------------------------------- 1 | buildAuthUrlFromBase('https://accounts.google.com/o/oauth2/auth', $state); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | protected function getTokenUrl() 40 | { 41 | return 'https://www.googleapis.com/oauth2/v4/token'; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | protected function getUserByToken($token) 48 | { 49 | $response = $this->getHttpClient()->get('https://www.googleapis.com/oauth2/v3/userinfo', [ 50 | RequestOptions::QUERY => [ 51 | 'prettyPrint' => 'false', 52 | ], 53 | RequestOptions::HEADERS => [ 54 | 'Accept' => 'application/json', 55 | 'Authorization' => 'Bearer '.$token, 56 | ], 57 | ]); 58 | 59 | return json_decode((string) $response->getBody(), true); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function refreshToken($refreshToken) 66 | { 67 | $response = $this->getRefreshTokenResponse($refreshToken); 68 | 69 | return new Token( 70 | Arr::get($response, 'access_token'), 71 | Arr::get($response, 'refresh_token', $refreshToken), 72 | Arr::get($response, 'expires_in'), 73 | explode($this->scopeSeparator, Arr::get($response, 'scope', '')) 74 | ); 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | protected function mapUserToObject(array $user) 81 | { 82 | // Deprecated: Fields added to keep backwards compatibility in 4.0. These will be removed in 5.0 83 | $user['id'] = Arr::get($user, 'sub'); 84 | $user['verified_email'] = Arr::get($user, 'email_verified'); 85 | $user['link'] = Arr::get($user, 'profile'); 86 | 87 | return (new User)->setRaw($user)->map([ 88 | 'id' => Arr::get($user, 'sub'), 89 | 'nickname' => Arr::get($user, 'nickname'), 90 | 'name' => Arr::get($user, 'name'), 91 | 'email' => Arr::get($user, 'email'), 92 | 'avatar' => $avatarUrl = Arr::get($user, 'picture'), 93 | 'avatar_original' => $avatarUrl, 94 | ]); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Two/InvalidStateException.php: -------------------------------------------------------------------------------- 1 | buildAuthUrlFromBase('https://www.linkedin.com/oauth/v2/authorization', $state); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function getTokenUrl() 35 | { 36 | return 'https://www.linkedin.com/oauth/v2/accessToken'; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function getUserByToken($token) 43 | { 44 | return $this->getBasicProfile($token); 45 | } 46 | 47 | /** 48 | * Get the basic profile fields for the user. 49 | * 50 | * @param string $token 51 | * @return array 52 | */ 53 | protected function getBasicProfile($token) 54 | { 55 | $response = $this->getHttpClient()->get('https://api.linkedin.com/v2/userinfo', [ 56 | RequestOptions::HEADERS => [ 57 | 'Authorization' => 'Bearer '.$token, 58 | 'X-RestLi-Protocol-Version' => '2.0.0', 59 | ], 60 | RequestOptions::QUERY => [ 61 | 'projection' => '(sub,email,email_verified,name,given_name,family_name,picture)', 62 | ], 63 | ]); 64 | 65 | return (array) json_decode($response->getBody(), true); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | protected function mapUserToObject(array $user) 72 | { 73 | return (new User)->setRaw($user)->map([ 74 | 'id' => $user['sub'], 75 | 'nickname' => null, 76 | 'name' => $user['name'], 77 | 'first_name' => $user['given_name'], 78 | 'last_name' => $user['family_name'], 79 | 'email' => $user['email'] ?? null, 80 | 'email_verified' => $user['email_verified'] ?? null, 81 | 'avatar' => $user['picture'] ?? null, 82 | 'avatar_original' => $user['picture'] ?? null, 83 | ]); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Two/LinkedInProvider.php: -------------------------------------------------------------------------------- 1 | buildAuthUrlFromBase('https://www.linkedin.com/oauth/v2/authorization', $state); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function getTokenUrl() 36 | { 37 | return 'https://www.linkedin.com/oauth/v2/accessToken'; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function getUserByToken($token) 44 | { 45 | $basicProfile = $this->getBasicProfile($token); 46 | $emailAddress = $this->getEmailAddress($token); 47 | 48 | return array_merge($basicProfile, $emailAddress); 49 | } 50 | 51 | /** 52 | * Get the basic profile fields for the user. 53 | * 54 | * @param string $token 55 | * @return array 56 | */ 57 | protected function getBasicProfile($token) 58 | { 59 | $fields = ['id', 'firstName', 'lastName', 'profilePicture(displayImage~:playableStreams)']; 60 | 61 | if (in_array('r_liteprofile', $this->getScopes())) { 62 | array_push($fields, 'vanityName'); 63 | } 64 | 65 | $response = $this->getHttpClient()->get('https://api.linkedin.com/v2/me', [ 66 | RequestOptions::HEADERS => [ 67 | 'Authorization' => 'Bearer '.$token, 68 | 'X-RestLi-Protocol-Version' => '2.0.0', 69 | ], 70 | RequestOptions::QUERY => [ 71 | 'projection' => '('.implode(',', $fields).')', 72 | ], 73 | ]); 74 | 75 | return (array) json_decode($response->getBody(), true); 76 | } 77 | 78 | /** 79 | * Get the email address for the user. 80 | * 81 | * @param string $token 82 | * @return array 83 | */ 84 | protected function getEmailAddress($token) 85 | { 86 | $response = $this->getHttpClient()->get('https://api.linkedin.com/v2/emailAddress', [ 87 | RequestOptions::HEADERS => [ 88 | 'Authorization' => 'Bearer '.$token, 89 | 'X-RestLi-Protocol-Version' => '2.0.0', 90 | ], 91 | RequestOptions::QUERY => [ 92 | 'q' => 'members', 93 | 'projection' => '(elements*(handle~))', 94 | ], 95 | ]); 96 | 97 | return (array) Arr::get((array) json_decode($response->getBody(), true), 'elements.0.handle~'); 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | protected function mapUserToObject(array $user) 104 | { 105 | $preferredLocale = Arr::get($user, 'firstName.preferredLocale.language').'_'.Arr::get($user, 'firstName.preferredLocale.country'); 106 | $firstName = Arr::get($user, 'firstName.localized.'.$preferredLocale); 107 | $lastName = Arr::get($user, 'lastName.localized.'.$preferredLocale); 108 | 109 | $images = (array) Arr::get($user, 'profilePicture.displayImage~.elements', []); 110 | $avatar = Arr::first($images, function ($image) { 111 | return ( 112 | $image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] ?? 113 | $image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['displaySize']['width'] 114 | ) === 100; 115 | }); 116 | $originalAvatar = Arr::first($images, function ($image) { 117 | return ( 118 | $image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] ?? 119 | $image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['displaySize']['width'] 120 | ) === 800; 121 | }); 122 | 123 | return (new User)->setRaw($user)->map([ 124 | 'id' => $user['id'], 125 | 'nickname' => null, 126 | 'name' => $firstName.' '.$lastName, 127 | 'first_name' => $firstName, 128 | 'last_name' => $lastName, 129 | 'email' => Arr::get($user, 'emailAddress'), 130 | 'avatar' => Arr::get($avatar, 'identifiers.0.identifier'), 131 | 'avatar_original' => Arr::get($originalAvatar, 'identifiers.0.identifier'), 132 | ]); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Two/ProviderInterface.php: -------------------------------------------------------------------------------- 1 | buildAuthUrlFromBase('https://slack.com/openid/connect/authorize', $state); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function getTokenUrl() 36 | { 37 | return 'https://slack.com/api/openid.connect.token'; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function getUserByToken($token) 44 | { 45 | $response = $this->getHttpClient()->get('https://slack.com/api/openid.connect.userInfo', [ 46 | RequestOptions::HEADERS => ['Authorization' => 'Bearer '.$token], 47 | ]); 48 | 49 | return json_decode($response->getBody(), true); 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | protected function mapUserToObject(array $user) 56 | { 57 | return (new User)->setRaw($user)->map([ 58 | 'id' => Arr::get($user, 'sub'), 59 | 'nickname' => null, 60 | 'name' => Arr::get($user, 'name'), 61 | 'email' => Arr::get($user, 'email'), 62 | 'avatar' => Arr::get($user, 'picture'), 63 | 'organization_id' => Arr::get($user, 'https://slack.com/team_id'), 64 | ]); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Two/SlackProvider.php: -------------------------------------------------------------------------------- 1 | scopeKey = 'scope'; 32 | 33 | return $this; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function getAuthUrl($state) 40 | { 41 | return $this->buildAuthUrlFromBase('https://slack.com/oauth/v2/authorize', $state); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | protected function getTokenUrl() 48 | { 49 | return 'https://slack.com/api/oauth.v2.access'; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | protected function getUserByToken($token) 56 | { 57 | $response = $this->getHttpClient()->get('https://slack.com/api/users.identity', [ 58 | RequestOptions::HEADERS => ['Authorization' => 'Bearer '.$token], 59 | ]); 60 | 61 | return json_decode($response->getBody(), true); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | protected function mapUserToObject(array $user) 68 | { 69 | return (new User)->setRaw($user)->map([ 70 | 'id' => Arr::get($user, 'user.id'), 71 | 'name' => Arr::get($user, 'user.name'), 72 | 'email' => Arr::get($user, 'user.email'), 73 | 'avatar' => Arr::get($user, 'user.image_512'), 74 | 'organization_id' => Arr::get($user, 'team.id'), 75 | ]); 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | protected function getCodeFields($state = null) 82 | { 83 | $fields = parent::getCodeFields($state); 84 | 85 | if ($this->scopeKey === 'user_scope') { 86 | $fields['scope'] = ''; 87 | $fields['user_scope'] = $this->formatScopes($this->scopes, $this->scopeSeparator); 88 | } 89 | 90 | return $fields; 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | */ 96 | public function getAccessTokenResponse($code) 97 | { 98 | $response = $this->getHttpClient()->post($this->getTokenUrl(), [ 99 | RequestOptions::HEADERS => $this->getTokenHeaders($code), 100 | RequestOptions::FORM_PARAMS => $this->getTokenFields($code), 101 | ]); 102 | 103 | $result = json_decode($response->getBody(), true); 104 | 105 | if ($this->scopeKey === 'user_scope') { 106 | return $result['authed_user']; 107 | } 108 | 109 | return $result; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Two/Token.php: -------------------------------------------------------------------------------- 1 | token = $token; 46 | $this->refreshToken = $refreshToken; 47 | $this->expiresIn = $expiresIn; 48 | $this->approvedScopes = $approvedScopes; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Two/TwitchProvider.php: -------------------------------------------------------------------------------- 1 | buildAuthUrlFromBase('https://id.twitch.tv/oauth2/authorize', $state); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function getTokenUrl() 36 | { 37 | return 'https://id.twitch.tv/oauth2/token'; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function getUserByToken($token) 44 | { 45 | $response = $this->getHttpClient()->get( 46 | 'https://api.twitch.tv/helix/users', 47 | [ 48 | RequestOptions::HEADERS => [ 49 | 'Accept' => 'application/json', 50 | 'Authorization' => 'Bearer '.$token, 51 | 'Client-ID' => $this->clientId, 52 | ], 53 | ] 54 | ); 55 | 56 | return json_decode((string) $response->getBody(), true); 57 | } 58 | 59 | /** 60 | * Create a user instance from the given data. 61 | * 62 | * @param array $response 63 | * @param array $user 64 | * @return \Laravel\Socialite\Two\User 65 | */ 66 | protected function userInstance(array $response, array $user) 67 | { 68 | $this->user = $this->mapUserToObject($user); 69 | 70 | $scopes = Arr::get($response, 'scope', []); 71 | 72 | if (! is_array($scopes)) { 73 | $scopes = explode($this->scopeSeparator, $scopes); 74 | } 75 | 76 | return $this->user->setToken(Arr::get($response, 'access_token')) 77 | ->setRefreshToken(Arr::get($response, 'refresh_token')) 78 | ->setExpiresIn(Arr::get($response, 'expires_in')) 79 | ->setApprovedScopes($scopes); 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | protected function mapUserToObject(array $user) 86 | { 87 | $user = $user['data']['0']; 88 | 89 | return (new User)->setRaw($user)->map([ 90 | 'id' => $user['id'], 91 | 'nickname' => $user['display_name'], 92 | 'name' => $user['display_name'], 93 | 'email' => Arr::get($user, 'email'), 94 | 'avatar' => $user['profile_image_url'], 95 | ]); 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function refreshToken($refreshToken) 102 | { 103 | $response = $this->getRefreshTokenResponse($refreshToken); 104 | 105 | return new Token( 106 | Arr::get($response, 'access_token'), 107 | Arr::get($response, 'refresh_token'), 108 | Arr::get($response, 'expires_in'), 109 | Arr::get($response, 'scope', []) 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Two/TwitterProvider.php: -------------------------------------------------------------------------------- 1 | buildAuthUrlFromBase('https://twitter.com/i/oauth2/authorize', $state); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | protected function getTokenUrl() 50 | { 51 | return 'https://api.twitter.com/2/oauth2/token'; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | protected function getUserByToken($token) 58 | { 59 | $response = $this->getHttpClient()->get('https://api.twitter.com/2/users/me', [ 60 | RequestOptions::HEADERS => ['Authorization' => 'Bearer '.$token], 61 | RequestOptions::QUERY => ['user.fields' => 'profile_image_url,confirmed_email'], 62 | ]); 63 | 64 | return Arr::get(json_decode($response->getBody(), true), 'data'); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | protected function mapUserToObject(array $user) 71 | { 72 | return (new User)->setRaw($user)->map([ 73 | 'id' => $user['id'], 74 | 'email' => $user['confirmed_email'] ?? null, 75 | 'nickname' => $user['username'], 76 | 'name' => $user['name'], 77 | 'avatar' => $user['profile_image_url'], 78 | ]); 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function getAccessTokenResponse($code) 85 | { 86 | $response = $this->getHttpClient()->post($this->getTokenUrl(), [ 87 | RequestOptions::HEADERS => ['Accept' => 'application/json'], 88 | RequestOptions::AUTH => [$this->clientId, $this->clientSecret], 89 | RequestOptions::FORM_PARAMS => $this->getTokenFields($code), 90 | ]); 91 | 92 | return json_decode($response->getBody(), true); 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | protected function getRefreshTokenResponse($refreshToken) 99 | { 100 | $response = $this->getHttpClient()->post($this->getTokenUrl(), [ 101 | RequestOptions::HEADERS => ['Accept' => 'application/json'], 102 | RequestOptions::AUTH => [$this->clientId, $this->clientSecret], 103 | RequestOptions::FORM_PARAMS => [ 104 | 'grant_type' => 'refresh_token', 105 | 'refresh_token' => $refreshToken, 106 | 'client_id' => $this->clientId, 107 | ], 108 | ]); 109 | 110 | return json_decode($response->getBody(), true); 111 | } 112 | 113 | /** 114 | * {@inheritdoc} 115 | */ 116 | protected function getCodeFields($state = null) 117 | { 118 | $fields = parent::getCodeFields($state); 119 | 120 | if ($this->isStateless()) { 121 | $fields['state'] = 'state'; 122 | } 123 | 124 | return $fields; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Two/User.php: -------------------------------------------------------------------------------- 1 | token = $token; 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * Set the refresh token required to obtain a new access token. 52 | * 53 | * @param string $refreshToken 54 | * @return $this 55 | */ 56 | public function setRefreshToken($refreshToken) 57 | { 58 | $this->refreshToken = $refreshToken; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Set the number of seconds the access token is valid for. 65 | * 66 | * @param int $expiresIn 67 | * @return $this 68 | */ 69 | public function setExpiresIn($expiresIn) 70 | { 71 | $this->expiresIn = $expiresIn; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Set the scopes that were approved by the user during authentication. 78 | * 79 | * @param array $approvedScopes 80 | * @return $this 81 | */ 82 | public function setApprovedScopes($approvedScopes) 83 | { 84 | $this->approvedScopes = $approvedScopes; 85 | 86 | return $this; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Two/XProvider.php: -------------------------------------------------------------------------------- 1 | buildAuthUrlFromBase('https://x.com/i/oauth2/authorize', $state); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | protected function getTokenUrl() 22 | { 23 | return 'https://api.x.com/2/oauth2/token'; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | protected function getUserByToken($token) 30 | { 31 | $response = $this->getHttpClient()->get('https://api.x.com/2/users/me', [ 32 | RequestOptions::HEADERS => ['Authorization' => 'Bearer '.$token], 33 | RequestOptions::QUERY => ['user.fields' => 'profile_image_url,confirmed_email'], 34 | ]); 35 | 36 | return Arr::get(json_decode($response->getBody(), true), 'data'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function mapUserToObject(array $user) 43 | { 44 | $user = parent::mapUserToObject($user); 45 | 46 | $user->email = $user['confirmed_email']; 47 | 48 | return $user; 49 | } 50 | } 51 | --------------------------------------------------------------------------------