@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
at recaptime-dev/main 228 lines 5.7 kB view raw
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}