Allow users to have multiple email addresses, and verify emails

Summary:
  - Move email to a separate table.
  - Migrate existing email to new storage.
  - Allow users to add and remove email addresses.
  - Allow users to verify email addresses.
  - Allow users to change their primary email address.
  - Convert all the registration/reset/login code to understand these changes.
  - There are a few security considerations here but I think I've addressed them. Principally, it is important to never let a user acquire a verified email address they don't actually own. We ensure this by tightening the scoping of token generation rules to be (user, email) specific.
  - This should have essentially zero impact on Facebook, but may require some minor changes in the registration code -- I don't exactly remember how it is set up.

Not included here (next steps):

  - Allow configuration to restrict email to certain domains.
  - Allow configuration to require validated email.

Test Plan:
This is a fairly extensive, difficult-to-test change.

  - From "Email Addresses" interface:
    - Added new email (verified email verifications sent).
    - Changed primary email (verified old/new notificactions sent).
    - Resent verification emails (verified they sent).
    - Removed email.
    - Tried to add already-owned email.
  - Created new users with "accountadmin". Edited existing users with "accountadmin".
  - Created new users with "add_user.php".
  - Created new users with web interface.
  - Clicked welcome email link, verified it verified email.
  - Reset password.
  - Linked/unlinked oauth accounts.
  - Logged in with oauth account.
  - Logged in with email.
  - Registered with Oauth account.
  - Tried to register with OAuth account with duplicate email.
  - Verified errors for email verification with bad tokens, etc.

Reviewers: btrahan, vrana, jungejason

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T1184

Differential Revision: https://secure.phabricator.com/D2393
This commit is contained in:
epriestley
2012-05-07 10:29:33 -07:00
parent 803dea1517
commit 87207b2f4e
38 changed files with 900 additions and 140 deletions

View File

@@ -57,17 +57,24 @@ final class PhabricatorEmailLoginController
// it expensive to fish for valid email addresses while giving the user
// a better error if they goof their email.
$target_user = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
$target_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
$target_user = null;
if ($target_email) {
$target_user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$target_email->getUserPHID());
}
if (!$target_user) {
$errors[] = "There is no account associated with that email address.";
$e_email = "Invalid";
}
if (!$errors) {
$uri = $target_user->getEmailLoginURI();
$uri = $target_user->getEmailLoginURI($target_email);
if ($is_serious) {
$body = <<<EOBODY
You can use this link to reset your Phabricator password:

View File

@@ -9,6 +9,7 @@
phutil_require_module('phabricator', 'aphront/response/400');
phutil_require_module('phabricator', 'applications/auth/controller/base');
phutil_require_module('phabricator', 'applications/metamta/storage/mail');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'view/form/base');

View File

@@ -55,11 +55,31 @@ final class PhabricatorEmailTokenController
$token = $this->token;
$email = $request->getStr('email');
$target_user = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
// NOTE: We need to bind verification to **addresses**, not **users**,
// because we verify addresses when they're used to login this way, and if
// we have a user-based verification you can:
//
// - 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 = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
if (!$target_user || !$target_user->validateEmailToken($token)) {
$target_user = null;
if ($target_email) {
$target_user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$target_email->getUserPHID());
}
if (!$target_email ||
!$target_user ||
!$target_user->validateEmailToken($target_email, $token)) {
$view = new AphrontRequestFailureView();
$view->setHeader('Unable to Login');
$view->appendChild(
@@ -71,19 +91,32 @@ final class PhabricatorEmailTokenController
'<div class="aphront-failure-continue">'.
'<a class="button" href="/login/email/">Send Another Email</a>'.
'</div>');
return $this->buildStandardPageResponse(
$view,
array(
'title' => 'Email Sent',
'title' => 'Login Failure',
));
}
// 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.
$target_email->setIsVerified(1);
$target_email->save();
$session_key = $target_user->establishSession('web');
$request->setCookie('phusr', $target_user->getUsername());
$request->setCookie('phsid', $session_key);
if (PhabricatorEnv::getEnvConfig('account.editable')) {
$next = '/settings/page/password/?token='.$token;
$next = (string)id(new PhutilURI('/settings/page/password/'))
->setQueryParams(
array(
'token' => $token,
'email' => $email,
));
} else {
$next = '/';
}

View File

@@ -9,6 +9,7 @@
phutil_require_module('phabricator', 'aphront/response/400');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/auth/controller/base');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'view/page/failure');

View File

@@ -113,9 +113,7 @@ final class PhabricatorLoginController
$username_or_email);
if (!$user) {
$user = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
$username_or_email);
$user = PhabricatorUser::loadOneWithEmailAddress($username_or_email);
}
if (!$errors) {

View File

@@ -176,8 +176,8 @@ final class PhabricatorOAuthLoginController
$oauth_email = $provider->retrieveUserEmail();
if ($oauth_email) {
$known_email = id(new PhabricatorUser())
->loadOneWhere('email = %s', $oauth_email);
$known_email = id(new PhabricatorUserEmail())
->loadOneWhere('address = %s', $oauth_email);
if ($known_email) {
$dialog = new AphrontDialogView();
$dialog->setUser($current_user);

View File

@@ -13,6 +13,7 @@ phutil_require_module('phabricator', 'aphront/writeguard');
phutil_require_module('phabricator', 'applications/auth/controller/base');
phutil_require_module('phabricator', 'applications/auth/oauth/provider/base');
phutil_require_module('phabricator', 'applications/auth/view/oauthfailure');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'applications/people/storage/useroauthinfo');
phutil_require_module('phabricator', 'infrastructure/env');

View File

@@ -33,7 +33,8 @@ final class PhabricatorOAuthDefaultRegistrationController
$user->setUsername($provider->retrieveUserAccountName());
$user->setRealName($provider->retrieveUserRealName());
$user->setEmail($provider->retrieveUserEmail());
$new_email = $provider->retrieveUserEmail();
if ($request->isFormPost()) {
@@ -49,9 +50,9 @@ final class PhabricatorOAuthDefaultRegistrationController
$e_username = null;
}
if ($user->getEmail() === null) {
$user->setEmail($request->getStr('email'));
if (!strlen($user->getEmail())) {
if (!$new_email) {
$new_email = trim($request->getStr('email'));
if (!$new_email) {
$e_email = 'Required';
$errors[] = 'Email is required.';
} else {
@@ -84,12 +85,29 @@ final class PhabricatorOAuthDefaultRegistrationController
try {
$user->save();
// NOTE: We don't verify OAuth email addresses by default because
// OAuth providers might associate email addresses with accounts that
// haven't actually verified they own them. We could selectively
// auto-verify some providers that we trust here, but the stakes for
// verifying an email address are high because having a corporate
// address at a company is sometimes the key to the castle.
$new_email = id(new PhabricatorUserEmail())
->setUserPHID($user->getPHID())
->setAddress($new_email)
->setIsPrimary(1)
->setIsVerified(0)
->save();
$oauth_info->setUserID($user->getID());
$oauth_info->save();
$session_key = $user->establishSession('web');
$request->setCookie('phusr', $user->getUsername());
$request->setCookie('phsid', $session_key);
$new_email->sendVerificationEmail($user);
return id(new AphrontRedirectResponse())->setURI('/');
} catch (AphrontQueryDuplicateKeyException $exception) {
@@ -97,9 +115,9 @@ final class PhabricatorOAuthDefaultRegistrationController
'userName = %s',
$user->getUserName());
$same_email = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
$user->getEmail());
$same_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$new_email);
if ($same_username) {
$e_username = 'Duplicate';

View File

@@ -9,6 +9,7 @@
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/auth/controller/oauthregistration/base');
phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/submit');