Improve validation of email registration tokens

Embed the sender's account identifier into the token. If a user is
not correctly signed in to Gerrit Code Review when they try to verify
the token the address will not be verified. This may be useful in web
authentication cases where the user signed out of the sending account
in order to sign in to read the email of the destination account.

Also decrease the default token age from 5 days to 12 hours. If a
user doesn't validate the link quickly, it isn't really useful to
allow it to remain out there. Email address may change hands within
5 days (e.g. domain re-register or site admin shifting users around)
but they are less likely to shfit hands in 12 hours.

Change-Id: I36fe2bdf8fbe0afec1c80f129c598a1f47d537dc
This commit is contained in:
Shawn O. Pearce
2012-01-20 12:40:51 -08:00
parent ac59f1eeda
commit d6bd00b5eb
11 changed files with 203 additions and 44 deletions

View File

@@ -206,7 +206,7 @@ order to validate their email address expires.
* y, year, years (`1 year` is treated as `365 days`)
+
Default is 5 days.
Default is 12 hours.
[[auth.httpHeader]]auth.httpHeader::
+

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.httpd.rpc.account;
import com.google.gerrit.httpd.rpc.RpcServletModule;
import com.google.gerrit.httpd.rpc.UiRpcModule;
import com.google.gerrit.server.config.FactoryModule;
import com.google.gerrit.server.mail.RegisterNewEmailSender;
public class AccountModule extends RpcServletModule {
public AccountModule() {
@@ -34,6 +35,7 @@ public class AccountModule extends RpcServletModule {
factory(ExternalIdDetailFactory.Factory.class);
factory(GroupDetailHandler.Factory.class);
factory(MyGroupsFactory.Factory.class);
factory(RegisterNewEmailSender.Factory.class);
factory(RenameGroup.Factory.class);
factory(VisibleGroupsHandler.Factory.class);
}

View File

@@ -46,21 +46,18 @@ import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.contact.ContactStore;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.EmailTokenVerifier;
import com.google.gerrit.server.mail.RegisterNewEmailSender;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwtjsonrpc.client.VoidResult;
import com.google.gwtjsonrpc.server.ValidToken;
import com.google.gwtjsonrpc.server.XsrfException;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.util.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -72,6 +69,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
private final AuthConfig authConfig;
private final Realm realm;
private final Provider<IdentifiedUser> user;
private final EmailTokenVerifier emailTokenVerifier;
private final RegisterNewEmailSender.Factory registerNewEmailFactory;
private final SshKeyCache sshKeyCache;
private final AccountByEmailCache byEmailCache;
@@ -92,6 +90,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
AccountSecurityImpl(final Provider<ReviewDb> schema,
final Provider<CurrentUser> currentUser, final ContactStore cs,
final AuthConfig ac, final Realm r, final Provider<IdentifiedUser> u,
final EmailTokenVerifier etv,
final RegisterNewEmailSender.Factory esf, final SshKeyCache skc,
final AccountByEmailCache abec, final AccountCache uac,
final AccountManager am,
@@ -107,6 +106,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
authConfig = ac;
realm = r;
user = u;
emailTokenVerifier = etv;
registerNewEmailFactory = esf;
sshKeyCache = skc;
byEmailCache = abec;
@@ -308,26 +308,18 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
}
}
public void validateEmail(final String token,
public void validateEmail(final String tokenString,
final AsyncCallback<VoidResult> callback) {
try {
final ValidToken t =
authConfig.getEmailRegistrationToken().checkToken(token, null);
if (t == null || t.getData() == null || "".equals(t.getData())) {
callback.onFailure(new IllegalStateException("Invalid token"));
return;
EmailTokenVerifier.ParsedToken token = emailTokenVerifier.decode(tokenString);
Account.Id currentUser = user.get().getAccountId();
if (currentUser.equals(token.getAccountId())) {
accountManager.link(currentUser, token.toAuthRequest());
callback.onSuccess(VoidResult.INSTANCE);
} else {
throw new EmailTokenVerifier.InvalidTokenException();
}
final String newEmail = new String(Base64.decode(t.getData()), "UTF-8");
if (!newEmail.contains("@")) {
callback.onFailure(new IllegalStateException("Invalid token"));
return;
}
accountManager.link(user.get().getAccountId(), AuthRequest
.forEmail(newEmail));
callback.onSuccess(VoidResult.INSTANCE);
} catch (XsrfException e) {
callback.onFailure(e);
} catch (UnsupportedEncodingException e) {
} catch (EmailTokenVerifier.InvalidTokenException e) {
callback.onFailure(e);
} catch (AccountException e) {
callback.onFailure(e);

View File

@@ -37,6 +37,7 @@ import com.google.gerrit.server.config.MasterNodeStartup;
import com.google.gerrit.server.contact.HttpContactStoreConnection;
import com.google.gerrit.server.git.PushReplication;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.mail.SmtpEmailSender;
import com.google.gerrit.server.schema.SchemaVersionCheck;
import com.google.gerrit.server.ssh.NoSshModule;
@@ -195,6 +196,7 @@ public class Daemon extends SiteProgram {
modules.add(new WorkQueue.Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
modules.add(new SmtpEmailSender.Module());
modules.add(new SignedTokenEmailTokenVerifier.Module());
modules.add(new PushReplication.Module());
if (httpd) {
modules.add(new CanonicalWebUrlModule() {

View File

@@ -61,7 +61,7 @@ public class AuthConfig {
if (key != null && !key.isEmpty()) {
int age = (int) ConfigUtil.getTimeUnit(cfg,
"auth", null, "maxRegisterEmailTokenAge",
TimeUnit.SECONDS.convert(5, TimeUnit.DAYS),
TimeUnit.SECONDS.convert(12, TimeUnit.HOURS),
TimeUnit.SECONDS);
emailReg = new SignedToken(age, key);
} else {

View File

@@ -39,7 +39,6 @@ import com.google.gerrit.server.mail.CommentSender;
import com.google.gerrit.server.mail.CreateChangeSender;
import com.google.gerrit.server.mail.MergeFailSender;
import com.google.gerrit.server.mail.MergedSender;
import com.google.gerrit.server.mail.RegisterNewEmailSender;
import com.google.gerrit.server.mail.ReplacePatchSetSender;
import com.google.gerrit.server.mail.RestoredSender;
import com.google.gerrit.server.mail.RevertedSender;
@@ -94,7 +93,6 @@ public class GerritRequestModule extends FactoryModule {
factory(CommentSender.Factory.class);
factory(MergedSender.Factory.class);
factory(MergeFailSender.Factory.class);
factory(RegisterNewEmailSender.Factory.class);
factory(PerformCreateGroup.Factory.class);
factory(PerformRenameGroup.Factory.class);
factory(VisibleGroups.Factory.class);

View File

@@ -0,0 +1,67 @@
// Copyright 2012 Google Inc. All Rights Reserved.
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.server.account.AuthRequest;
/** Verifies the token sent by {@link RegisterNewEmailSender}. */
public interface EmailTokenVerifier {
/**
* Construct a token to verify an email address for a user.
*
* @param accountId the caller that wants to add an email to their account.
* @param emailAddress the address to add.
* @return an unforgeable string to email to {@code emailAddress}. Presenting
* the string provides proof the user has the ability to read messages
* sent to that address.
*/
public String encode(Account.Id accountId, String emailAddress);
/**
* Decode a token previously created.
* @param tokenString the string created by encode.
* @return a pair of account id and email address.
* @throws InvalidTokenException the token is invalid, expired, malformed, etc.
*/
public ParsedToken decode(String tokenString) throws InvalidTokenException;
/** Exception thrown when a token does not parse correctly. */
public static class InvalidTokenException extends Exception {
public InvalidTokenException() {
super("Invalid token");
}
public InvalidTokenException(Throwable cause) {
super("Invalid token", cause);
}
}
/** Pair returned from decode to provide the data used during encode. */
public static class ParsedToken {
private final Account.Id accountId;
private final String emailAddress;
public ParsedToken(Account.Id accountId, String emailAddress) {
this.accountId = accountId;
this.emailAddress = emailAddress;
}
public Account.Id getAccountId() {
return accountId;
}
public String getEmailAddress() {
return emailAddress;
}
public AuthRequest toAuthRequest() {
return AuthRequest.forEmail(getEmailAddress());
}
@Override
public String toString() {
return accountId + " adds " + emailAddress;
}
}
}

View File

@@ -14,30 +14,30 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtjsonrpc.server.XsrfException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.util.Base64;
import java.io.UnsupportedEncodingException;
public class RegisterNewEmailSender extends OutgoingEmail {
public interface Factory {
public RegisterNewEmailSender create(String address);
}
private final AuthConfig authConfig;
private final EmailTokenVerifier tokenVerifier;
private final IdentifiedUser user;
private final String addr;
private String emailToken;
@Inject
public RegisterNewEmailSender(EmailArguments ea, AuthConfig ac,
public RegisterNewEmailSender(EmailArguments ea,
EmailTokenVerifier etv,
@AnonymousCowardName String anonymousCowardName,
IdentifiedUser callingUser,
@Assisted final String address) {
super(ea, anonymousCowardName, "registernewemail");
authConfig = ac;
tokenVerifier = etv;
user = callingUser;
addr = address;
}
@@ -58,14 +58,29 @@ public class RegisterNewEmailSender extends OutgoingEmail {
appendText(velocifyFile("RegisterNewEmail.vm"));
}
public String getEmailRegistrationToken() {
try {
return authConfig.getEmailRegistrationToken().newToken(
Base64.encodeBytes(addr.getBytes("UTF-8")));
} catch (XsrfException e) {
throw new IllegalArgumentException(e);
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
public String getUserNameEmail() {
String name = user.getAccount().getFullName();
String email = user.getAccount().getPreferredEmail();
if (name != null && email != null) {
return name + " <" + email + ">";
} else if (email != null) {
return email;
} else if (name != null) {
return name;
} else {
String username = user.getUserName();
if (username != null) {
return username;
}
}
return null;
}
public String getEmailRegistrationToken() {
if (emailToken == null) {
emailToken = tokenVerifier.encode(user.getAccountId(), addr);
}
return emailToken;
}
}

View File

@@ -0,0 +1,81 @@
// Copyright 2012 Google Inc. All Rights Reserved.
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtjsonrpc.server.SignedToken;
import com.google.gwtjsonrpc.server.ValidToken;
import com.google.gwtjsonrpc.server.XsrfException;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import org.eclipse.jgit.util.Base64;
import java.io.UnsupportedEncodingException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Verifies the token sent by {@link RegisterNewEmailSender}. */
public class SignedTokenEmailTokenVerifier implements EmailTokenVerifier {
private final SignedToken emailRegistrationToken;
public static class Module extends AbstractModule {
@Override
protected void configure() {
bind(EmailTokenVerifier.class).to(SignedTokenEmailTokenVerifier.class);
}
}
@Inject
SignedTokenEmailTokenVerifier(AuthConfig config) {
emailRegistrationToken = config.getEmailRegistrationToken();
}
public String encode(Account.Id accountId, String emailAddress) {
try {
String payload = String.format("%s:%s", accountId, emailAddress);
byte[] utf8 = payload.getBytes("UTF-8");
String base64 = Base64.encodeBytes(utf8);
return emailRegistrationToken.newToken(base64);
} catch (XsrfException e) {
throw new IllegalArgumentException(e);
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
}
public ParsedToken decode(String tokenString) throws InvalidTokenException {
ValidToken token;
try {
token = emailRegistrationToken.checkToken(tokenString, null);
} catch (XsrfException err) {
throw new InvalidTokenException(err);
}
if (token == null || token.getData() == null || token.getData().isEmpty()) {
throw new InvalidTokenException();
}
String payload;
try {
payload = new String(Base64.decode(token.getData()), "UTF-8");
} catch (UnsupportedEncodingException err) {
throw new InvalidTokenException(err);
}
Matcher matcher = Pattern.compile("^([0-9]+):(.+@.+)$").matcher(payload);
if (!matcher.matches()) {
throw new InvalidTokenException();
}
Account.Id id;
try {
id = Account.Id.parse(matcher.group(1));
} catch (IllegalArgumentException err) {
throw new InvalidTokenException(err);
}
String newEmail = matcher.group(2);
return new ParsedToken(id, newEmail);
}
}

View File

@@ -34,12 +34,12 @@
Welcome to Gerrit Code Review at ${email.gerritHost}.
To add a verified email address to your user account, please
click on the following link:
click on the following link#if($email.userNameEmail) while signed in as $email.userNameEmail#end:
$email.gerritUrl#/VE/$email.emailRegistrationToken
If you have received this mail in error, you do not need to take any
action to cancel the account. The account will not be activated, and
action to cancel the account. The address will not be activated, and
you will not receive any further emails.
If clicking the link above does not work, copy and paste the URL in a

View File

@@ -29,6 +29,7 @@ import com.google.gerrit.server.contact.HttpContactStoreConnection;
import com.google.gerrit.server.git.LocalDiskRepositoryManager;
import com.google.gerrit.server.git.PushReplication;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.mail.SmtpEmailSender;
import com.google.gerrit.server.schema.DataSourceProvider;
import com.google.gerrit.server.schema.DatabaseModule;
@@ -185,6 +186,7 @@ public class WebAppInitializer extends GuiceServletContextListener {
modules.add(new WorkQueue.Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
modules.add(new SmtpEmailSender.Module());
modules.add(new SignedTokenEmailTokenVerifier.Module());
modules.add(new PushReplication.Module());
modules.add(new CanonicalWebUrlModule() {
@Override