Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / auth / provider / PhabricatorOAuth1AuthProvider.php
blobef1991e8d7e2492b1e70ecaf2b620459cdd7176a
1 <?php
3 abstract class PhabricatorOAuth1AuthProvider
4 extends PhabricatorOAuthAuthProvider {
6 protected $adapter;
8 const PROPERTY_CONSUMER_KEY = 'oauth1:consumer:key';
9 const PROPERTY_CONSUMER_SECRET = 'oauth1:consumer:secret';
10 const PROPERTY_PRIVATE_KEY = 'oauth1:private:key';
12 protected function getIDKey() {
13 return self::PROPERTY_CONSUMER_KEY;
16 protected function getSecretKey() {
17 return self::PROPERTY_CONSUMER_SECRET;
20 protected function configureAdapter(PhutilOAuth1AuthAdapter $adapter) {
21 $config = $this->getProviderConfig();
22 $adapter->setConsumerKey($config->getProperty(self::PROPERTY_CONSUMER_KEY));
23 $secret = $config->getProperty(self::PROPERTY_CONSUMER_SECRET);
24 if (strlen($secret)) {
25 $adapter->setConsumerSecret(new PhutilOpaqueEnvelope($secret));
27 $adapter->setCallbackURI(PhabricatorEnv::getURI($this->getLoginURI()));
28 return $adapter;
31 protected function renderLoginForm(AphrontRequest $request, $mode) {
32 $attributes = array(
33 'method' => 'POST',
34 'uri' => $this->getLoginURI(),
36 return $this->renderStandardLoginButton($request, $mode, $attributes);
39 public function processLoginRequest(
40 PhabricatorAuthLoginController $controller) {
42 $request = $controller->getRequest();
43 $adapter = $this->getAdapter();
44 $account = null;
45 $response = null;
47 if ($request->isHTTPPost()) {
48 // Add a CSRF code to the callback URI, which we'll verify when
49 // performing the login.
51 $client_code = $this->getAuthCSRFCode($request);
53 $callback_uri = $adapter->getCallbackURI();
54 $callback_uri = $callback_uri.$client_code.'/';
55 $adapter->setCallbackURI($callback_uri);
57 $uri = $adapter->getClientRedirectURI();
59 $this->saveHandshakeTokenSecret(
60 $client_code,
61 $adapter->getTokenSecret());
63 $response = id(new AphrontRedirectResponse())
64 ->setIsExternal(true)
65 ->setURI($uri);
66 return array($account, $response);
69 $denied = $request->getStr('denied');
70 if (strlen($denied)) {
71 // Twitter indicates that the user cancelled the login attempt by
72 // returning "denied" as a parameter.
73 throw new PhutilAuthUserAbortedException();
76 // NOTE: You can get here via GET, this should probably be a bit more
77 // user friendly.
79 $this->verifyAuthCSRFCode($request, $controller->getExtraURIData());
81 $token = $request->getStr('oauth_token');
82 $verifier = $request->getStr('oauth_verifier');
84 if (!$token) {
85 throw new Exception(pht("Expected '%s' in request!", 'oauth_token'));
88 if (!$verifier) {
89 throw new Exception(pht("Expected '%s' in request!", 'oauth_verifier'));
92 $adapter->setToken($token);
93 $adapter->setVerifier($verifier);
95 $client_code = $this->getAuthCSRFCode($request);
96 $token_secret = $this->loadHandshakeTokenSecret($client_code);
97 $adapter->setTokenSecret($token_secret);
99 // NOTE: As a side effect, this will cause the OAuth adapter to request
100 // an access token.
102 try {
103 $identifiers = $adapter->getAccountIdentifiers();
104 } catch (Exception $ex) {
105 // TODO: Handle this in a more user-friendly way.
106 throw $ex;
109 if (!$identifiers) {
110 $response = $controller->buildProviderErrorResponse(
111 $this,
112 pht(
113 'The OAuth provider failed to retrieve an account ID.'));
115 return array($account, $response);
118 $account = $this->newExternalAccountForIdentifiers($identifiers);
120 return array($account, $response);
123 public function processEditForm(
124 AphrontRequest $request,
125 array $values) {
127 $key_ckey = self::PROPERTY_CONSUMER_KEY;
128 $key_csecret = self::PROPERTY_CONSUMER_SECRET;
130 return $this->processOAuthEditForm(
131 $request,
132 $values,
133 pht('Consumer key is required.'),
134 pht('Consumer secret is required.'));
137 public function extendEditForm(
138 AphrontRequest $request,
139 AphrontFormView $form,
140 array $values,
141 array $issues) {
143 return $this->extendOAuthEditForm(
144 $request,
145 $form,
146 $values,
147 $issues,
148 pht('OAuth Consumer Key'),
149 pht('OAuth Consumer Secret'));
152 public function renderConfigPropertyTransactionTitle(
153 PhabricatorAuthProviderConfigTransaction $xaction) {
155 $author_phid = $xaction->getAuthorPHID();
156 $old = $xaction->getOldValue();
157 $new = $xaction->getNewValue();
158 $key = $xaction->getMetadataValue(
159 PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
161 switch ($key) {
162 case self::PROPERTY_CONSUMER_KEY:
163 if (strlen($old)) {
164 return pht(
165 '%s updated the OAuth consumer key for this provider from '.
166 '"%s" to "%s".',
167 $xaction->renderHandleLink($author_phid),
168 $old,
169 $new);
170 } else {
171 return pht(
172 '%s set the OAuth consumer key for this provider to '.
173 '"%s".',
174 $xaction->renderHandleLink($author_phid),
175 $new);
177 case self::PROPERTY_CONSUMER_SECRET:
178 if (strlen($old)) {
179 return pht(
180 '%s updated the OAuth consumer secret for this provider.',
181 $xaction->renderHandleLink($author_phid));
182 } else {
183 return pht(
184 '%s set the OAuth consumer secret for this provider.',
185 $xaction->renderHandleLink($author_phid));
189 return parent::renderConfigPropertyTransactionTitle($xaction);
192 protected function synchronizeOAuthAccount(
193 PhabricatorExternalAccount $account) {
194 $adapter = $this->getAdapter();
196 $oauth_token = $adapter->getToken();
197 $oauth_token_secret = $adapter->getTokenSecret();
199 $account->setProperty('oauth1.token', $oauth_token);
200 $account->setProperty('oauth1.token.secret', $oauth_token_secret);
203 public function willRenderLinkedAccount(
204 PhabricatorUser $viewer,
205 PHUIObjectItemView $item,
206 PhabricatorExternalAccount $account) {
208 $item->addAttribute(pht('OAuth1 Account'));
210 parent::willRenderLinkedAccount($viewer, $item, $account);
213 protected function getContentSecurityPolicyFormActions() {
214 return $this->getAdapter()->getContentSecurityPolicyFormActions();
217 /* -( Temporary Secrets )-------------------------------------------------- */
220 private function saveHandshakeTokenSecret($client_code, $secret) {
221 $secret_type = PhabricatorOAuth1SecretTemporaryTokenType::TOKENTYPE;
222 $key = $this->getHandshakeTokenKeyFromClientCode($client_code);
223 $type = $this->getTemporaryTokenType($secret_type);
225 // Wipe out an existing token, if one exists.
226 $token = id(new PhabricatorAuthTemporaryTokenQuery())
227 ->setViewer(PhabricatorUser::getOmnipotentUser())
228 ->withTokenResources(array($key))
229 ->withTokenTypes(array($type))
230 ->executeOne();
231 if ($token) {
232 $token->delete();
235 // Save the new secret.
236 id(new PhabricatorAuthTemporaryToken())
237 ->setTokenResource($key)
238 ->setTokenType($type)
239 ->setTokenExpires(time() + phutil_units('1 hour in seconds'))
240 ->setTokenCode($secret)
241 ->save();
244 private function loadHandshakeTokenSecret($client_code) {
245 $secret_type = PhabricatorOAuth1SecretTemporaryTokenType::TOKENTYPE;
246 $key = $this->getHandshakeTokenKeyFromClientCode($client_code);
247 $type = $this->getTemporaryTokenType($secret_type);
249 $token = id(new PhabricatorAuthTemporaryTokenQuery())
250 ->setViewer(PhabricatorUser::getOmnipotentUser())
251 ->withTokenResources(array($key))
252 ->withTokenTypes(array($type))
253 ->withExpired(false)
254 ->executeOne();
256 if (!$token) {
257 throw new Exception(
258 pht(
259 'Unable to load your OAuth1 token secret from storage. It may '.
260 'have expired. Try authenticating again.'));
263 return $token->getTokenCode();
266 private function getTemporaryTokenType($core_type) {
267 // Namespace the type so that multiple providers don't step on each
268 // others' toes if a user starts Mediawiki and Bitbucket auth at the
269 // same time.
271 // TODO: This isn't really a proper use of the table and should get
272 // cleaned up some day: the type should be constant.
274 return $core_type.':'.$this->getProviderConfig()->getID();
277 private function getHandshakeTokenKeyFromClientCode($client_code) {
278 // NOTE: This is very slightly coercive since the TemporaryToken table
279 // expects an "objectPHID" as an identifier, but nothing about the storage
280 // is bound to PHIDs.
282 return 'oauth1:secret/'.$client_code;