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(); + } +}