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}