Skip to content

Commit

Permalink
Add an optional IAP-enabled ID token when using the Nomulus tool (#1887)
Browse files Browse the repository at this point in the history
We can use the saved refresh token associated with the nomulus tool to
request an ID token with an audience of the IAP client in order to
satisfy IAP with with the Nomulus tool.

Note: this requires that the user of the Nomulus tool, e.g.
"gbrodman@google.com" has a User object stored in SQL.

Tested on QA
  • Loading branch information
gbrodman authored Jan 4, 2023
1 parent 9b24318 commit db95259
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 4 deletions.
6 changes: 6 additions & 0 deletions core/src/main/java/google/registry/config/RegistryConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,12 @@ public static ImmutableSet<String> provideAllowedOauthClientIds(RegistryConfigSe
return ImmutableSet.copyOf(config.oAuth.allowedOauthClientIds);
}

@Provides
@Config("iapClientId")
public static Optional<String> provideIapClientId(RegistryConfigSettings config) {
return Optional.ofNullable(config.oAuth.iapClientId);
}

/**
* Provides the OAuth scopes required for accessing Google APIs using the default credential.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public static class OAuth {
public List<String> availableOauthScopes;
public List<String> requiredOauthScopes;
public List<String> allowedOauthClientIds;
public String iapClientId;
}

/** Configuration options for accessing Google APIs. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ oAuth:
# in this list. Client IDs are typically of the format
# numbers-alphanumerics.apps.googleusercontent.com
allowedOauthClientIds: []
# GCP Identity-Aware Proxy client ID, if set up (note: this requires manual setup
# of User objects in the database for Nomulus tool users)
iapClientId: null

credentialOAuth:
# OAuth scopes required for accessing Google APIs using the default
Expand Down
62 changes: 60 additions & 2 deletions core/src/main/java/google/registry/tools/RequestFactoryModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,23 @@

package google.registry.tools;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.UrlEncodedContent;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.util.GenericData;
import com.google.auth.oauth2.UserCredentials;
import dagger.Module;
import dagger.Provides;
import google.registry.config.CredentialModule.DefaultCredential;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryConfig.Config;
import google.registry.util.GoogleCredentialsBundle;
import java.io.IOException;
import java.net.URI;
import java.util.Optional;

/**
* Module for providing the HttpRequestFactory.
Expand All @@ -33,9 +43,21 @@ class RequestFactoryModule {

static final int REQUEST_TIMEOUT_MS = 10 * 60 * 1000;

/**
* Server to use if we want to manually request an IAP ID token
*
* <p>If we need to have an IAP-enabled audience, we can use the existing refresh token and the
* IAP client ID audience to request an IAP-enabled ID token. This token is read and used by
* {@link google.registry.request.auth.IapHeaderAuthenticationMechanism}, and it requires that the
* user have a {@link google.registry.model.console.User} object present in the database.
*/
private static final GenericUrl TOKEN_SERVER_URL =
new GenericUrl(URI.create("https://oauth2.googleapis.com/token"));

@Provides
static HttpRequestFactory provideHttpRequestFactory(
@DefaultCredential GoogleCredentialsBundle credentialsBundle) {
@DefaultCredential GoogleCredentialsBundle credentialsBundle,
@Config("iapClientId") Optional<String> iapClientId) {
if (RegistryConfig.areServersLocal()) {
return new NetHttpTransport()
.createRequestFactory(
Expand All @@ -47,7 +69,15 @@ static HttpRequestFactory provideHttpRequestFactory(
return new NetHttpTransport()
.createRequestFactory(
request -> {
credentialsBundle.getHttpRequestInitializer().initialize(request);
// If using IAP, use the refresh token to acquire an IAP-enabled ID token and use
// that for authentication.
if (iapClientId.isPresent()) {
String idToken = getIdToken(credentialsBundle, iapClientId.get());
request.getHeaders().setAuthorization("Bearer " + idToken);
} else {
// Otherwise, use the standard credential HTTP initializer
credentialsBundle.getHttpRequestInitializer().initialize(request);
}
// GAE request times out after 10 min, so here we set the timeout to 10 min. This is
// needed to support some nomulus commands like updating premium lists that take
// a lot of time to complete.
Expand All @@ -58,4 +88,32 @@ static HttpRequestFactory provideHttpRequestFactory(
});
}
}

/**
* Uses the saved desktop-app refresh token to acquire an IAP ID token.
*
* <p>This is lifted mostly from the Google Auth Library's {@link UserCredentials}
* "doRefreshAccessToken" method (which is private and thus inaccessible) while adding in the
* audience of the IAP client ID. That addition of the audience is what allows us to satisfy IAP
* auth. See
* https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_desktop_app for
* more details.
*/
private static String getIdToken(GoogleCredentialsBundle credentialsBundle, String iapClientId)
throws IOException {
UserCredentials credentials = (UserCredentials) credentialsBundle.getGoogleCredentials();
GenericData tokenRequest = new GenericData();
tokenRequest.set("client_id", credentials.getClientId());
tokenRequest.set("client_secret", credentials.getClientSecret());
tokenRequest.set("refresh_token", credentials.getRefreshToken());
tokenRequest.set("audience", iapClientId);
tokenRequest.set("grant_type", "refresh_token");
UrlEncodedContent content = new UrlEncodedContent(tokenRequest);

HttpRequestFactory requestFactory = credentialsBundle.getHttpTransport().createRequestFactory();
HttpRequest request = requestFactory.buildPostRequest(TOKEN_SERVER_URL, content);
request.setParser(credentialsBundle.getJsonFactory().createJsonObjectParser());
HttpResponse response = request.execute();
return response.parseAs(GenericData.class).get("id_token").toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import static com.google.common.truth.Truth.assertThat;
import static google.registry.tools.RequestFactoryModule.REQUEST_TIMEOUT_MS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
Expand All @@ -25,9 +27,15 @@
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.util.GenericData;
import com.google.auth.oauth2.UserCredentials;
import google.registry.config.RegistryConfig;
import google.registry.testing.SystemPropertyExtension;
import google.registry.util.GoogleCredentialsBundle;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand Down Expand Up @@ -57,7 +65,7 @@ void test_provideHttpRequestFactory_localhost() throws Exception {
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = true;
try {
HttpRequestFactory factory =
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle);
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, Optional.empty());
HttpRequestInitializer initializer = factory.getInitializer();
assertThat(initializer).isNotNull();
HttpRequest request = factory.buildGetRequest(new GenericUrl("http://localhost"));
Expand All @@ -76,7 +84,7 @@ void test_provideHttpRequestFactory_remote() throws Exception {
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = false;
try {
HttpRequestFactory factory =
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle);
RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, Optional.empty());
HttpRequestInitializer initializer = factory.getInitializer();
assertThat(initializer).isNotNull();
// HttpRequestFactory#buildGetRequest() calls initialize() once.
Expand All @@ -89,4 +97,38 @@ void test_provideHttpRequestFactory_remote() throws Exception {
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = origIsLocal;
}
}

@Test
void test_provideHttpRequestFactory_remote_withIap() throws Exception {
// Mock the request/response to/from the IAP server requesting an ID token
UserCredentials mockUserCredentials = mock(UserCredentials.class);
when(credentialsBundle.getGoogleCredentials()).thenReturn(mockUserCredentials);
HttpTransport mockTransport = mock(HttpTransport.class);
when(credentialsBundle.getHttpTransport()).thenReturn(mockTransport);
when(credentialsBundle.getJsonFactory()).thenReturn(GsonFactory.getDefaultInstance());
HttpRequestFactory mockRequestFactory = mock(HttpRequestFactory.class);
when(mockTransport.createRequestFactory()).thenReturn(mockRequestFactory);
HttpRequest mockPostRequest = mock(HttpRequest.class);
when(mockRequestFactory.buildPostRequest(any(), any())).thenReturn(mockPostRequest);
HttpResponse mockResponse = mock(HttpResponse.class);
when(mockPostRequest.execute()).thenReturn(mockResponse);
GenericData genericDataResponse = new GenericData();
genericDataResponse.set("id_token", "iapIdToken");
when(mockResponse.parseAs(GenericData.class)).thenReturn(genericDataResponse);

boolean origIsLocal = RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal;
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = false;
try {
HttpRequestFactory factory =
RequestFactoryModule.provideHttpRequestFactory(
credentialsBundle, Optional.of("iapClientId"));
HttpRequest request = factory.buildGetRequest(new GenericUrl("http://localhost"));
assertThat(request.getHeaders().getAuthorization()).isEqualTo("Bearer iapIdToken");
assertThat(request.getConnectTimeout()).isEqualTo(REQUEST_TIMEOUT_MS);
assertThat(request.getReadTimeout()).isEqualTo(REQUEST_TIMEOUT_MS);
verifyNoMoreInteractions(httpRequestInitializer);
} finally {
RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = origIsLocal;
}
}
}

0 comments on commit db95259

Please sign in to comment.