Merge "Upgrade gwtjsonrpc to 1.12" into stable-2.16
diff --git a/WORKSPACE b/WORKSPACE
index 20f02ee..505694a 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -255,9 +255,9 @@
 
 maven_jar(
     name = "gwtjsonrpc",
-    artifact = "com.google.gerrit:gwtjsonrpc:1.11",
-    sha1 = "0990e7eec9eec3a15661edcf9232acbac4aeacec",
-    src_sha1 = "a682afc46284fb58197a173cb5818770a1e7834a",
+    artifact = "com.google.gerrit:gwtjsonrpc:1.12",
+    sha1 = "cade35e5628af56f687d651dd0f43d17d46b20f5",
+    src_sha1 = "e4c17ec9a453f4d41d5e0e55f7020e5919725b0d",
 )
 
 maven_jar(
diff --git a/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java b/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
index 66fe07e..ba9bff8 100644
--- a/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
+++ b/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
@@ -17,9 +17,11 @@
 import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.io.BaseEncoding;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
+import com.google.gwtjsonrpc.common.CheckTokenException;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.gwtjsonrpc.server.ValidToken;
 import com.google.gwtjsonrpc.server.XsrfException;
@@ -28,7 +30,6 @@
 import com.google.inject.Singleton;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import org.eclipse.jgit.util.Base64;
 
 /** Verifies the token sent by {@link RegisterNewEmailSender}. */
 @Singleton
@@ -53,7 +54,7 @@
     try {
       String payload = String.format("%s:%s", accountId, emailAddress);
       byte[] utf8 = payload.getBytes(UTF_8);
-      String base64 = Base64.encodeBytes(utf8);
+      String base64 = BaseEncoding.base64Url().encode(utf8);
       return emailRegistrationToken.newToken(base64);
     } catch (XsrfException e) {
       throw new IllegalArgumentException(e);
@@ -66,14 +67,14 @@
     ValidToken token;
     try {
       token = emailRegistrationToken.checkToken(tokenString, null);
-    } catch (XsrfException err) {
+    } catch (XsrfException | CheckTokenException err) {
       throw new InvalidTokenException(err);
     }
     if (token == null || token.getData() == null || token.getData().isEmpty()) {
       throw new InvalidTokenException();
     }
 
-    String payload = new String(Base64.decode(token.getData()), UTF_8);
+    String payload = new String(BaseEncoding.base64Url().decode(token.getData()), UTF_8);
     Matcher matcher = Pattern.compile("^([0-9]+):(.+@.+)$").matcher(payload);
     if (!matcher.matches()) {
       throw new InvalidTokenException();
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index b09135d..9846b4c 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -1091,7 +1091,7 @@
   @Test
   @GerritConfig(
       name = "auth.registerEmailPrivateKey",
-      value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
+      value = "HsOc6l_2lhS9G7sE_RsnS7Z6GJjdRDX14co=")
   public void addEmailSendsConfirmationEmail() throws Exception {
     String email = "new.email@example.com";
     EmailInput input = newEmailInput(email, false);
@@ -1105,7 +1105,7 @@
   @Test
   @GerritConfig(
       name = "auth.registerEmailPrivateKey",
-      value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
+      value = "HsOc6l_2lhS9G7sE_RsnS7Z6GJjdRDX14co=")
   public void addEmailToBeConfirmedToOwnAccount() throws Exception {
     TestAccount user = accountCreator.create();
     setApiUser(user);
@@ -1129,7 +1129,7 @@
   @Test
   @GerritConfig(
       name = "auth.registerEmailPrivateKey",
-      value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
+      value = "HsOc6l_2lhS9G7sE_RsnS7Z6GJjdRDX14co=")
   public void addEmailToBeConfirmedToOtherAccount() throws Exception {
     TestAccount user = accountCreator.create();
     String email = "me@example.com";
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/SignedTokenEmailTokenVerifierIT.java b/javatests/com/google/gerrit/acceptance/server/mail/SignedTokenEmailTokenVerifierIT.java
new file mode 100644
index 0000000..7335112
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/mail/SignedTokenEmailTokenVerifierIT.java
@@ -0,0 +1,124 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.acceptance.server.mail;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.io.BaseEncoding;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.server.mail.EmailTokenVerifier;
+import com.google.gerrit.server.mail.EmailTokenVerifier.InvalidTokenException;
+import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gwtjsonrpc.server.SignedToken;
+import java.nio.charset.StandardCharsets;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SignedTokenEmailTokenVerifierIT extends AbstractDaemonTest {
+  @ConfigSuite.Default
+  public static Config defaultConfig() {
+    Config cfg = new Config();
+    cfg.setString("auth", null, "registerEmailPrivateKey", SignedToken.generateRandomKey());
+    return cfg;
+  }
+
+  private SignedTokenEmailTokenVerifier signedTokenEmailTokenVerifier;
+
+  @Before
+  public void setUp() throws Exception {
+    signedTokenEmailTokenVerifier =
+        server
+            .getTestInjector()
+            .getBinding(SignedTokenEmailTokenVerifier.class)
+            .getProvider()
+            .get();
+  }
+
+  /** Test encode */
+  @Test
+  public void encode() throws Exception {
+    String tokenString = signedTokenEmailTokenVerifier.encode(user.id, user.email);
+    int index = tokenString.indexOf("$");
+    String text = tokenString.substring(index + 1);
+    String textDecoded = new String(BaseEncoding.base64Url().decode(text), StandardCharsets.UTF_8);
+    int pos = textDecoded.indexOf(":");
+    assertThat(textDecoded.substring(0, pos)).isEqualTo(user.id.toString());
+    assertThat(textDecoded.substring(pos + 1)).isEqualTo(user.email);
+  }
+
+  /** Test decode */
+  @Test
+  public void decode() throws Exception {
+    String tokenString = signedTokenEmailTokenVerifier.encode(user.id, user.email);
+    String tokenKey = tokenString.substring(0, tokenString.indexOf("$"));
+    String text = user.id + ":" + user.email;
+    String invalidTokenString =
+        tokenKey + "$" + BaseEncoding.base64Url().encode(text.getBytes(StandardCharsets.UTF_8));
+    EmailTokenVerifier.ParsedToken parsedToken =
+        signedTokenEmailTokenVerifier.decode(invalidTokenString);
+    assertThat(parsedToken.getAccountId()).isEqualTo(user.id);
+    assertThat(parsedToken.getEmailAddress()).isEqualTo(user.email);
+  }
+
+  /** Test token format is wrong(without '$' to split key and text) */
+  @Test
+  public void invalidFormat() throws Exception {
+    InvalidTokenException thrown =
+        assertThrows(
+            InvalidTokenException.class,
+            () -> signedTokenEmailTokenVerifier.decode("Invalid token"));
+    assertThat(thrown)
+        .hasCauseThat()
+        .hasMessageThat()
+        .isEqualTo("Token does not contain character '$'");
+  }
+
+  /** Test input token string is empty or null */
+  @Test
+  public void emptyInput() throws Exception {
+    InvalidTokenException thrownWithNull =
+        assertThrows(InvalidTokenException.class, () -> signedTokenEmailTokenVerifier.decode(null));
+    InvalidTokenException thrownWithEmpty =
+        assertThrows(InvalidTokenException.class, () -> signedTokenEmailTokenVerifier.decode(""));
+    assertThat(thrownWithNull).hasCauseThat().hasMessageThat().isEqualTo("Empty token");
+    assertThat(thrownWithEmpty).hasCauseThat().hasMessageThat().isEqualTo("Empty token");
+  }
+
+  /** Test token format is right but key is an illegal BASE64 string */
+  @Test
+  public void illegalTokenKey() throws Exception {
+    InvalidTokenException thrown =
+        assertThrows(
+            InvalidTokenException.class,
+            () -> signedTokenEmailTokenVerifier.decode("Illegal token key$...."));
+    assertThat(thrown).hasCauseThat().hasMessageThat().isEqualTo("Token length mismatch");
+  }
+
+  /** Test token text not match the required pattern */
+  @Test
+  public void tokenTextPatternMismatch() throws Exception {
+    String tokenString = signedTokenEmailTokenVerifier.encode(user.id, user.email);
+    String tokenKey = tokenString.substring(0, tokenString.indexOf("$"));
+    String pattern = user.id + ":" + user.email;
+    String invalidTokenTextPattern = tokenKey + "$" + pattern.replace(":", "");
+    InvalidTokenException thrown =
+        assertThrows(
+            InvalidTokenException.class,
+            () -> signedTokenEmailTokenVerifier.decode(invalidTokenTextPattern));
+    assertThat(thrown).hasCauseThat().hasMessageThat().isEqualTo("Token text mismatch");
+  }
+}