Differential D9252 Diff 21982 src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php
Changeset View
Changeset View
Standalone View
Standalone View
src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php
- This file was added.
<?php | |||||
final class PhabricatorAuthOneTimeLoginController | |||||
extends PhabricatorAuthController { | |||||
private $id; | |||||
private $key; | |||||
private $emailID; | |||||
private $linkType; | |||||
public function shouldRequireLogin() { | |||||
return false; | |||||
} | |||||
public function willProcessRequest(array $data) { | |||||
$this->linkType = $data['type']; | |||||
$this->id = $data['id']; | |||||
$this->key = $data['key']; | |||||
$this->emailID = idx($data, 'emailID'); | |||||
} | |||||
public function processRequest() { | |||||
$request = $this->getRequest(); | |||||
if ($request->getUser()->isLoggedIn()) { | |||||
return $this->renderError( | |||||
pht('You are already logged in.')); | |||||
} | |||||
$target_user = id(new PhabricatorPeopleQuery()) | |||||
->setViewer(PhabricatorUser::getOmnipotentUser()) | |||||
->withIDs(array($this->id)) | |||||
->executeOne(); | |||||
if (!$target_user) { | |||||
return new Aphront404Response(); | |||||
} | |||||
// NOTE: As a convenience to users, these one-time login URIs may also | |||||
// be associated with an email address which will be verified when the | |||||
// URI is used. | |||||
// This improves the new user experience for users receiving "Welcome" | |||||
// emails on installs that require verification: if we did not verify the | |||||
// email, they'd immediately get roadblocked with a "Verify Your Email" | |||||
// error and have to go back to their email account, wait for a | |||||
// "Verification" email, and then click that link to actually get access to | |||||
// their account. This is hugely unwieldy, and if the link was only sent | |||||
// to the user's email in the first place we can safely verify it as a | |||||
// side effect of login. | |||||
// The email hashed into the URI so users can't verify some email they | |||||
// do not own by doing this: | |||||
// | |||||
// - Add some address you do not own; | |||||
// - request a password reset; | |||||
// - change the URI in the email to the address you don't own; | |||||
// - login via the email link; and | |||||
// - get a "verified" address you don't control. | |||||
$target_email = null; | |||||
if ($this->emailID) { | |||||
$target_email = id(new PhabricatorUserEmail())->loadOneWhere( | |||||
'userPHID = %s AND id = %d', | |||||
$target_user->getPHID(), | |||||
$this->emailID); | |||||
if (!$target_email) { | |||||
return new Aphront404Response(); | |||||
} | |||||
} | |||||
$engine = new PhabricatorAuthSessionEngine(); | |||||
$token = $engine->loadOneTimeLoginKey( | |||||
$target_user, | |||||
$target_email, | |||||
$this->key); | |||||
if (!$token) { | |||||
return $this->newDialog() | |||||
->setTitle(pht('Unable to Login')) | |||||
->setShortTitle(pht('Login Failure')) | |||||
->appendParagraph( | |||||
pht( | |||||
'The login link you clicked is invalid, out of date, or has '. | |||||
'already been used.')) | |||||
->appendParagraph( | |||||
pht( | |||||
'Make sure you are copy-and-pasting the entire link into '. | |||||
'your browser. Login links are only valid for 24 hours, and '. | |||||
'can only be used once.')) | |||||
->appendParagraph( | |||||
pht('You can try again, or request a new link via email.')) | |||||
->addCancelButton('/login/email/', pht('Send Another Email')); | |||||
} | |||||
if ($request->isFormPost()) { | |||||
// If we have an email bound into this URI, verify email so that clicking | |||||
// the link in the "Welcome" email is good enough, without requiring users | |||||
// to go through a second round of email verification. | |||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); | |||||
// Nuke the token so that this URI is one-time only. | |||||
$token->delete(); | |||||
if ($target_email) { | |||||
$target_user->openTransaction(); | |||||
$target_email->setIsVerified(1); | |||||
$target_email->save(); | |||||
// If this was the user's primary email address, also mark their | |||||
// account as verified. | |||||
$primary_email = $target_user->loadPrimaryEmail(); | |||||
if ($primary_email->getID() == $target_email->getID()) { | |||||
$target_user->setIsEmailVerified(1); | |||||
$target_user->save(); | |||||
} | |||||
$target_user->saveTransaction(); | |||||
} | |||||
unset($unguarded); | |||||
$next = '/'; | |||||
if (!PhabricatorAuthProviderPassword::getPasswordProvider()) { | |||||
$next = '/settings/panel/external/'; | |||||
} else if (PhabricatorEnv::getEnvConfig('account.editable')) { | |||||
// We're going to let the user reset their password without knowing | |||||
// the old one. Generate a one-time token for that. | |||||
$key = Filesystem::readRandomCharacters(16); | |||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); | |||||
id(new PhabricatorAuthTemporaryToken()) | |||||
->setObjectPHID($target_user->getPHID()) | |||||
->setTokenType( | |||||
PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE) | |||||
->setTokenExpires(time() + phutil_units('1 hour in seconds')) | |||||
->setTokenCode(PhabricatorHash::digest($key)) | |||||
->save(); | |||||
unset($unguarded); | |||||
$next = (string)id(new PhutilURI('/settings/panel/password/')) | |||||
->setQueryParams( | |||||
array( | |||||
'key' => $key, | |||||
)); | |||||
$request->setTemporaryCookie(PhabricatorCookies::COOKIE_HISEC, 'yes'); | |||||
} | |||||
PhabricatorCookies::setNextURICookie($request, $next, $force = true); | |||||
return $this->loginUser($target_user); | |||||
} | |||||
// NOTE: We need to CSRF here so attackers can't generate an email link, | |||||
// then log a user in to an account they control via sneaky invisible | |||||
// form submissions. | |||||
switch ($this->linkType) { | |||||
case PhabricatorAuthSessionEngine::ONETIME_WELCOME: | |||||
$title = pht('Welcome to Phabricator'); | |||||
break; | |||||
case PhabricatorAuthSessionEngine::ONETIME_RECOVER: | |||||
$title = pht('Account Recovery'); | |||||
break; | |||||
case PhabricatorAuthSessionEngine::ONETIME_USERNAME: | |||||
case PhabricatorAuthSessionEngine::ONETIME_RESET: | |||||
default: | |||||
$title = pht('Login to Phabricator'); | |||||
break; | |||||
} | |||||
$body = array(); | |||||
$body[] = pht( | |||||
'Use the button below to log in as: %s', | |||||
phutil_tag('strong', array(), $target_user->getUsername())); | |||||
if ($target_email && !$target_email->getIsVerified()) { | |||||
$body[] = pht( | |||||
'Logging in will verify %s as an email address you own.', | |||||
phutil_tag('strong', array(), $target_email->getAddress())); | |||||
} | |||||
$body[] = pht( | |||||
'After logging in you should set a password for your account, or '. | |||||
'link your account to an external account that you can use to '. | |||||
'authenticate in the future.'); | |||||
$dialog = $this->newDialog() | |||||
->setTitle($title) | |||||
->addSubmitButton(pht('Login (%s)', $target_user->getUsername())) | |||||
->addCancelButton('/'); | |||||
foreach ($body as $paragraph) { | |||||
$dialog->appendParagraph($paragraph); | |||||
} | |||||
return id(new AphrontDialogResponse())->setDialog($dialog); | |||||
} | |||||
} |