@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
1<?php
2
3abstract class PhabricatorOAuth1AuthProvider
4 extends PhabricatorOAuthAuthProvider {
5
6 protected $adapter;
7
8 const PROPERTY_CONSUMER_KEY = 'oauth1:consumer:key';
9 const PROPERTY_CONSUMER_SECRET = 'oauth1:consumer:secret';
10 const PROPERTY_PRIVATE_KEY = 'oauth1:private:key';
11
12 protected function getIDKey() {
13 return self::PROPERTY_CONSUMER_KEY;
14 }
15
16 protected function getSecretKey() {
17 return self::PROPERTY_CONSUMER_SECRET;
18 }
19
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 (phutil_nonempty_string($secret)) {
25 $adapter->setConsumerSecret(new PhutilOpaqueEnvelope($secret));
26 }
27 $adapter->setCallbackURI(PhabricatorEnv::getURI($this->getLoginURI()));
28 return $adapter;
29 }
30
31 protected function renderLoginForm(AphrontRequest $request, $mode) {
32 $attributes = array(
33 'method' => 'POST',
34 'uri' => $this->getLoginURI(),
35 );
36 return $this->renderStandardLoginButton($request, $mode, $attributes);
37 }
38
39 public function processLoginRequest(
40 PhabricatorAuthLoginController $controller) {
41
42 $request = $controller->getRequest();
43 $adapter = $this->getAdapter();
44 $account = null;
45 $response = null;
46
47 if ($request->isHTTPPost()) {
48 // Add a CSRF code to the callback URI, which we'll verify when
49 // performing the login.
50
51 $client_code = $this->getAuthCSRFCode($request);
52
53 $callback_uri = $adapter->getCallbackURI();
54 $callback_uri = $callback_uri.$client_code.'/';
55 $adapter->setCallbackURI($callback_uri);
56
57 $uri = $adapter->getClientRedirectURI();
58
59 $this->saveHandshakeTokenSecret(
60 $client_code,
61 $adapter->getTokenSecret());
62
63 $response = id(new AphrontRedirectResponse())
64 ->setIsExternal(true)
65 ->setURI($uri);
66 return array($account, $response);
67 }
68
69 $denied = $request->getStr('denied');
70 if ($denied) {
71 // Twitter indicates that the user cancelled the login attempt by
72 // returning "denied" as a parameter.
73 throw new PhutilAuthUserAbortedException();
74 }
75
76 // NOTE: You can get here via GET, this should probably be a bit more
77 // user friendly.
78
79 $this->verifyAuthCSRFCode($request, $controller->getExtraURIData());
80
81 $token = $request->getStr('oauth_token');
82 $verifier = $request->getStr('oauth_verifier');
83
84 if (!$token) {
85 throw new Exception(pht("Expected '%s' in request!", 'oauth_token'));
86 }
87
88 if (!$verifier) {
89 throw new Exception(pht("Expected '%s' in request!", 'oauth_verifier'));
90 }
91
92 $adapter->setToken($token);
93 $adapter->setVerifier($verifier);
94
95 $client_code = $this->getAuthCSRFCode($request);
96 $token_secret = $this->loadHandshakeTokenSecret($client_code);
97 $adapter->setTokenSecret($token_secret);
98
99 // NOTE: As a side effect, this will cause the OAuth adapter to request
100 // an access token.
101
102 try {
103 $identifiers = $adapter->getAccountIdentifiers();
104 } catch (Exception $ex) {
105 // TODO: Handle this in a more user-friendly way.
106 throw $ex;
107 }
108
109 if (!$identifiers) {
110 $response = $controller->buildProviderErrorResponse(
111 $this,
112 pht(
113 'The OAuth provider failed to retrieve an account ID.'));
114
115 return array($account, $response);
116 }
117
118 $account = $this->newExternalAccountForIdentifiers($identifiers);
119
120 return array($account, $response);
121 }
122
123 public function processEditForm(
124 AphrontRequest $request,
125 array $values) {
126
127 $key_ckey = self::PROPERTY_CONSUMER_KEY;
128 $key_csecret = self::PROPERTY_CONSUMER_SECRET;
129
130 return $this->processOAuthEditForm(
131 $request,
132 $values,
133 pht('Consumer key is required.'),
134 pht('Consumer secret is required.'));
135 }
136
137 public function extendEditForm(
138 AphrontRequest $request,
139 AphrontFormView $form,
140 array $values,
141 array $issues) {
142
143 return $this->extendOAuthEditForm(
144 $request,
145 $form,
146 $values,
147 $issues,
148 pht('OAuth Consumer Key'),
149 pht('OAuth Consumer Secret'));
150 }
151
152 public function renderConfigPropertyTransactionTitle(
153 PhabricatorAuthProviderConfigTransaction $xaction) {
154
155 $author_phid = $xaction->getAuthorPHID();
156 $old = $xaction->getOldValue();
157 $new = $xaction->getNewValue();
158 $key = $xaction->getMetadataValue(
159 PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);
160
161 switch ($key) {
162 case self::PROPERTY_CONSUMER_KEY:
163 if (phutil_nonempty_string($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);
176 }
177 case self::PROPERTY_CONSUMER_SECRET:
178 if (phutil_nonempty_string($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));
186 }
187 }
188
189 return parent::renderConfigPropertyTransactionTitle($xaction);
190 }
191
192 protected function synchronizeOAuthAccount(
193 PhabricatorExternalAccount $account) {
194 $adapter = $this->getAdapter();
195
196 $oauth_token = $adapter->getToken();
197 $oauth_token_secret = $adapter->getTokenSecret();
198
199 $account->setProperty('oauth1.token', $oauth_token);
200 $account->setProperty('oauth1.token.secret', $oauth_token_secret);
201 }
202
203 public function willRenderLinkedAccount(
204 PhabricatorUser $viewer,
205 PHUIObjectItemView $item,
206 PhabricatorExternalAccount $account) {
207
208 $item->addAttribute(pht('OAuth1 Account'));
209
210 parent::willRenderLinkedAccount($viewer, $item, $account);
211 }
212
213 protected function getContentSecurityPolicyFormActions() {
214 return $this->getAdapter()->getContentSecurityPolicyFormActions();
215 }
216
217/* -( Temporary Secrets )-------------------------------------------------- */
218
219
220 private function saveHandshakeTokenSecret($client_code, $secret) {
221 $secret_type = PhabricatorOAuth1SecretTemporaryTokenType::TOKENTYPE;
222 $key = $this->getHandshakeTokenKeyFromClientCode($client_code);
223 $type = $this->getTemporaryTokenType($secret_type);
224
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();
233 }
234
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();
242 }
243
244 private function loadHandshakeTokenSecret($client_code) {
245 $secret_type = PhabricatorOAuth1SecretTemporaryTokenType::TOKENTYPE;
246 $key = $this->getHandshakeTokenKeyFromClientCode($client_code);
247 $type = $this->getTemporaryTokenType($secret_type);
248
249 $token = id(new PhabricatorAuthTemporaryTokenQuery())
250 ->setViewer(PhabricatorUser::getOmnipotentUser())
251 ->withTokenResources(array($key))
252 ->withTokenTypes(array($type))
253 ->withExpired(false)
254 ->executeOne();
255
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.'));
261 }
262
263 return $token->getTokenCode();
264 }
265
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.
270
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.
273
274 return $core_type.':'.$this->getProviderConfig()->getID();
275 }
276
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.
281
282 return 'oauth1:secret/'.$client_code;
283 }
284
285}