Path: blob/master/src/applications/auth/controller/PhabricatorAuthController.php
12256 views
<?php12abstract class PhabricatorAuthController extends PhabricatorController {34protected function renderErrorPage($title, array $messages) {5$view = new PHUIInfoView();6$view->setTitle($title);7$view->setErrors($messages);89return $this->newPage()10->setTitle($title)11->appendChild($view);1213}1415/**16* Returns true if this install is newly setup (i.e., there are no user17* accounts yet). In this case, we enter a special mode to permit creation18* of the first account form the web UI.19*/20protected function isFirstTimeSetup() {21// If there are any auth providers, this isn't first time setup, even if22// we don't have accounts.23if (PhabricatorAuthProvider::getAllEnabledProviders()) {24return false;25}2627// Otherwise, check if there are any user accounts. If not, we're in first28// time setup.29$any_users = id(new PhabricatorPeopleQuery())30->setViewer(PhabricatorUser::getOmnipotentUser())31->setLimit(1)32->execute();3334return !$any_users;35}363738/**39* Log a user into a web session and return an @{class:AphrontResponse} which40* corresponds to continuing the login process.41*42* Normally, this is a redirect to the validation controller which makes sure43* the user's cookies are set. However, event listeners can intercept this44* event and do something else if they prefer.45*46* @param PhabricatorUser User to log the viewer in as.47* @param bool True to issue a full session immediately, bypassing MFA.48* @return AphrontResponse Response which continues the login process.49*/50protected function loginUser(51PhabricatorUser $user,52$force_full_session = false) {5354$response = $this->buildLoginValidateResponse($user);55$session_type = PhabricatorAuthSession::TYPE_WEB;5657if ($force_full_session) {58$partial_session = false;59} else {60$partial_session = true;61}6263$session_key = id(new PhabricatorAuthSessionEngine())64->establishSession($session_type, $user->getPHID(), $partial_session);6566// NOTE: We allow disabled users to login and roadblock them later, so67// there's no check for users being disabled here.6869$request = $this->getRequest();70$request->setCookie(71PhabricatorCookies::COOKIE_USERNAME,72$user->getUsername());73$request->setCookie(74PhabricatorCookies::COOKIE_SESSION,75$session_key);7677$this->clearRegistrationCookies();7879return $response;80}8182protected function clearRegistrationCookies() {83$request = $this->getRequest();8485// Clear the registration key.86$request->clearCookie(PhabricatorCookies::COOKIE_REGISTRATION);8788// Clear the client ID / OAuth state key.89$request->clearCookie(PhabricatorCookies::COOKIE_CLIENTID);9091// Clear the invite cookie.92$request->clearCookie(PhabricatorCookies::COOKIE_INVITE);93}9495private function buildLoginValidateResponse(PhabricatorUser $user) {96$validate_uri = new PhutilURI($this->getApplicationURI('validate/'));97$validate_uri->replaceQueryParam('expect', $user->getUsername());9899return id(new AphrontRedirectResponse())->setURI((string)$validate_uri);100}101102protected function renderError($message) {103return $this->renderErrorPage(104pht('Authentication Error'),105array(106$message,107));108}109110protected function loadAccountForRegistrationOrLinking($account_key) {111$request = $this->getRequest();112$viewer = $request->getUser();113114$account = null;115$provider = null;116$response = null;117118if (!$account_key) {119$response = $this->renderError(120pht('Request did not include account key.'));121return array($account, $provider, $response);122}123124// NOTE: We're using the omnipotent user because the actual user may not125// be logged in yet, and because we want to tailor an error message to126// distinguish between "not usable" and "does not exist". We do explicit127// checks later on to make sure this account is valid for the intended128// operation. This requires edit permission for completeness and consistency129// but it won't actually be meaningfully checked because we're using the130// omnipotent user.131132$account = id(new PhabricatorExternalAccountQuery())133->setViewer(PhabricatorUser::getOmnipotentUser())134->withAccountSecrets(array($account_key))135->needImages(true)136->requireCapabilities(137array(138PhabricatorPolicyCapability::CAN_VIEW,139PhabricatorPolicyCapability::CAN_EDIT,140))141->executeOne();142143if (!$account) {144$response = $this->renderError(pht('No valid linkable account.'));145return array($account, $provider, $response);146}147148if ($account->getUserPHID()) {149if ($account->getUserPHID() != $viewer->getPHID()) {150$response = $this->renderError(151pht(152'The account you are attempting to register or link is already '.153'linked to another user.'));154} else {155$response = $this->renderError(156pht(157'The account you are attempting to link is already linked '.158'to your account.'));159}160return array($account, $provider, $response);161}162163$registration_key = $request->getCookie(164PhabricatorCookies::COOKIE_REGISTRATION);165166// NOTE: This registration key check is not strictly necessary, because167// we're only creating new accounts, not linking existing accounts. It168// might be more hassle than it is worth, especially for email.169//170// The attack this prevents is getting to the registration screen, then171// copy/pasting the URL and getting someone else to click it and complete172// the process. They end up with an account bound to credentials you173// control. This doesn't really let you do anything meaningful, though,174// since you could have simply completed the process yourself.175176if (!$registration_key) {177$response = $this->renderError(178pht(179'Your browser did not submit a registration key with the request. '.180'You must use the same browser to begin and complete registration. '.181'Check that cookies are enabled and try again.'));182return array($account, $provider, $response);183}184185// We store the digest of the key rather than the key itself to prevent a186// theoretical attacker with read-only access to the database from187// hijacking registration sessions.188189$actual = $account->getProperty('registrationKey');190$expect = PhabricatorHash::weakDigest($registration_key);191if (!phutil_hashes_are_identical($actual, $expect)) {192$response = $this->renderError(193pht(194'Your browser submitted a different registration key than the one '.195'associated with this account. You may need to clear your cookies.'));196return array($account, $provider, $response);197}198199$config = $account->getProviderConfig();200if (!$config->getIsEnabled()) {201$response = $this->renderError(202pht(203'The account you are attempting to register with uses a disabled '.204'authentication provider ("%s"). An administrator may have '.205'recently disabled this provider.',206$config->getDisplayName()));207return array($account, $provider, $response);208}209210$provider = $config->getProvider();211212return array($account, $provider, null);213}214215protected function loadInvite() {216$invite_cookie = PhabricatorCookies::COOKIE_INVITE;217$invite_code = $this->getRequest()->getCookie($invite_cookie);218if (!$invite_code) {219return null;220}221222$engine = id(new PhabricatorAuthInviteEngine())223->setViewer($this->getViewer())224->setUserHasConfirmedVerify(true);225226try {227return $engine->processInviteCode($invite_code);228} catch (Exception $ex) {229// If this fails for any reason, just drop the invite. In normal230// circumstances, we gave them a detailed explanation of any error231// before they jumped into this workflow.232return null;233}234}235236protected function renderInviteHeader(PhabricatorAuthInvite $invite) {237$viewer = $this->getViewer();238239// Since the user hasn't registered yet, they may not be able to see other240// user accounts. Load the inviting user with the omnipotent viewer.241$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();242243$invite_author = id(new PhabricatorPeopleQuery())244->setViewer($omnipotent_viewer)245->withPHIDs(array($invite->getAuthorPHID()))246->needProfileImage(true)247->executeOne();248249// If we can't load the author for some reason, just drop this message.250// We lose the value of contextualizing things without author details.251if (!$invite_author) {252return null;253}254255$invite_item = id(new PHUIObjectItemView())256->setHeader(257pht(258'Welcome to %s!',259PlatformSymbols::getPlatformServerName()))260->setImageURI($invite_author->getProfileImageURI())261->addAttribute(262pht(263'%s has invited you to join %s.',264$invite_author->getFullName(),265PlatformSymbols::getPlatformServerName()));266267$invite_list = id(new PHUIObjectItemListView())268->addItem($invite_item)269->setFlush(true);270271return id(new PHUIBoxView())272->addMargin(PHUI::MARGIN_LARGE)273->appendChild($invite_list);274}275276277final protected function newCustomStartMessage() {278$viewer = $this->getViewer();279280$text = PhabricatorAuthMessage::loadMessageText(281$viewer,282PhabricatorAuthLoginMessageType::MESSAGEKEY);283284if ($text === null || !strlen($text)) {285return null;286}287288$remarkup_view = new PHUIRemarkupView($viewer, $text);289290return phutil_tag(291'div',292array(293'class' => 'auth-custom-message',294),295$remarkup_view);296}297298}299300301