diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index d4e021955c..d58b04a623 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt @@ -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:: + diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java index d2e61186b0..957f33951a 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java @@ -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); } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java index fba7107c85..6a6a692b61 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java @@ -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 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 schema, final Provider currentUser, final ContactStore cs, final AuthConfig ac, final Realm r, final Provider 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 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); diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java index 624768d05d..93968f479b 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java @@ -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() { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java index 88639b7a70..fc189dd446 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java @@ -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 { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java index 179660e41a..1dc97b5fba 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java @@ -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); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java new file mode 100644 index 0000000000..374ae34445 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java @@ -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; + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java index bc967da835..17fe9c6388 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java @@ -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; } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java new file mode 100644 index 0000000000..aad2f76708 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java @@ -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); + } +} diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm index 0e42e01825..7e095fb570 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm @@ -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 diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java index 6a656e83a7..e1155eae08 100644 --- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java +++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java @@ -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