@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
3final class PhabricatorAuthLoginController
4 extends PhabricatorAuthController {
5
6 private $providerKey;
7 private $extraURIData;
8 private $provider;
9
10 public function shouldRequireLogin() {
11 return false;
12 }
13
14 public function shouldAllowRestrictedParameter($parameter_name) {
15 // Whitelist the OAuth 'code' parameter.
16
17 if ($parameter_name == 'code') {
18 return true;
19 }
20
21 return parent::shouldAllowRestrictedParameter($parameter_name);
22 }
23
24 public function getExtraURIData() {
25 return $this->extraURIData;
26 }
27
28 public function handleRequest(AphrontRequest $request) {
29 $viewer = $this->getViewer();
30 $this->providerKey = $request->getURIData('pkey');
31 $this->extraURIData = $request->getURIData('extra');
32
33 $response = $this->loadProvider();
34 if ($response) {
35 return $response;
36 }
37
38 $invite = $this->loadInvite();
39 $provider = $this->provider;
40
41 try {
42 list($account, $response) = $provider->processLoginRequest($this);
43 } catch (PhutilAuthUserAbortedException $ex) {
44 if ($viewer->isLoggedIn()) {
45 // If a logged-in user cancels, take them back to the external accounts
46 // panel.
47 $next_uri = '/settings/panel/external/';
48 } else {
49 // If a logged-out user cancels, take them back to the auth start page.
50 $next_uri = '/';
51 }
52
53 // User explicitly hit "Cancel".
54 $dialog = id(new AphrontDialogView())
55 ->setUser($viewer)
56 ->setTitle(pht('Authentication Canceled'))
57 ->appendChild(
58 pht('You canceled authentication.'))
59 ->addCancelButton($next_uri, pht('Continue'));
60 return id(new AphrontDialogResponse())->setDialog($dialog);
61 }
62
63 if ($response) {
64 return $response;
65 }
66
67 if (!$account) {
68 throw new Exception(
69 pht(
70 'Auth provider failed to load an account from %s!',
71 'processLoginRequest()'));
72 }
73
74 if ($account->getUserPHID()) {
75 // The account is already attached to a Phabricator user, so this is
76 // either a login or a bad account link request.
77 if (!$viewer->isLoggedIn()) {
78 if ($provider->shouldAllowLogin()) {
79 return $this->processLoginUser($account);
80 } else {
81 return $this->renderError(
82 pht(
83 'The external service ("%s") you just authenticated with is '.
84 'not configured to allow logins on this server. An '.
85 'administrator may have recently disabled it.',
86 $provider->getProviderName()));
87 }
88 } else if ($viewer->getPHID() == $account->getUserPHID()) {
89 // This is either an attempt to re-link an existing and already
90 // linked account (which is silly) or a refresh of an external account
91 // (e.g., an OAuth account).
92 return id(new AphrontRedirectResponse())
93 ->setURI('/settings/panel/external/');
94 } else {
95 return $this->renderError(
96 pht(
97 'The external service ("%s") you just used to log in is already '.
98 'associated with another %s user account. Log in to the '.
99 'other %s account and unlink the external account before '.
100 'linking it to a new %s account.',
101 $provider->getProviderName(),
102 PlatformSymbols::getPlatformServerName(),
103 PlatformSymbols::getPlatformServerName(),
104 PlatformSymbols::getPlatformServerName()));
105 }
106 } else {
107 // The account is not yet attached to a Phabricator user, so this is
108 // either a registration or an account link request.
109 if (!$viewer->isLoggedIn()) {
110 if ($provider->shouldAllowRegistration() || $invite) {
111 return $this->processRegisterUser($account);
112 } else {
113 return $this->renderError(
114 pht(
115 'The external service ("%s") you just authenticated with is '.
116 'not configured to allow registration on this server. An '.
117 'administrator may have recently disabled it.',
118 $provider->getProviderName()));
119 }
120 } else {
121
122 // If the user already has a linked account on this provider, prevent
123 // them from linking a second account. This can happen if they swap
124 // logins and then refresh the account link.
125
126 // There's no technical reason we can't allow you to link multiple
127 // accounts from a single provider; disallowing this is currently a
128 // product deciison. See T2549.
129
130 $existing_accounts = id(new PhabricatorExternalAccountQuery())
131 ->setViewer($viewer)
132 ->withUserPHIDs(array($viewer->getPHID()))
133 ->withProviderConfigPHIDs(
134 array(
135 $provider->getProviderConfigPHID(),
136 ))
137 ->execute();
138 if ($existing_accounts) {
139 return $this->renderError(
140 pht(
141 'Your %s account is already connected to an external '.
142 'account on this service ("%s"), but you are currently logged '.
143 'in to the service with a different account. Log out of the '.
144 'external service, then log back in with the correct account '.
145 'before refreshing the account link.',
146 PlatformSymbols::getPlatformServerName(),
147 $provider->getProviderName()));
148 }
149
150 if ($provider->shouldAllowAccountLink()) {
151 return $this->processLinkUser($account);
152 } else {
153 return $this->renderError(
154 pht(
155 'The external service ("%s") you just authenticated with is '.
156 'not configured to allow account linking on this server. An '.
157 'administrator may have recently disabled it.',
158 $provider->getProviderName()));
159 }
160 }
161 }
162
163 // This should be unreachable, but fail explicitly if we get here somehow.
164 return new Aphront400Response();
165 }
166
167 private function processLoginUser(PhabricatorExternalAccount $account) {
168 $user = id(new PhabricatorUser())->loadOneWhere(
169 'phid = %s',
170 $account->getUserPHID());
171
172 if (!$user) {
173 return $this->renderError(
174 pht(
175 'The external account you just logged in with is not associated '.
176 'with a valid %s user account.',
177 PlatformSymbols::getPlatformServerName()));
178 }
179
180 return $this->loginUser($user);
181 }
182
183 private function processRegisterUser(PhabricatorExternalAccount $account) {
184 $account_secret = $account->getAccountSecret();
185 $register_uri = $this->getApplicationURI('register/'.$account_secret.'/');
186 return $this->setAccountKeyAndContinue($account, $register_uri);
187 }
188
189 private function processLinkUser(PhabricatorExternalAccount $account) {
190 $account_secret = $account->getAccountSecret();
191 $confirm_uri = $this->getApplicationURI('confirmlink/'.$account_secret.'/');
192 return $this->setAccountKeyAndContinue($account, $confirm_uri);
193 }
194
195 private function setAccountKeyAndContinue(
196 PhabricatorExternalAccount $account,
197 $next_uri) {
198
199 if ($account->getUserPHID()) {
200 throw new Exception(pht('Account is already registered or linked.'));
201 }
202
203 // Regenerate the registration secret key, set it on the external account,
204 // set a cookie on the user's machine, and redirect them to registration.
205 // See PhabricatorAuthRegisterController for discussion of the registration
206 // key.
207
208 $registration_key = Filesystem::readRandomCharacters(32);
209 $account->setProperty(
210 'registrationKey',
211 PhabricatorHash::weakDigest($registration_key));
212
213 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
214 $account->save();
215 unset($unguarded);
216
217 $this->getRequest()->setTemporaryCookie(
218 PhabricatorCookies::COOKIE_REGISTRATION,
219 $registration_key);
220
221 return id(new AphrontRedirectResponse())->setURI($next_uri);
222 }
223
224 private function loadProvider() {
225 $provider = PhabricatorAuthProvider::getEnabledProviderByKey(
226 $this->providerKey);
227
228 if (!$provider) {
229 return $this->renderError(
230 pht(
231 'The account you are attempting to log in with uses a nonexistent '.
232 'or disabled authentication provider (with key "%s"). An '.
233 'administrator may have recently disabled this provider.',
234 $this->providerKey));
235 }
236
237 $this->provider = $provider;
238
239 return null;
240 }
241
242 protected function renderError($message) {
243 return $this->renderErrorPage(
244 pht('Login Failed'),
245 array($message));
246 }
247
248 public function buildProviderPageResponse(
249 PhabricatorAuthProvider $provider,
250 $content) {
251
252 $crumbs = $this->buildApplicationCrumbs();
253 $viewer = $this->getViewer();
254
255 if ($viewer->isLoggedIn()) {
256 $crumbs->addTextCrumb(pht('Link Account'), $provider->getSettingsURI());
257 } else {
258 $crumbs->addTextCrumb(pht('Login'), $this->getApplicationURI('start/'));
259
260 $content = array(
261 $this->newCustomStartMessage(),
262 $content,
263 );
264 }
265
266 $crumbs->addTextCrumb($provider->getProviderName());
267 $crumbs->setBorder(true);
268
269 return $this->newPage()
270 ->setTitle(pht('Login'))
271 ->setCrumbs($crumbs)
272 ->appendChild($content);
273 }
274
275 public function buildProviderErrorResponse(
276 PhabricatorAuthProvider $provider,
277 $message) {
278
279 $message = pht(
280 'Authentication provider ("%s") encountered an error while attempting '.
281 'to log in. %s', $provider->getProviderName(), $message);
282
283 return $this->renderError($message);
284 }
285
286}