Remove all "FileHasObject" edge reads and writes
[phabricator.git] / src / applications / auth / adapter / PhutilOAuthAuthAdapter.php
blob47d299ee368edd743cd86869249856d37d83c9d2
1 <?php
3 /**
4 * Abstract adapter for OAuth2 providers.
5 */
6 abstract class PhutilOAuthAuthAdapter extends PhutilAuthAdapter {
8 private $clientID;
9 private $clientSecret;
10 private $redirectURI;
11 private $scope;
12 private $state;
13 private $code;
15 private $accessTokenData;
16 private $oauthAccountData;
18 abstract protected function getAuthenticateBaseURI();
19 abstract protected function getTokenBaseURI();
20 abstract protected function loadOAuthAccountData();
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();
30 $uri = new PhutilURI($this->getAuthenticateBaseURI(), $params);
32 return phutil_string_cast($uri);
35 public function getAdapterType() {
36 $this_class = get_class($this);
37 $type_name = str_replace('PhutilAuthAdapterOAuth', '', $this_class);
38 return strtolower($type_name);
41 public function setState($state) {
42 $this->state = $state;
43 return $this;
46 public function getState() {
47 return $this->state;
50 public function setCode($code) {
51 $this->code = $code;
52 return $this;
55 public function getCode() {
56 return $this->code;
59 public function setRedirectURI($redirect_uri) {
60 $this->redirectURI = $redirect_uri;
61 return $this;
64 public function getRedirectURI() {
65 return $this->redirectURI;
68 public function getExtraAuthenticateParameters() {
69 return array();
72 public function getExtraTokenParameters() {
73 return array();
76 public function getExtraRefreshParameters() {
77 return array();
80 public function setScope($scope) {
81 $this->scope = $scope;
82 return $this;
85 public function getScope() {
86 return $this->scope;
89 public function setClientSecret(PhutilOpaqueEnvelope $client_secret) {
90 $this->clientSecret = $client_secret;
91 return $this;
94 public function getClientSecret() {
95 return $this->clientSecret;
98 public function setClientID($client_id) {
99 $this->clientID = $client_id;
100 return $this;
103 public function getClientID() {
104 return $this->clientID;
107 public function getAccessToken() {
108 return $this->getAccessTokenData('access_token');
111 public function getAccessTokenExpires() {
112 return $this->getAccessTokenData('expires_epoch');
115 public function getRefreshToken() {
116 return $this->getAccessTokenData('refresh_token');
119 protected function getAccessTokenData($key, $default = null) {
120 if ($this->accessTokenData === null) {
121 $this->accessTokenData = $this->loadAccessTokenData();
124 return idx($this->accessTokenData, $key, $default);
127 public function supportsTokenRefresh() {
128 return false;
131 public function refreshAccessToken($refresh_token) {
132 $this->accessTokenData = $this->loadRefreshTokenData($refresh_token);
133 return $this;
136 protected function loadRefreshTokenData($refresh_token) {
137 $params = array(
138 'refresh_token' => $refresh_token,
139 ) + $this->getExtraRefreshParameters();
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.
145 return $this->makeTokenRequest($params) + array(
146 'refresh_token' => $refresh_token,
150 protected function loadAccessTokenData() {
151 $code = $this->getCode();
152 if (!$code) {
153 throw new PhutilInvalidStateException('setCode');
156 $params = array(
157 'code' => $this->getCode(),
158 ) + $this->getExtraTokenParameters();
160 return $this->makeTokenRequest($params);
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;
171 $future = new HTTPSFuture($uri, $query_data);
172 $future->setMethod('POST');
173 list($body) = $future->resolvex();
175 $data = $this->readAccessTokenResponse($body);
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'];
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();
192 if (isset($data['error'])) {
193 throw new Exception(pht('Access token error: %s', $data['error']));
196 return $data;
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.
204 $data = json_decode($body, true);
206 if (!is_array($data)) {
207 $data = array();
208 parse_str($body, $data);
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));
217 return $data;
220 protected function getOAuthAccountData($key, $default = null) {
221 if ($this->oauthAccountData === null) {
222 $this->oauthAccountData = $this->loadOAuthAccountData();
225 return idx($this->oauthAccountData, $key, $default);