diff --git a/core/src/main/java/google/registry/module/backend/BackendComponent.java b/core/src/main/java/google/registry/module/backend/BackendComponent.java
index 97319d770..edd22b947 100644
--- a/core/src/main/java/google/registry/module/backend/BackendComponent.java
+++ b/core/src/main/java/google/registry/module/backend/BackendComponent.java
@@ -39,7 +39,7 @@ import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.persistence.PersistenceModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.rde.JSchModule;
-import google.registry.request.Modules.Jackson2Module;
+import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.request.Modules.UrlFetchServiceModule;
@@ -67,7 +67,7 @@ import javax.inject.Singleton;
GroupsModule.class,
GroupssettingsModule.class,
JSchModule.class,
- Jackson2Module.class,
+ GsonModule.class,
KeyModule.class,
KeyringModule.class,
KmsModule.class,
diff --git a/core/src/main/java/google/registry/module/frontend/FrontendComponent.java b/core/src/main/java/google/registry/module/frontend/FrontendComponent.java
index 3a7c76b7a..49e53e0b0 100644
--- a/core/src/main/java/google/registry/module/frontend/FrontendComponent.java
+++ b/core/src/main/java/google/registry/module/frontend/FrontendComponent.java
@@ -32,7 +32,7 @@ import google.registry.keyring.kms.KmsModule;
import google.registry.module.frontend.FrontendRequestComponent.FrontendRequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
-import google.registry.request.Modules.Jackson2Module;
+import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule;
@@ -56,7 +56,7 @@ import javax.inject.Singleton;
FrontendRequestComponentModule.class,
GroupsModule.class,
GroupssettingsModule.class,
- Jackson2Module.class,
+ GsonModule.class,
KeyModule.class,
KeyringModule.class,
KmsModule.class,
diff --git a/core/src/main/java/google/registry/module/pubapi/PubApiComponent.java b/core/src/main/java/google/registry/module/pubapi/PubApiComponent.java
index b04b03a2c..ef1d9c4ef 100644
--- a/core/src/main/java/google/registry/module/pubapi/PubApiComponent.java
+++ b/core/src/main/java/google/registry/module/pubapi/PubApiComponent.java
@@ -32,7 +32,7 @@ import google.registry.module.pubapi.PubApiRequestComponent.PubApiRequestCompone
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.persistence.PersistenceModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
-import google.registry.request.Modules.Jackson2Module;
+import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule;
@@ -51,7 +51,7 @@ import javax.inject.Singleton;
DummyKeyringModule.class,
GroupsModule.class,
GroupssettingsModule.class,
- Jackson2Module.class,
+ GsonModule.class,
KeyModule.class,
KeyringModule.class,
KmsModule.class,
diff --git a/core/src/main/java/google/registry/module/tools/ToolsComponent.java b/core/src/main/java/google/registry/module/tools/ToolsComponent.java
index 11c58ad0d..547db3bfd 100644
--- a/core/src/main/java/google/registry/module/tools/ToolsComponent.java
+++ b/core/src/main/java/google/registry/module/tools/ToolsComponent.java
@@ -33,7 +33,7 @@ import google.registry.keyring.kms.KmsModule;
import google.registry.module.tools.ToolsRequestComponent.ToolsRequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.privileges.secretmanager.SecretManagerModule;
-import google.registry.request.Modules.Jackson2Module;
+import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule;
@@ -54,7 +54,7 @@ import javax.inject.Singleton;
DriveModule.class,
GroupsModule.class,
GroupssettingsModule.class,
- Jackson2Module.class,
+ GsonModule.class,
KeyModule.class,
KeyringModule.class,
KmsModule.class,
diff --git a/core/src/main/java/google/registry/request/Modules.java b/core/src/main/java/google/registry/request/Modules.java
index ce959b94d..cc7e18c4c 100644
--- a/core/src/main/java/google/registry/request/Modules.java
+++ b/core/src/main/java/google/registry/request/Modules.java
@@ -19,7 +19,7 @@ import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
-import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.client.json.gson.GsonFactory;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import com.google.appengine.api.users.UserService;
@@ -63,17 +63,12 @@ public final class Modules {
}
}
- /**
- * Dagger module that causes the Jackson2 JSON parser to be used for Google APIs requests.
- *
- *
Jackson1 and GSON can also satisfy the {@link JsonFactory} interface, but we've decided to
- * go with Jackson2, since it's what's used in the public examples for using Google APIs.
- */
+ /** Dagger module that causes the Google GSON parser to be used for Google APIs requests. */
@Module
- public static final class Jackson2Module {
+ public static final class GsonModule {
@Provides
static JsonFactory provideJsonFactory() {
- return JacksonFactory.getDefaultInstance();
+ return GsonFactory.getDefaultInstance();
}
}
diff --git a/core/src/main/java/google/registry/request/auth/AuthModule.java b/core/src/main/java/google/registry/request/auth/AuthModule.java
index 21d78db71..9d5456c30 100644
--- a/core/src/main/java/google/registry/request/auth/AuthModule.java
+++ b/core/src/main/java/google/registry/request/auth/AuthModule.java
@@ -14,11 +14,17 @@
package google.registry.request.auth;
+import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.JsonFactory;
import com.google.appengine.api.oauth.OAuthService;
import com.google.appengine.api.oauth.OAuthServiceFactory;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import dagger.Module;
import dagger.Provides;
+import google.registry.config.RegistryConfig.Config;
+import javax.inject.Singleton;
/**
* Dagger module for authentication routines.
@@ -29,8 +35,9 @@ public class AuthModule {
/** Provides the custom authentication mechanisms (including OAuth). */
@Provides
ImmutableList provideApiAuthenticationMechanisms(
- OAuthAuthenticationMechanism oauthAuthenticationMechanism) {
- return ImmutableList.of(oauthAuthenticationMechanism);
+ OAuthAuthenticationMechanism oauthAuthenticationMechanism,
+ CookieOAuth2AuthenticationMechanism cookieOAuth2AuthenticationMechanism) {
+ return ImmutableList.of(oauthAuthenticationMechanism, cookieOAuth2AuthenticationMechanism);
}
/** Provides the OAuthService instance. */
@@ -38,4 +45,15 @@ public class AuthModule {
OAuthService provideOauthService() {
return OAuthServiceFactory.getOAuthService();
}
+
+ @Provides
+ @Singleton
+ GoogleIdTokenVerifier provideGoogleIdTokenVerifier(
+ @Config("allowedOauthClientIds") ImmutableSet allowedOauthClientIds,
+ NetHttpTransport httpTransport,
+ JsonFactory jsonFactory) {
+ return new GoogleIdTokenVerifier.Builder(httpTransport, jsonFactory)
+ .setAudience(allowedOauthClientIds)
+ .build();
+ }
}
diff --git a/core/src/main/java/google/registry/request/auth/CookieOAuth2AuthenticationMechanism.java b/core/src/main/java/google/registry/request/auth/CookieOAuth2AuthenticationMechanism.java
new file mode 100644
index 000000000..4d367360d
--- /dev/null
+++ b/core/src/main/java/google/registry/request/auth/CookieOAuth2AuthenticationMechanism.java
@@ -0,0 +1,89 @@
+// Copyright 2022 The Nomulus Authors. All Rights Reserved.
+//
+// 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 google.registry.request.auth;
+
+import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
+import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
+import com.google.common.flogger.FluentLogger;
+import google.registry.model.console.User;
+import google.registry.model.console.UserDao;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * A way to authenticate HTTP requests using OAuth2 ID tokens stored in cookies.
+ *
+ * This is generic to Google Single-Sign-On and doesn't have any ties with Google App Engine.
+ */
+public class CookieOAuth2AuthenticationMechanism implements AuthenticationMechanism {
+
+ private static final String ID_TOKEN_COOKIE_NAME = "idToken";
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final GoogleIdTokenVerifier googleIdTokenVerifier;
+
+ @Inject
+ public CookieOAuth2AuthenticationMechanism(GoogleIdTokenVerifier googleIdTokenVerifier) {
+ this.googleIdTokenVerifier = googleIdTokenVerifier;
+ }
+
+ @Override
+ public AuthResult authenticate(HttpServletRequest request) {
+ String rawIdToken = getRawIdTokenFromCookie(request);
+ if (rawIdToken == null) {
+ return AuthResult.NOT_AUTHENTICATED;
+ }
+ GoogleIdToken googleIdToken;
+ try {
+ googleIdToken = googleIdTokenVerifier.verify(rawIdToken);
+ } catch (IOException | GeneralSecurityException e) {
+ logger.atInfo().withCause(e).log("Error when verifying access token");
+ return AuthResult.NOT_AUTHENTICATED;
+ }
+ // A null token means the provided ID token was invalid or expired
+ if (googleIdToken == null) {
+ logger.atInfo().log("Token %s failed validation", rawIdToken);
+ return AuthResult.NOT_AUTHENTICATED;
+ }
+ String emailAddress = googleIdToken.getPayload().getEmail();
+ Optional maybeUser = UserDao.loadUser(emailAddress);
+ if (!maybeUser.isPresent()) {
+ logger.atInfo().log("No user found for email address %s", emailAddress);
+ return AuthResult.NOT_AUTHENTICATED;
+ }
+ return AuthResult.create(AuthLevel.USER, UserAuthInfo.create(maybeUser.get()));
+ }
+
+ @Nullable
+ private String getRawIdTokenFromCookie(HttpServletRequest request) {
+ if (request.getCookies() == null) {
+ logger.atInfo().log("No cookies passed in request");
+ return null;
+ }
+ for (Cookie cookie : request.getCookies()) {
+ if (cookie.getName().equals(ID_TOKEN_COOKIE_NAME)) {
+ return cookie.getValue();
+ }
+ }
+ logger.atInfo().log("No ID token cookie");
+ return null;
+ }
+}
diff --git a/core/src/main/java/google/registry/tools/RegistryToolComponent.java b/core/src/main/java/google/registry/tools/RegistryToolComponent.java
index 55f3b55b0..e3ed485d2 100644
--- a/core/src/main/java/google/registry/tools/RegistryToolComponent.java
+++ b/core/src/main/java/google/registry/tools/RegistryToolComponent.java
@@ -36,7 +36,7 @@ import google.registry.persistence.PersistenceModule.ReadOnlyReplicaJpaTm;
import google.registry.persistence.transaction.JpaTransactionManager;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.rde.RdeModule;
-import google.registry.request.Modules.Jackson2Module;
+import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.request.Modules.UrlFetchServiceModule;
import google.registry.request.Modules.UserServiceModule;
@@ -65,7 +65,7 @@ import javax.inject.Singleton;
CloudTasksUtilsModule.class,
DummyKeyringModule.class,
DnsUpdateWriterModule.class,
- Jackson2Module.class,
+ GsonModule.class,
KeyModule.class,
KeyringModule.class,
KmsModule.class,
diff --git a/core/src/test/java/google/registry/request/auth/CookieOAuth2AuthenticationMechanismTest.java b/core/src/test/java/google/registry/request/auth/CookieOAuth2AuthenticationMechanismTest.java
new file mode 100644
index 000000000..f09abbf40
--- /dev/null
+++ b/core/src/test/java/google/registry/request/auth/CookieOAuth2AuthenticationMechanismTest.java
@@ -0,0 +1,107 @@
+// Copyright 2022 The Nomulus Authors. All Rights Reserved.
+//
+// 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 google.registry.request.auth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static google.registry.testing.DatabaseHelper.insertInDb;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
+import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
+import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
+import com.google.api.client.json.webtoken.JsonWebSignature.Header;
+import com.google.common.truth.Truth8;
+import google.registry.model.console.GlobalRole;
+import google.registry.model.console.User;
+import google.registry.model.console.UserRoles;
+import google.registry.persistence.transaction.JpaTestExtensions;
+import java.security.GeneralSecurityException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/** Tests for {@link CookieOAuth2AuthenticationMechanism}. */
+@ExtendWith(MockitoExtension.class)
+public class CookieOAuth2AuthenticationMechanismTest {
+
+ @RegisterExtension
+ public final JpaTestExtensions.JpaUnitTestExtension jpaExtension =
+ new JpaTestExtensions.Builder().withEntityClass(User.class).buildUnitTestExtension();
+
+ @Mock private GoogleIdTokenVerifier tokenVerifier;
+ @Mock private HttpServletRequest request;
+
+ private GoogleIdToken token;
+ private CookieOAuth2AuthenticationMechanism authenticationMechanism;
+
+ @BeforeEach
+ void beforeEach() {
+ authenticationMechanism = new CookieOAuth2AuthenticationMechanism(tokenVerifier);
+ Payload payload = new Payload();
+ payload.setEmail("email@email.com");
+ payload.setSubject("gaiaId");
+ token = new GoogleIdToken(new Header(), payload, new byte[0], new byte[0]);
+ }
+
+ @Test
+ void testSuccess_validUser() throws Exception {
+ User user =
+ new User.Builder()
+ .setEmailAddress("email@email.com")
+ .setGaiaId("gaiaId")
+ .setUserRoles(
+ new UserRoles.Builder().setIsAdmin(true).setGlobalRole(GlobalRole.FTE).build())
+ .build();
+ insertInDb(user);
+ when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("idToken", "asdf")});
+ when(tokenVerifier.verify("asdf")).thenReturn(token);
+ AuthResult authResult = authenticationMechanism.authenticate(request);
+ assertThat(authResult.isAuthenticated()).isTrue();
+ Truth8.assertThat(authResult.userAuthInfo()).isPresent();
+ Truth8.assertThat(authResult.userAuthInfo().get().consoleUser()).hasValue(user);
+ }
+
+ @Test
+ void testFailure_noCookie() {
+ when(request.getCookies()).thenReturn(new Cookie[0]);
+ assertThat(authenticationMechanism.authenticate(request).isAuthenticated()).isFalse();
+ }
+
+ @Test
+ void testFailure_badToken() throws Exception {
+ when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("idToken", "asdf")});
+ when(tokenVerifier.verify("asdf")).thenReturn(null);
+ assertThat(authenticationMechanism.authenticate(request).isAuthenticated()).isFalse();
+ }
+
+ @Test
+ void testFailure_errorVerifyingToken() throws Exception {
+ when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("idToken", "asdf")});
+ when(tokenVerifier.verify("asdf")).thenThrow(new GeneralSecurityException("hi"));
+ assertThat(authenticationMechanism.authenticate(request).isAuthenticated()).isFalse();
+ }
+
+ @Test
+ void testFailure_goodTokenButUnknownUser() throws Exception {
+ when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("idToken", "asdf")});
+ when(tokenVerifier.verify("asdf")).thenReturn(token);
+ assertThat(authenticationMechanism.authenticate(request).isAuthenticated()).isFalse();
+ }
+}