friendship ended with social-app. php is my new best friend
1<?php 2/** 3 * Class Steam 4 * 5 * @created 15.03.2021 6 * @author smiley <smiley@chillerlan.net> 7 * @copyright 2021 smiley 8 * @license MIT 9 */ 10declare(strict_types=1); 11 12namespace chillerlan\OAuth\Providers; 13 14use chillerlan\HTTP\Utils\{MessageUtil, QueryUtil, UriUtil}; 15use chillerlan\OAuth\Core\{AccessToken, AuthenticatedUser, OAuthProvider, UserInfo}; 16use Psr\Http\Message\{RequestInterface, ResponseInterface, UriInterface}; 17use function explode, intval, str_replace; 18 19/** 20 * Steam OpenID 21 * 22 * @link https://steamcommunity.com/dev 23 * @link https://partner.steamgames.com/doc/webapi_overview 24 * @link https://partner.steamgames.com/doc/features/auth 25 * @link https://steamwebapi.azurewebsites.net/ 26 */ 27class Steam extends OAuthProvider implements UserInfo{ 28 29 public const IDENTIFIER = 'STEAM'; 30 31 protected string $authorizationURL = 'https://steamcommunity.com/openid/login'; 32 protected string $accessTokenURL = 'https://steamcommunity.com/openid/login'; 33 protected string $apiURL = 'https://api.steampowered.com'; 34 protected string|null $applicationURL = 'https://steamcommunity.com/dev/apikey'; 35 protected string|null $apiDocs = 'https://developer.valvesoftware.com/wiki/Steam_Web_API'; 36 37 /** 38 * we ignore user supplied params here 39 * 40 * @inheritDoc 41 * 42 * @param array<string, string>|null $params 43 * @param string[]|null $scopes 44 */ 45 public function getAuthorizationURL(array|null $params = null, array|null $scopes = null):UriInterface{ 46 47 $params = [ 48 'openid.ns' => 'http://specs.openid.net/auth/2.0', 49 'openid.mode' => 'checkid_setup', 50 'openid.return_to' => $this->options->callbackURL, 51 'openid.realm' => $this->options->key, 52 'openid.identity' => 'http://specs.openid.net/auth/2.0/identifier_select', 53 'openid.claimed_id' => 'http://specs.openid.net/auth/2.0/identifier_select', 54 ]; 55 56 return $this->uriFactory->createUri(QueryUtil::merge($this->authorizationURL, $params)); 57 } 58 59 /** 60 * Obtains an "authentication token" (the steamID64) 61 * 62 * @param array<string, string> $urlQuery 63 */ 64 public function getAccessToken(array $urlQuery):AccessToken{ 65 $body = $this->getAccessTokenRequestBodyParams($urlQuery); 66 $response = $this->sendAccessTokenRequest($this->accessTokenURL, $body); 67 $token = $this->parseTokenResponse($response, $urlQuery['openid_claimed_id']); 68 69 $this->storage->storeAccessToken($token, $this->name); 70 71 return $token; 72 } 73 74 /** 75 * prepares the request body parameters for the access token request 76 * 77 * @param array<string, string> $received 78 * @return array<string, string> 79 */ 80 protected function getAccessTokenRequestBodyParams(array $received):array{ 81 82 $body = [ 83 'openid.mode' => 'check_authentication', 84 'openid.ns' => 'http://specs.openid.net/auth/2.0', 85 'openid.sig' => $received['openid_sig'], 86 ]; 87 88 foreach(explode(',', $received['openid_signed']) as $item){ 89 $body['openid.'.$item] = $received['openid_'.$item]; 90 } 91 92 return $body; 93 } 94 95 /** 96 * sends a request to the access token endpoint $url with the given $params as URL query 97 * 98 * @param array<string, string> $body 99 */ 100 protected function sendAccessTokenRequest(string $url, array $body):ResponseInterface{ 101 102 $request = $this->requestFactory 103 ->createRequest('POST', $url) 104 ->withHeader('Content-Type', 'application/x-www-form-urlencoded') 105 ->withBody($this->streamFactory->createStream(QueryUtil::build($body))); 106 107 return $this->http->sendRequest($request); 108 } 109 110 /** 111 * @throws \chillerlan\OAuth\Providers\ProviderException 112 */ 113 protected function parseTokenResponse(ResponseInterface $response, string $claimed_id):AccessToken{ 114 $data = explode("\x0a", MessageUtil::getContents($response)); 115 116 if(!isset($data[1]) || !str_starts_with($data[1], 'is_valid')){ 117 throw new ProviderException('unable to parse token response'); 118 } 119 120 if($data[1] !== 'is_valid:true'){ 121 throw new ProviderException('invalid id'); 122 } 123 124 $token = $this->createAccessToken(); 125 $id = str_replace('https://steamcommunity.com/openid/id/', '', $claimed_id); 126 127 // as this method is intended for one-time authentication only we'll not receive a token. 128 // instead we're gonna save the verified steam user id as token as it is required 129 // for several "authenticated" endpoints. 130 $token->accessToken = $id; 131 $token->expires = AccessToken::NEVER_EXPIRES; 132 $token->extraParams = [ 133 'claimed_id' => $claimed_id, 134 'id_int' => intval($id), 135 ]; 136 137 return $token; 138 } 139 140 public function getRequestAuthorization(RequestInterface $request, AccessToken|null $token = null):RequestInterface{ 141 $uri = UriUtil::withQueryValue($request->getUri(), 'key', $this->options->secret); 142 143 return $request->withUri($uri); 144 } 145 146 /** @codeCoverageIgnore */ 147 public function me():AuthenticatedUser{ 148 $token = $this->storage->getAccessToken($this->name); 149 $json = $this->getMeResponseData('/ISteamUser/GetPlayerSummaries/v0002/', ['steamids' => $token->accessToken]); 150 151 if(!isset($json['response']['players'][0])){ 152 throw new ProviderException('invalid response'); 153 } 154 155 $data = $json['response']['players'][0]; 156 157 $userdata = [ 158 'data' => $data, 159 'avatar' => $data['avatarfull'], 160 'displayName' => $data['personaname'], 161 'id' => $data['steamid'], 162 'url' => $data['profileurl'], 163 ]; 164 165 return new AuthenticatedUser($userdata); 166 } 167 168}