mirror of
https://github.com/google/nomulus
synced 2026-09-12 11:06:29 +00:00
Change the cookie auth mechanism to use IAP-provided JWTs (#1877)
This commit is contained in:
@@ -106,6 +106,12 @@ public final class RegistryConfig {
|
||||
return config.gcpProject.projectId;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("projectIdNumber")
|
||||
public static long provideProjectIdNumber(RegistryConfigSettings config) {
|
||||
return config.gcpProject.projectIdNumber;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("locationId")
|
||||
public static String provideLocationId(RegistryConfigSettings config) {
|
||||
|
||||
@@ -47,6 +47,7 @@ public class RegistryConfigSettings {
|
||||
/** Configuration options that apply to the entire GCP project. */
|
||||
public static class GcpProject {
|
||||
public String projectId;
|
||||
public long projectIdNumber;
|
||||
public String locationId;
|
||||
public boolean isLocal;
|
||||
public String defaultServiceUrl;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
gcpProject:
|
||||
# Globally unique GCP project ID
|
||||
projectId: registry-project-id
|
||||
# Corresponding project ID number
|
||||
projectIdNumber: 123456789012
|
||||
# Location of the GCP project, note that us-central1 and europe-west1 are special in that
|
||||
# they are used without the trailing number in GCP commands and Google Cloud Console.
|
||||
# See: https://cloud.google.com/appengine/docs/locations as an example
|
||||
|
||||
@@ -14,13 +14,10 @@
|
||||
|
||||
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.auth.oauth2.TokenVerifier;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
@@ -32,12 +29,14 @@ import javax.inject.Singleton;
|
||||
@Module
|
||||
public class AuthModule {
|
||||
|
||||
private static final String IAP_ISSUER_URL = "https://cloud.google.com/iap";
|
||||
|
||||
/** Provides the custom authentication mechanisms (including OAuth). */
|
||||
@Provides
|
||||
ImmutableList<AuthenticationMechanism> provideApiAuthenticationMechanisms(
|
||||
OAuthAuthenticationMechanism oauthAuthenticationMechanism,
|
||||
CookieOAuth2AuthenticationMechanism cookieOAuth2AuthenticationMechanism) {
|
||||
return ImmutableList.of(oauthAuthenticationMechanism, cookieOAuth2AuthenticationMechanism);
|
||||
IapHeaderAuthenticationMechanism iapHeaderAuthenticationMechanism) {
|
||||
return ImmutableList.of(oauthAuthenticationMechanism, iapHeaderAuthenticationMechanism);
|
||||
}
|
||||
|
||||
/** Provides the OAuthService instance. */
|
||||
@@ -48,12 +47,9 @@ public class AuthModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
GoogleIdTokenVerifier provideGoogleIdTokenVerifier(
|
||||
@Config("allowedOauthClientIds") ImmutableSet<String> allowedOauthClientIds,
|
||||
NetHttpTransport httpTransport,
|
||||
JsonFactory jsonFactory) {
|
||||
return new GoogleIdTokenVerifier.Builder(httpTransport, jsonFactory)
|
||||
.setAudience(allowedOauthClientIds)
|
||||
.build();
|
||||
TokenVerifier provideTokenVerifier(
|
||||
@Config("projectId") String projectId, @Config("projectIdNumber") long projectIdNumber) {
|
||||
String audience = String.format("/projects/%d/apps/%s", projectIdNumber, projectId);
|
||||
return TokenVerifier.newBuilder().setAudience(audience).setIssuer(IAP_ISSUER_URL).build();
|
||||
}
|
||||
}
|
||||
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
// 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.
|
||||
*
|
||||
* <p>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<User> 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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.json.webtoken.JsonWebSignature;
|
||||
import com.google.auth.oauth2.TokenVerifier;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.console.User;
|
||||
import google.registry.model.console.UserDao;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* A way to authenticate HTTP requests that have gone through the GCP Identity-Aware Proxy.
|
||||
*
|
||||
* <p>When the user logs in, IAP provides a JWT in the <code>X-Goog-IAP-JWT-Assertion</code> header.
|
||||
* This header is included on all requests to IAP-enabled services (which should be all of them that
|
||||
* receive requests from the front end). The token verification libraries ensure that the signed
|
||||
* token has the proper audience and issuer.
|
||||
*
|
||||
* @see <a href="https://cloud.google.com/iap/docs/signed-headers-howto">the documentation on GCP
|
||||
* IAP's signed headers for more information.</a>
|
||||
*/
|
||||
public class IapHeaderAuthenticationMechanism implements AuthenticationMechanism {
|
||||
|
||||
private static final String ID_TOKEN_HEADER_NAME = "X-Goog-IAP-JWT-Assertion";
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
// A workaround that allows "use" of the IAP-based authenticator when running local testing, i.e.
|
||||
// the RegistryTestServer
|
||||
private static Optional<User> userForTesting = Optional.empty();
|
||||
|
||||
private final TokenVerifier tokenVerifier;
|
||||
|
||||
@Inject
|
||||
public IapHeaderAuthenticationMechanism(TokenVerifier tokenVerifier) {
|
||||
this.tokenVerifier = tokenVerifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthResult authenticate(HttpServletRequest request) {
|
||||
if (RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)
|
||||
&& userForTesting.isPresent()) {
|
||||
return AuthResult.create(AuthLevel.USER, UserAuthInfo.create(userForTesting.get()));
|
||||
}
|
||||
String rawIdToken = request.getHeader(ID_TOKEN_HEADER_NAME);
|
||||
if (rawIdToken == null) {
|
||||
return AuthResult.NOT_AUTHENTICATED;
|
||||
}
|
||||
JsonWebSignature token;
|
||||
try {
|
||||
token = tokenVerifier.verify(rawIdToken);
|
||||
} catch (TokenVerifier.VerificationException e) {
|
||||
logger.atInfo().withCause(e).log("Error when verifying access token");
|
||||
return AuthResult.NOT_AUTHENTICATED;
|
||||
}
|
||||
String emailAddress = (String) token.getPayload().get("email");
|
||||
Optional<User> 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()));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setUserAuthInfoForTestServer(@Nullable User user) {
|
||||
userForTesting = Optional.ofNullable(user);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user