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:
@@ -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:
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 = '/';
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user