4 * Abstract adapter for OAuth2 providers.
6 abstract class PhutilOAuthAuthAdapter
extends PhutilAuthAdapter
{
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() {
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;
46 public function getState() {
50 public function setCode($code) {
55 public function getCode() {
59 public function setRedirectURI($redirect_uri) {
60 $this->redirectURI
= $redirect_uri;
64 public function getRedirectURI() {
65 return $this->redirectURI
;
68 public function getExtraAuthenticateParameters() {
72 public function getExtraTokenParameters() {
76 public function getExtraRefreshParameters() {
80 public function setScope($scope) {
81 $this->scope
= $scope;
85 public function getScope() {
89 public function setClientSecret(PhutilOpaqueEnvelope
$client_secret) {
90 $this->clientSecret
= $client_secret;
94 public function getClientSecret() {
95 return $this->clientSecret
;
98 public function setClientID($client_id) {
99 $this->clientID
= $client_id;
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() {
131 public function refreshAccessToken($refresh_token) {
132 $this->accessTokenData
= $this->loadRefreshTokenData($refresh_token);
136 protected function loadRefreshTokenData($refresh_token) {
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();
153 throw new PhutilInvalidStateException('setCode');
157 'code' => $this->getCode(),
158 ) +
$this->getExtraTokenParameters();
160 return $this->makeTokenRequest($params);
163 private function makeTokenRequest(array $params) {
164 $uri = $this->getTokenBaseURI();
166 'client_id' => $this->getClientID(),
167 'client_secret' => $this->getClientSecret()->openEnvelope(),
168 'redirect_uri' => $this->getRedirectURI(),
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
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']));
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
204 $data = json_decode($body, true);
206 if (!is_array($data)) {
208 parse_str($body, $data);
211 if (empty($data['access_token']) &&
212 empty($data['error'])) {
214 pht('Failed to decode OAuth access token response: %s', $body));
220 protected function getOAuthAccountData($key, $default = null) {
221 if ($this->oauthAccountData
=== null) {
222 $this->oauthAccountData
= $this->loadOAuthAccountData();
225 return idx($this->oauthAccountData
, $key, $default);