@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * Abstract adapter for OAuth2 providers.
5 */
6abstract class PhutilOAuthAuthAdapter extends PhutilAuthAdapter {
7
8 private $clientID;
9 private $clientSecret;
10 private $redirectURI;
11 private $scope;
12 private $state;
13 private $code;
14
15 private $accessTokenData;
16 private $oauthAccountData;
17
18 abstract protected function getAuthenticateBaseURI();
19 abstract protected function getTokenBaseURI();
20 abstract protected function loadOAuthAccountData();
21
22 public function getAuthenticateURI() {
23 $params = array(
24 'client_id' => $this->getClientID(),
25 'scope' => $this->getScope(),
26 'redirect_uri' => $this->getRedirectURI(),
27 'state' => $this->getState(),
28 ) + $this->getExtraAuthenticateParameters();
29
30 $uri = new PhutilURI($this->getAuthenticateBaseURI(), $params);
31
32 return phutil_string_cast($uri);
33 }
34
35 public function getAdapterType() {
36 $this_class = get_class($this);
37 $type_name = str_replace('PhutilAuthAdapterOAuth', '', $this_class);
38 return strtolower($type_name);
39 }
40
41 public function setState($state) {
42 $this->state = $state;
43 return $this;
44 }
45
46 public function getState() {
47 return $this->state;
48 }
49
50 public function setCode($code) {
51 $this->code = $code;
52 return $this;
53 }
54
55 public function getCode() {
56 return $this->code;
57 }
58
59 public function setRedirectURI($redirect_uri) {
60 $this->redirectURI = $redirect_uri;
61 return $this;
62 }
63
64 public function getRedirectURI() {
65 return $this->redirectURI;
66 }
67
68 public function getExtraAuthenticateParameters() {
69 return array();
70 }
71
72 public function getExtraTokenParameters() {
73 return array();
74 }
75
76 public function getExtraRefreshParameters() {
77 return array();
78 }
79
80 public function setScope($scope) {
81 $this->scope = $scope;
82 return $this;
83 }
84
85 public function getScope() {
86 return $this->scope;
87 }
88
89 public function setClientSecret(PhutilOpaqueEnvelope $client_secret) {
90 $this->clientSecret = $client_secret;
91 return $this;
92 }
93
94 public function getClientSecret() {
95 return $this->clientSecret;
96 }
97
98 public function setClientID($client_id) {
99 $this->clientID = $client_id;
100 return $this;
101 }
102
103 public function getClientID() {
104 return $this->clientID;
105 }
106
107 public function getAccessToken() {
108 return $this->getAccessTokenData('access_token');
109 }
110
111 public function getAccessTokenExpires() {
112 return $this->getAccessTokenData('expires_epoch');
113 }
114
115 public function getRefreshToken() {
116 return $this->getAccessTokenData('refresh_token');
117 }
118
119 protected function getAccessTokenData($key, $default = null) {
120 if ($this->accessTokenData === null) {
121 $this->accessTokenData = $this->loadAccessTokenData();
122 }
123
124 return idx($this->accessTokenData, $key, $default);
125 }
126
127 public function supportsTokenRefresh() {
128 return false;
129 }
130
131 public function refreshAccessToken($refresh_token) {
132 $this->accessTokenData = $this->loadRefreshTokenData($refresh_token);
133 return $this;
134 }
135
136 protected function loadRefreshTokenData($refresh_token) {
137 $params = array(
138 'refresh_token' => $refresh_token,
139 ) + $this->getExtraRefreshParameters();
140
141 // NOTE: Make sure we return the refresh_token so that subsequent
142 // calls to getRefreshToken() return it; providers normally do not echo
143 // it back for token refresh requests.
144
145 return $this->makeTokenRequest($params) + array(
146 'refresh_token' => $refresh_token,
147 );
148 }
149
150 protected function loadAccessTokenData() {
151 $code = $this->getCode();
152 if (!$code) {
153 throw new PhutilInvalidStateException('setCode');
154 }
155
156 $params = array(
157 'code' => $this->getCode(),
158 ) + $this->getExtraTokenParameters();
159
160 return $this->makeTokenRequest($params);
161 }
162
163 private function makeTokenRequest(array $params) {
164 $uri = $this->getTokenBaseURI();
165 $query_data = array(
166 'client_id' => $this->getClientID(),
167 'client_secret' => $this->getClientSecret()->openEnvelope(),
168 'redirect_uri' => $this->getRedirectURI(),
169 ) + $params;
170
171 $future = new HTTPSFuture($uri, $query_data);
172 $future->setMethod('POST');
173 list($body) = $future->resolvex();
174
175 $data = $this->readAccessTokenResponse($body);
176
177 if (isset($data['expires_in'])) {
178 $data['expires_epoch'] = $data['expires_in'];
179 } else if (isset($data['expires'])) {
180 $data['expires_epoch'] = $data['expires'];
181 }
182
183 // If we got some "expires" value back, interpret it as an epoch timestamp
184 // if it's after the year 2010 and as a relative number of seconds
185 // otherwise.
186 if (isset($data['expires_epoch'])) {
187 if ($data['expires_epoch'] < (60 * 60 * 24 * 365 * 40)) {
188 $data['expires_epoch'] += time();
189 }
190 }
191
192 if (isset($data['error'])) {
193 throw new Exception(pht('Access token error: %s', $data['error']));
194 }
195
196 return $data;
197 }
198
199 protected function readAccessTokenResponse($body) {
200 // NOTE: Most providers either return JSON or HTTP query strings, so try
201 // both mechanisms. If your provider does something else, override this
202 // method.
203
204 $data = json_decode($body, true);
205
206 if (!is_array($data)) {
207 $data = array();
208 parse_str($body, $data);
209 }
210
211 if (empty($data['access_token']) &&
212 empty($data['error'])) {
213 throw new Exception(
214 pht('Failed to decode OAuth access token response: %s', $body));
215 }
216
217 return $data;
218 }
219
220 protected function getOAuthAccountData($key, $default = null) {
221 if ($this->oauthAccountData === null) {
222 $this->oauthAccountData = $this->loadOAuthAccountData();
223 }
224
225 return idx($this->oauthAccountData, $key, $default);
226 }
227
228}