@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 PhabricatorAuthController extends PhabricatorController {
4
5 /**
6 * @return PhabricatorStandardPageView PhabricatorStandardPageView with a
7 * @{class:PHUIInfoView} child displaying error messages.
8 */
9 protected function renderErrorPage($title, array $messages) {
10 $view = new PHUIInfoView();
11 $view->setTitle($title);
12 $view->setErrors($messages);
13
14 return $this->newPage()
15 ->setTitle($title)
16 ->appendChild($view);
17
18 }
19
20 /**
21 * Returns true if this install is newly set up (i.e., there are no user
22 * accounts yet). In this case, we enter a special mode to permit creation
23 * of the first account form the web UI.
24 *
25 * @return bool
26 */
27 protected function isFirstTimeSetup() {
28 // If there are any auth providers, this isn't first time setup, even if
29 // we don't have accounts.
30 if (PhabricatorAuthProvider::getAllEnabledProviders()) {
31 return false;
32 }
33
34 // Otherwise, check if there are any user accounts. If not, we're in first
35 // time setup.
36 $any_users = id(new PhabricatorPeopleQuery())
37 ->setViewer(PhabricatorUser::getOmnipotentUser())
38 ->setLimit(1)
39 ->execute();
40
41 return !$any_users;
42 }
43
44
45 /**
46 * Log a user into a web session and return an @{class:AphrontResponse} which
47 * corresponds to continuing the login process.
48 *
49 * Normally, this is a redirect to the validation controller which makes sure
50 * the user's cookies are set. However, event listeners can intercept this
51 * event and do something else if they prefer.
52 *
53 * @param PhabricatorUser $user User to log the viewer in as.
54 * @param bool $force_full_session (optional) True to issue a full session
55 * immediately, bypassing MFA.
56 * @return AphrontResponse Response which continues the login process.
57 */
58 protected function loginUser(
59 PhabricatorUser $user,
60 $force_full_session = false) {
61
62 $response = $this->buildLoginValidateResponse($user);
63 $session_type = PhabricatorAuthSession::TYPE_WEB;
64
65 if ($force_full_session) {
66 $partial_session = false;
67 } else {
68 $partial_session = true;
69 }
70
71 $session_key = id(new PhabricatorAuthSessionEngine())
72 ->establishSession($session_type, $user->getPHID(), $partial_session);
73
74 // NOTE: We allow disabled users to login and roadblock them later, so
75 // there's no check for users being disabled here.
76
77 $request = $this->getRequest();
78 $request->setCookie(
79 PhabricatorCookies::COOKIE_USERNAME,
80 $user->getUsername());
81 $request->setCookie(
82 PhabricatorCookies::COOKIE_SESSION,
83 $session_key);
84
85 $this->clearRegistrationCookies();
86
87 return $response;
88 }
89
90 protected function clearRegistrationCookies() {
91 $request = $this->getRequest();
92
93 // Clear the registration key.
94 $request->clearCookie(PhabricatorCookies::COOKIE_REGISTRATION);
95
96 // Clear the client ID / OAuth state key.
97 $request->clearCookie(PhabricatorCookies::COOKIE_CLIENTID);
98
99 // Clear the invite cookie.
100 $request->clearCookie(PhabricatorCookies::COOKIE_INVITE);
101 }
102
103 /**
104 * @return AphrontRedirectResponse Redirect to /auth/validate/, including
105 * an "expect" parameter with the expected username, to be validated in
106 * @{class:PhabricatorAuthValidateController}
107 */
108 private function buildLoginValidateResponse(PhabricatorUser $user) {
109 $validate_uri = new PhutilURI($this->getApplicationURI('validate/'));
110 $validate_uri->replaceQueryParam('expect', $user->getUsername());
111
112 return id(new AphrontRedirectResponse())->setURI((string)$validate_uri);
113 }
114
115 protected function renderError($message) {
116 return $this->renderErrorPage(
117 pht('Authentication Error'),
118 array(
119 $message,
120 ));
121 }
122
123 /**
124 * @return array Returns <null,null,PhabricatorStandardPageView> or
125 * <PhabricatorExternalAccount,null,PhabricatorStandardPageView> in case of
126 * an error, or <PhabricatorExternalAccount,PhabricatorAuthProvider,null>
127 * in case of success.
128 */
129 protected function loadAccountForRegistrationOrLinking($account_key) {
130 $request = $this->getRequest();
131 $viewer = $request->getUser();
132
133 $account = null;
134 $provider = null;
135 $response = null;
136
137 if (!$account_key) {
138 $response = $this->renderError(
139 pht('Request did not include account key.'));
140 return array($account, $provider, $response);
141 }
142
143 // NOTE: We're using the omnipotent user because the actual user may not
144 // be logged in yet, and because we want to tailor an error message to
145 // distinguish between "not usable" and "does not exist". We do explicit
146 // checks later on to make sure this account is valid for the intended
147 // operation. This requires edit permission for completeness and consistency
148 // but it won't actually be meaningfully checked because we're using the
149 // omnipotent user.
150
151 $account = id(new PhabricatorExternalAccountQuery())
152 ->setViewer(PhabricatorUser::getOmnipotentUser())
153 ->withAccountSecrets(array($account_key))
154 ->needImages(true)
155 ->requireCapabilities(
156 array(
157 PhabricatorPolicyCapability::CAN_VIEW,
158 PhabricatorPolicyCapability::CAN_EDIT,
159 ))
160 ->executeOne();
161
162 if (!$account) {
163 $response = $this->renderError(pht('No valid linkable account.'));
164 return array($account, $provider, $response);
165 }
166
167 if ($account->getUserPHID()) {
168 if ($account->getUserPHID() != $viewer->getPHID()) {
169 $response = $this->renderError(
170 pht(
171 'The account you are attempting to register or link is already '.
172 'linked to another user.'));
173 } else {
174 $response = $this->renderError(
175 pht(
176 'The account you are attempting to link is already linked '.
177 'to your account.'));
178 }
179 return array($account, $provider, $response);
180 }
181
182 $registration_key = $request->getCookie(
183 PhabricatorCookies::COOKIE_REGISTRATION);
184
185 // NOTE: This registration key check is not strictly necessary, because
186 // we're only creating new accounts, not linking existing accounts. It
187 // might be more hassle than it is worth, especially for email.
188 //
189 // The attack this prevents is getting to the registration screen, then
190 // copy/pasting the URL and getting someone else to click it and complete
191 // the process. They end up with an account bound to credentials you
192 // control. This doesn't really let you do anything meaningful, though,
193 // since you could have simply completed the process yourself.
194
195 if (!$registration_key) {
196 $response = $this->renderError(
197 pht(
198 'Your browser did not submit a registration key with the request. '.
199 'You must use the same browser to begin and complete registration. '.
200 'Check that cookies are enabled and try again.'));
201 return array($account, $provider, $response);
202 }
203
204 // We store the digest of the key rather than the key itself to prevent a
205 // theoretical attacker with read-only access to the database from
206 // hijacking registration sessions.
207
208 $actual = $account->getProperty('registrationKey');
209 $expect = PhabricatorHash::weakDigest($registration_key);
210 if (!phutil_hashes_are_identical($actual, $expect)) {
211 $response = $this->renderError(
212 pht(
213 'Your browser submitted a different registration key than the one '.
214 'associated with this account. You may need to clear your cookies.'));
215 return array($account, $provider, $response);
216 }
217
218 $config = $account->getProviderConfig();
219 if (!$config->getIsEnabled()) {
220 $response = $this->renderError(
221 pht(
222 'The account you are attempting to register with uses a disabled '.
223 'authentication provider ("%s"). An administrator may have '.
224 'recently disabled this provider.',
225 $config->getDisplayName()));
226 return array($account, $provider, $response);
227 }
228
229 $provider = $config->getProvider();
230
231 return array($account, $provider, null);
232 }
233
234 /**
235 * @return PhabricatorAuthInvite|null Invitation, or null if none exists
236 */
237 protected function loadInvite() {
238 $invite_cookie = PhabricatorCookies::COOKIE_INVITE;
239 $invite_code = $this->getRequest()->getCookie($invite_cookie);
240 if (!$invite_code) {
241 return null;
242 }
243
244 $engine = id(new PhabricatorAuthInviteEngine())
245 ->setViewer($this->getViewer())
246 ->setUserHasConfirmedVerify(true);
247
248 try {
249 return $engine->processInviteCode($invite_code);
250 } catch (Exception $ex) {
251 // If this fails for any reason, just drop the invite. In normal
252 // circumstances, we gave them a detailed explanation of any error
253 // before they jumped into this workflow.
254 return null;
255 }
256 }
257
258 /**
259 * @return PHUIBoxView|null
260 */
261 protected function renderInviteHeader(PhabricatorAuthInvite $invite) {
262 $viewer = $this->getViewer();
263
264 // Since the user hasn't registered yet, they may not be able to see other
265 // user accounts. Load the inviting user with the omnipotent viewer.
266 $omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
267
268 $invite_author = id(new PhabricatorPeopleQuery())
269 ->setViewer($omnipotent_viewer)
270 ->withPHIDs(array($invite->getAuthorPHID()))
271 ->needProfileImage(true)
272 ->executeOne();
273
274 // If we can't load the author for some reason, just drop this message.
275 // We lose the value of contextualizing things without author details.
276 if (!$invite_author) {
277 return null;
278 }
279
280 $invite_item = id(new PHUIObjectItemView())
281 ->setHeader(
282 pht(
283 'Welcome to %s!',
284 PlatformSymbols::getPlatformServerName()))
285 ->setImageURI($invite_author->getProfileImageURI())
286 ->addAttribute(
287 pht(
288 '%s has invited you to join %s.',
289 $invite_author->getFullName(),
290 PlatformSymbols::getPlatformServerName()));
291
292 $invite_list = id(new PHUIObjectItemListView())
293 ->addItem($invite_item)
294 ->setFlush(true);
295
296 return id(new PHUIBoxView())
297 ->addMargin(PHUI::MARGIN_LARGE)
298 ->appendChild($invite_list);
299 }
300
301 /**
302 * @return PhutilSafeHTML|null
303 */
304 final protected function newCustomStartMessage() {
305 $viewer = $this->getViewer();
306
307 $text = PhabricatorAuthMessage::loadMessageText(
308 $viewer,
309 PhabricatorAuthLoginMessageType::MESSAGEKEY);
310
311 if (!phutil_nonempty_string($text)) {
312 return null;
313 }
314
315 $remarkup_view = new PHUIRemarkupView($viewer, $text);
316
317 return phutil_tag(
318 'div',
319 array(
320 'class' => 'auth-custom-message',
321 ),
322 $remarkup_view);
323 }
324
325}