Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / auth / provider / PhabricatorOAuth2AuthProvider.php
blob19e85ae7bc49e4623407eb6a2db3445e73442291
1 <?php
3 abstract class PhabricatorOAuth2AuthProvider
4 extends PhabricatorOAuthAuthProvider {
6 const PROPERTY_APP_ID = 'oauth:app:id';
7 const PROPERTY_APP_SECRET = 'oauth:app:secret';
9 protected function getIDKey() {
10 return self::PROPERTY_APP_ID;
13 protected function getSecretKey() {
14 return self::PROPERTY_APP_SECRET;
18 protected function configureAdapter(PhutilOAuthAuthAdapter $adapter) {
19 $config = $this->getProviderConfig();
20 $adapter->setClientID($config->getProperty(self::PROPERTY_APP_ID));
21 $adapter->setClientSecret(
22 new PhutilOpaqueEnvelope(
23 $config->getProperty(self::PROPERTY_APP_SECRET)));
24 $adapter->setRedirectURI(PhabricatorEnv::getURI($this->getLoginURI()));
25 return $adapter;
28 protected function renderLoginForm(AphrontRequest $request, $mode) {
29 $adapter = $this->getAdapter();
30 $adapter->setState($this->getAuthCSRFCode($request));
32 $scope = $request->getStr('scope');
33 if ($scope) {
34 $adapter->setScope($scope);
37 $attributes = array(
38 'method' => 'GET',
39 'uri' => $adapter->getAuthenticateURI(),
42 return $this->renderStandardLoginButton($request, $mode, $attributes);
45 public function processLoginRequest(
46 PhabricatorAuthLoginController $controller) {
48 $request = $controller->getRequest();
49 $adapter = $this->getAdapter();
50 $account = null;
51 $response = null;
53 $error = $request->getStr('error');
54 if ($error) {
55 $response = $controller->buildProviderErrorResponse(
56 $this,
57 pht(
58 'The OAuth provider returned an error: %s',
59 $error));
61 return array($account, $response);
64 $this->verifyAuthCSRFCode($request, $request->getStr('state'));
66 $code = $request->getStr('code');
67 if (!strlen($code)) {
68 $response = $controller->buildProviderErrorResponse(
69 $this,
70 pht(
71 'The OAuth provider did not return a "code" parameter in its '.
72 'response.'));
74 return array($account, $response);
77 $adapter->setCode($code);
79 // NOTE: As a side effect, this will cause the OAuth adapter to request
80 // an access token.
82 try {
83 $identifiers = $adapter->getAccountIdentifiers();
84 } catch (Exception $ex) {
85 // TODO: Handle this in a more user-friendly way.
86 throw $ex;
89 if (!$identifiers) {
90 $response = $controller->buildProviderErrorResponse(
91 $this,
92 pht(
93 'The OAuth provider failed to retrieve an account ID.'));
95 return array($account, $response);
98 $account = $this->newExternalAccountForIdentifiers($identifiers);
100 return array($account, $response);
103 public function processEditForm(
104 AphrontRequest $request,
105 array $values) {
107 return $this->processOAuthEditForm(
108 $request,
109 $values,
110 pht('Application ID is required.'),
111 pht('Application secret is required.'));
114 public function extendEditForm(
115 AphrontRequest $request,
116 AphrontFormView $form,
117 array $values,
118 array $issues) {
120 return $this->extendOAuthEditForm(
121 $request,
122 $form,
123 $values,
124 $issues,
125 pht('OAuth App ID'),
126 pht('OAuth App Secret'));
129 public function renderConfigPropertyTransactionTitle(
130 PhabricatorAuthProviderConfigTransaction $xaction) {
132 $author_phid = $xaction->getAuthorPHID();
133 $old = $xaction->getOldValue();
134 $new = $xaction->getNewValue();
135 $key = $xaction->getMetadataValue(
136 PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
138 switch ($key) {
139 case self::PROPERTY_APP_ID:
140 if (strlen($old)) {
141 return pht(
142 '%s updated the OAuth application ID for this provider from '.
143 '"%s" to "%s".',
144 $xaction->renderHandleLink($author_phid),
145 $old,
146 $new);
147 } else {
148 return pht(
149 '%s set the OAuth application ID for this provider to '.
150 '"%s".',
151 $xaction->renderHandleLink($author_phid),
152 $new);
154 case self::PROPERTY_APP_SECRET:
155 if (strlen($old)) {
156 return pht(
157 '%s updated the OAuth application secret for this provider.',
158 $xaction->renderHandleLink($author_phid));
159 } else {
160 return pht(
161 '%s set the OAuth application secret for this provider.',
162 $xaction->renderHandleLink($author_phid));
164 case self::PROPERTY_NOTE:
165 if (strlen($old)) {
166 return pht(
167 '%s updated the OAuth application notes for this provider.',
168 $xaction->renderHandleLink($author_phid));
169 } else {
170 return pht(
171 '%s set the OAuth application notes for this provider.',
172 $xaction->renderHandleLink($author_phid));
177 return parent::renderConfigPropertyTransactionTitle($xaction);
180 protected function synchronizeOAuthAccount(
181 PhabricatorExternalAccount $account) {
182 $adapter = $this->getAdapter();
184 $oauth_token = $adapter->getAccessToken();
185 $account->setProperty('oauth.token.access', $oauth_token);
187 if ($adapter->supportsTokenRefresh()) {
188 $refresh_token = $adapter->getRefreshToken();
189 $account->setProperty('oauth.token.refresh', $refresh_token);
190 } else {
191 $account->setProperty('oauth.token.refresh', null);
194 $expires = $adapter->getAccessTokenExpires();
195 $account->setProperty('oauth.token.access.expires', $expires);
198 public function getOAuthAccessToken(
199 PhabricatorExternalAccount $account,
200 $force_refresh = false) {
202 if ($account->getProviderConfigPHID() !== $this->getProviderConfigPHID()) {
203 throw new Exception(pht('Account does not match provider!'));
206 if (!$force_refresh) {
207 $access_expires = $account->getProperty('oauth.token.access.expires');
208 $access_token = $account->getProperty('oauth.token.access');
210 // Don't return a token with fewer than this many seconds remaining until
211 // it expires.
212 $shortest_token = 60;
213 if ($access_token) {
214 if ($access_expires === null ||
215 $access_expires > (time() + $shortest_token)) {
216 return $access_token;
221 $refresh_token = $account->getProperty('oauth.token.refresh');
222 if ($refresh_token) {
223 $adapter = $this->getAdapter();
224 if ($adapter->supportsTokenRefresh()) {
225 $adapter->refreshAccessToken($refresh_token);
227 $this->synchronizeOAuthAccount($account);
228 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
229 $account->save();
230 unset($unguarded);
232 return $account->getProperty('oauth.token.access');
236 return null;
239 public function willRenderLinkedAccount(
240 PhabricatorUser $viewer,
241 PHUIObjectItemView $item,
242 PhabricatorExternalAccount $account) {
244 // Get a valid token, possibly refreshing it. If we're unable to refresh
245 // it, render a message to that effect. The user may be able to repair the
246 // link by manually reconnecting.
248 $is_invalid = false;
249 try {
250 $oauth_token = $this->getOAuthAccessToken($account);
251 } catch (Exception $ex) {
252 $oauth_token = null;
253 $is_invalid = true;
256 $item->addAttribute(pht('OAuth2 Account'));
258 if ($oauth_token) {
259 $oauth_expires = $account->getProperty('oauth.token.access.expires');
260 if ($oauth_expires) {
261 $item->addAttribute(
262 pht(
263 'Active OAuth Token (Expires: %s)',
264 phabricator_datetime($oauth_expires, $viewer)));
265 } else {
266 $item->addAttribute(
267 pht('Active OAuth Token'));
269 } else if ($is_invalid) {
270 $item->addAttribute(pht('Invalid OAuth Access Token'));
271 } else {
272 $item->addAttribute(pht('No OAuth Access Token'));
275 parent::willRenderLinkedAccount($viewer, $item, $account);
278 public function supportsAutoLogin() {
279 return true;
282 public function getAutoLoginURI(AphrontRequest $request) {
283 $csrf_code = $this->getAuthCSRFCode($request);
285 $adapter = $this->getAdapter();
286 $adapter->setState($csrf_code);
288 return $adapter->getAuthenticateURI();