Path: blob/master/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php
13401 views
<?php12final class PhabricatorAuthOneTimeLoginController3extends PhabricatorAuthController {45public function shouldRequireLogin() {6return false;7}89public function handleRequest(AphrontRequest $request) {10$viewer = $this->getViewer();11$id = $request->getURIData('id');12$link_type = $request->getURIData('type');13$key = $request->getURIData('key');14$email_id = $request->getURIData('emailID');1516$target_user = id(new PhabricatorPeopleQuery())17->setViewer(PhabricatorUser::getOmnipotentUser())18->withIDs(array($id))19->executeOne();20if (!$target_user) {21return new Aphront404Response();22}2324// NOTE: We allow you to use a one-time login link for your own current25// login account. This supports the "Set Password" flow.2627$is_logged_in = false;28if ($viewer->isLoggedIn()) {29if ($viewer->getPHID() !== $target_user->getPHID()) {30return $this->renderError(31pht('You are already logged in.'));32} else {33$is_logged_in = true;34}35}3637// NOTE: As a convenience to users, these one-time login URIs may also38// be associated with an email address which will be verified when the39// URI is used.4041// This improves the new user experience for users receiving "Welcome"42// emails on installs that require verification: if we did not verify the43// email, they'd immediately get roadblocked with a "Verify Your Email"44// error and have to go back to their email account, wait for a45// "Verification" email, and then click that link to actually get access to46// their account. This is hugely unwieldy, and if the link was only sent47// to the user's email in the first place we can safely verify it as a48// side effect of login.4950// The email hashed into the URI so users can't verify some email they51// do not own by doing this:52//53// - Add some address you do not own;54// - request a password reset;55// - change the URI in the email to the address you don't own;56// - login via the email link; and57// - get a "verified" address you don't control.5859$target_email = null;60if ($email_id) {61$target_email = id(new PhabricatorUserEmail())->loadOneWhere(62'userPHID = %s AND id = %d',63$target_user->getPHID(),64$email_id);65if (!$target_email) {66return new Aphront404Response();67}68}6970$engine = new PhabricatorAuthSessionEngine();71$token = $engine->loadOneTimeLoginKey(72$target_user,73$target_email,74$key);7576if (!$token) {77return $this->newDialog()78->setTitle(pht('Unable to Log In'))79->setShortTitle(pht('Login Failure'))80->appendParagraph(81pht(82'The login link you clicked is invalid, out of date, or has '.83'already been used.'))84->appendParagraph(85pht(86'Make sure you are copy-and-pasting the entire link into '.87'your browser. Login links are only valid for 24 hours, and '.88'can only be used once.'))89->appendParagraph(90pht('You can try again, or request a new link via email.'))91->addCancelButton('/login/email/', pht('Send Another Email'));92}9394if (!$target_user->canEstablishWebSessions()) {95return $this->newDialog()96->setTitle(pht('Unable to Establish Web Session'))97->setShortTitle(pht('Login Failure'))98->appendParagraph(99pht(100'You are trying to gain access to an account ("%s") that can not '.101'establish a web session.',102$target_user->getUsername()))103->appendParagraph(104pht(105'Special users like daemons and mailing lists are not permitted '.106'to log in via the web. Log in as a normal user instead.'))107->addCancelButton('/');108}109110if ($request->isFormPost() || $is_logged_in) {111// If we have an email bound into this URI, verify email so that clicking112// the link in the "Welcome" email is good enough, without requiring users113// to go through a second round of email verification.114115$editor = id(new PhabricatorUserEditor())116->setActor($target_user);117118$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();119// Nuke the token and all other outstanding password reset tokens.120// There is no particular security benefit to destroying them all, but121// it should reduce HackerOne reports of nebulous harm.122$editor->revokePasswordResetLinks($target_user);123124if ($target_email) {125$editor->verifyEmail($target_user, $target_email);126}127unset($unguarded);128129$next_uri = $this->getNextStepURI($target_user);130131// If the user is already logged in, we're just doing a "password set"132// flow. Skip directly to the next step.133if ($is_logged_in) {134return id(new AphrontRedirectResponse())->setURI($next_uri);135}136137PhabricatorCookies::setNextURICookie($request, $next_uri, $force = true);138139$force_full_session = false;140if ($link_type === PhabricatorAuthSessionEngine::ONETIME_RECOVER) {141$force_full_session = $token->getShouldForceFullSession();142}143144return $this->loginUser($target_user, $force_full_session);145}146147// NOTE: We need to CSRF here so attackers can't generate an email link,148// then log a user in to an account they control via sneaky invisible149// form submissions.150151switch ($link_type) {152case PhabricatorAuthSessionEngine::ONETIME_WELCOME:153$title = pht(154'Welcome to %s',155PlatformSymbols::getPlatformServerName());156break;157case PhabricatorAuthSessionEngine::ONETIME_RECOVER:158$title = pht('Account Recovery');159break;160case PhabricatorAuthSessionEngine::ONETIME_USERNAME:161case PhabricatorAuthSessionEngine::ONETIME_RESET:162default:163$title = pht(164'Log in to %s',165PlatformSymbols::getPlatformServerName());166break;167}168169$body = array();170$body[] = pht(171'Use the button below to log in as: %s',172phutil_tag('strong', array(), $target_user->getUsername()));173174if ($target_email && !$target_email->getIsVerified()) {175$body[] = pht(176'Logging in will verify %s as an email address you own.',177phutil_tag('strong', array(), $target_email->getAddress()));178179}180181$body[] = pht(182'After logging in you should set a password for your account, or '.183'link your account to an external account that you can use to '.184'authenticate in the future.');185186$dialog = $this->newDialog()187->setTitle($title)188->addSubmitButton(pht('Log In (%s)', $target_user->getUsername()))189->addCancelButton('/');190191foreach ($body as $paragraph) {192$dialog->appendParagraph($paragraph);193}194195return id(new AphrontDialogResponse())->setDialog($dialog);196}197198private function getNextStepURI(PhabricatorUser $user) {199$request = $this->getRequest();200201// If we have password auth, let the user set or reset their password after202// login.203$have_passwords = PhabricatorPasswordAuthProvider::getPasswordProvider();204if ($have_passwords) {205// We're going to let the user reset their password without knowing206// the old one. Generate a one-time token for that.207$key = Filesystem::readRandomCharacters(16);208$password_type =209PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE;210211$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();212id(new PhabricatorAuthTemporaryToken())213->setTokenResource($user->getPHID())214->setTokenType($password_type)215->setTokenExpires(time() + phutil_units('1 hour in seconds'))216->setTokenCode(PhabricatorHash::weakDigest($key))217->save();218unset($unguarded);219220$panel_uri = '/auth/password/';221222$request->setTemporaryCookie(PhabricatorCookies::COOKIE_HISEC, 'yes');223224$params = array(225'key' => $key,226);227228return (string)new PhutilURI($panel_uri, $params);229}230231// Check if the user already has external accounts linked. If they do,232// it's not obvious why they aren't using them to log in, but assume they233// know what they're doing. We won't send them to the link workflow.234$accounts = id(new PhabricatorExternalAccountQuery())235->setViewer($user)236->withUserPHIDs(array($user->getPHID()))237->execute();238239$configs = id(new PhabricatorAuthProviderConfigQuery())240->setViewer($user)241->withIsEnabled(true)242->execute();243244$linkable = array();245foreach ($configs as $config) {246if (!$config->getShouldAllowLink()) {247continue;248}249250$provider = $config->getProvider();251if (!$provider->isLoginFormAButton()) {252continue;253}254255$linkable[] = $provider;256}257258// If there's at least one linkable provider, and the user doesn't already259// have accounts, send the user to the link workflow.260if (!$accounts && $linkable) {261return '/auth/external/';262}263264// If there are no configured providers and the user is an administrator,265// send them to Auth to configure a provider. This is probably where they266// want to go. You can end up in this state by accidentally losing your267// first session during initial setup, or after restoring exported data268// from a hosted instance.269if (!$configs && $user->getIsAdmin()) {270return '/auth/';271}272273// If we didn't find anywhere better to send them, give up and just send274// them to the home page.275return '/';276}277278}279280281