@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
at upstream/main 325 lines 11 kB view raw
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}