From 922f82a26fb7243d7e7ff2ec8ba7e957e7b9eeb7 Mon Sep 17 00:00:00 2001 From: strd0x <61651824+strd0x@users.noreply.github.com> Date: Tue, 29 Mar 2022 00:38:40 +0200 Subject: [PATCH] [Steam] Authorization domain spoof check (#817) Co-authored-by: Lucas Michot Co-authored-by: atymic --- OpenIDValidationException.php | 9 + Provider.php | 328 ++++++++++++++++++++++++++++++++++ README.md | 62 +++++++ SteamExtendSocialite.php | 18 ++ composer.json | 33 ++++ 5 files changed, 450 insertions(+) create mode 100644 OpenIDValidationException.php create mode 100644 Provider.php create mode 100644 README.md create mode 100644 SteamExtendSocialite.php create mode 100644 composer.json diff --git a/OpenIDValidationException.php b/OpenIDValidationException.php new file mode 100644 index 0000000..6b204f2 --- /dev/null +++ b/OpenIDValidationException.php @@ -0,0 +1,9 @@ +buildUrl(); + } + + /** + * {@inheritdoc} + */ + public function user() + { + if (!$this->validate()) { + $error = $this->getParams()['openid.error'] ?? 'unknown error'; + + throw new OpenIDValidationException('Failed to validate OpenID login: '.$error); + } + + return $this->mapUserToObject($this->getUserByToken($this->steamId)); + } + + /** + * {@inheritdoc} + */ + protected function parseAccessToken($body) + { + return null; + } + + /** + * {@inheritdoc} + */ + protected function getUserByToken($token) + { + if (is_null($token)) { + return null; + } + + if (empty($this->clientSecret)) { + throw new RuntimeException('The Steam API key has not been specified.'); + } + + $response = $this->getHttpClient()->request( + 'GET', + sprintf(self::STEAM_INFO_URL, $this->clientSecret, $token) + ); + + $contents = json_decode((string) $response->getBody(), true); + + return Arr::get($contents, 'response.players.0'); + } + + /** + * {@inheritdoc} + */ + protected function mapUserToObject(array $user) + { + return (new User())->setRaw($user)->map([ + 'id' => $user['steamid'], + 'nickname' => Arr::get($user, 'personaname'), + 'name' => Arr::get($user, 'realname'), + 'email' => null, + 'avatar' => Arr::get($user, 'avatarmedium'), + ]); + } + + /** + * Build the Steam login URL. + * + * @return string + */ + private function buildUrl() + { + $realm = $this->getConfig('realm', $this->request->server('HTTP_HOST')); + + $params = [ + 'openid.ns' => self::OPENID_NS, + 'openid.mode' => 'checkid_setup', + 'openid.return_to' => $this->redirectUrl, + 'openid.realm' => sprintf('%s://%s', $this->request->getScheme(), $realm), + 'openid.identity' => 'http://specs.openid.net/auth/2.0/identifier_select', + 'openid.claimed_id' => 'http://specs.openid.net/auth/2.0/identifier_select', + ]; + + return self::OPENID_URL.'?'.http_build_query($params, '', '&'); + } + + /** + * Checks the steam login. + * + * @throws \SocialiteProviders\Steam\OpenIDValidationException + * + * @return bool + */ + public function validate() + { + if (!$this->requestIsValid()) { + return false; + } + + if (!$this->validateHost($this->request->get('openid_return_to'))) { + throw new OpenIDValidationException('Invalid return_to host'); + } + + $requestOptions = $this->getDefaultRequestOptions(); + $customOptions = $this->getCustomRequestOptions(); + + if (!empty($customOptions) && is_array($customOptions)) { + $requestOptions = array_merge($requestOptions, $customOptions); + } + + $response = $this->getHttpClient()->request('POST', self::OPENID_URL, $requestOptions); + + $results = $this->parseResults((string) $response->getBody()); + + $isValid = $results['is_valid'] === 'true'; + + if ($isValid) { + $this->parseSteamID(); + } + + return $isValid; + } + + /** + * Validates if the request object has required stream attributes. + * + * @return bool + */ + private function requestIsValid() + { + return $this->request->has(self::OPENID_ASSOC_HANDLE) + && $this->request->has(self::OPENID_SIGNED) + && $this->request->has(self::OPENID_SIG); + } + + /** + * @return array + */ + public function getDefaultRequestOptions() + { + return [ + RequestOptions::FORM_PARAMS => $this->getParams(), + RequestOptions::PROXY => $this->getConfig('proxy'), + ]; + } + + /** + * @return array + */ + public function getCustomRequestOptions() + { + return $this->customRequestOptions; + } + + /** + * Get param list for openId validation. + * + * @return array + */ + public function getParams() + { + $params = [ + 'openid.assoc_handle' => $this->request->get(self::OPENID_ASSOC_HANDLE), + 'openid.signed' => $this->request->get(self::OPENID_SIGNED), + 'openid.sig' => $this->request->get(self::OPENID_SIG), + 'openid.ns' => self::OPENID_NS, + 'openid.mode' => 'check_authentication', + 'openid.error' => $this->request->get(self::OPENID_ERROR), + ]; + + $signedParams = explode(',', $this->request->get(self::OPENID_SIGNED)); + + foreach ($signedParams as $item) { + $value = $this->request->get('openid_'.str_replace('.', '_', $item)); + $params['openid.'.$item] = $value; + } + + return $params; + } + + /** + * Parse openID response to an array. + * + * @param string $results openid response body + * + * @return array + */ + public function parseResults($results) + { + $parsed = []; + $lines = explode("\n", $results); + + foreach ($lines as $line) { + if (empty($line)) { + continue; + } + + $line = explode(':', $line, 2); + $parsed[$line[0]] = $line[1]; + } + + return $parsed; + } + + /** + * Parse the steamID from the OpenID response. + * + * @return void + */ + public function parseSteamID() + { + preg_match( + '#^https?://steamcommunity.com/openid/id/([0-9]{17,25})#', + $this->request->get('openid_claimed_id'), + $matches + ); + + $this->steamId = isset($matches[1]) && is_numeric($matches[1]) ? $matches[1] : 0; + } + + /** + * {@inheritdoc} + */ + public function getAccessTokenResponse($code) + { + } + + /** + * {@inheritdoc} + */ + protected function getTokenUrl() + { + } + + /** + * {@inheritdoc} + */ + public static function additionalConfigKeys() + { + return ['realm', 'proxy', 'allowed_hosts']; + } + + /** + * Validation of the domain available for authorization. + * + * @return bool + */ + protected function validateHost(string $url): bool + { + $allowedHosts = $this->getConfig('allowed_hosts', []); + + return count($allowedHosts) === 0 || in_array(parse_url($url, PHP_URL_HOST), $allowedHosts, true); + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9c6388 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Steam + +```bash +composer require socialiteproviders/steam +``` + +## Installation & Basic Usage + +Please see the [Base Installation Guide](https://socialiteproviders.com/usage/), then follow the provider specific instructions below. + +## Add configuration to `config/services.php` + +```php +'steam' => [ + 'client_id' => null, + 'client_secret' => env('STEAM_CLIENT_SECRET'), + 'redirect' => env('STEAM_REDIRECT_URI'), + 'allowed_hosts' => [ + 'example.com', + ] +], +``` + +### allowed_hosts +Set this for protect against authorization domain spoofing. When the user returns from the Steam login page, along with the OpenID validation, the return_to parameter will be checked against the available domains in `allowed_hosts`. + +If you don't specify the setting, then fraudsters have the opportunity to enter the application under other users + +Issue resolved in https://github.com/SocialiteProviders/Providers/pull/817 + +By default this protection is disabled. It will only be active when allowed hosts is not equal to an empty array. + + +## Add provider event listener + +Configure the package's listener to listen for `SocialiteWasCalled` events. + +Add the event to your `listen[]` array in `app/Providers/EventServiceProvider`. See the [Base Installation Guide](https://socialiteproviders.com/usage/) for detailed instructions. + +```php +protected $listen = [ + \SocialiteProviders\Manager\SocialiteWasCalled::class => [ + // ... other providers + \SocialiteProviders\Steam\SteamExtendSocialite::class.'@handle', + ], +]; +``` + +## Usage + +You should now be able to use the provider like you would regularly use Socialite (assuming you have the facade installed): + +```php +return Socialite::driver('steam')->redirect(); +``` + +## Returned User fields + +- ``id`` +- ``nickname`` +- ``name`` +- ``avatar`` diff --git a/SteamExtendSocialite.php b/SteamExtendSocialite.php new file mode 100644 index 0000000..360aa00 --- /dev/null +++ b/SteamExtendSocialite.php @@ -0,0 +1,18 @@ +extendSocialite('steam', Provider::class); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..83229d7 --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name": "socialiteproviders/steam", + "description": "Steam OpenID Provider for Laravel Socialite", + "keywords": [ + "laravel", + "openid", + "provider", + "socialite", + "steam" + ], + "license": "MIT", + "authors": [ + { + "name": "Christopher Eklund", + "email": "eklundchristopher@gmail.com" + } + ], + "require": { + "php": "^7.2 || ^8.0", + "ext-json": "*", + "socialiteproviders/manager": "~4.0" + }, + "autoload": { + "psr-4": { + "SocialiteProviders\\Steam\\": "" + } + }, + "support": { + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers", + "docs": "https://socialiteproviders.com/steam" + } +}