Use self-managed credential in remote api installer

RemoteApiOption has a package-private method that takes a Stream representing the content of a JSON and use a GoogleCredential created from it as its credential. This CL uses reflection to change the access modifier of that method in order to supply a credential stream that is self-managed. This is obviously not ideal and prone to breakage in case the getGoogleCredentialStream method is changed. Unfortunately upstream is not willing to make it public citing the reason that GoogleCredential.fromStream() (which getGoogleCredentialStream uses) is a @Beta annotated function (see https://groups.google.com[]forum/#!searchin/domain-registry-eng/remoteapioptions%7Csort:date/domain-registry-eng/Flsah6skszQ/CySZv2XEBwAJ). However this function is introduced 5 years ago as a public function (b857184bfa). I think at this point it is safe to assume that it is part of the widely used APIs and will not change without sufficient notice.

Note here that RemoteApiOptions creates its own copy of GoogleCredential to be used to call App Engine APIs locally, whereas communications to Nomulus endpoints use the Credential provided in AuthModule. Even though both credentials are created from the same client id, client secret and refresh token (the three elements needed to construct a GoogleCredential this way, see https://github.com/googleapis/google-api-java-client/blob/master/google-api-client/src/main/java/com/google/api/client/googleapis/auth/oauth2/GoogleCredential.java#L842), their refreshes cycles are independent of each other. I verified that refreshing one of the credential does not invalidate the access token of the other credential, as long as it is not expired yet.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=224156131
This commit is contained in:
jianglai 2018-12-05 08:10:29 -08:00
parent aeedc427ad
commit 6352b8a01a
13 changed files with 229 additions and 74 deletions

View file

@ -16,37 +16,63 @@ package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.JUnitBackports.assertThrows;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.api.client.auth.oauth2.ClientParametersAuthentication;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.auth.oauth2.StoredCredential;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.AbstractDataStoreFactory;
import com.google.api.client.util.store.DataStore;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link AuthModule}. */
@RunWith(JUnit4.class)
public class AuthModuleTest {
private static final String TEST_CLIENT_SECRET_FILENAME =
"/google/registry/tools/resources/client_secret_UNITTEST.json";
private static final Credential FAKE_CREDENTIAL = new Credential(
new Credential.AccessMethod() {
@Override
public void intercept(HttpRequest request, String accessToken) {}
private static final String CLIENT_ID = "UNITTEST-CLIENT-ID";
private static final String CLIENT_SECRET = "UNITTEST-CLIENT-SECRET";
private static final String ACCESS_TOKEN = "FakeAccessToken";
private static final String REFRESH_TOKEN = "FakeReFreshToken";
@Override
public String getAccessTokenFromRequest(HttpRequest request) {
return "MockAccessToken";
}
});
private final Credential fakeCredential =
new Credential.Builder(
new Credential.AccessMethod() {
@Override
public void intercept(HttpRequest request, String accessToken) {}
@Override
public String getAccessTokenFromRequest(HttpRequest request) {
return ACCESS_TOKEN;
}
})
// We need to set the following fields because they are checked when
// Credential#setRefreshToken is called. However they are not actually persisted in the
// DataStore and not actually used in tests.
.setJsonFactory(new JacksonFactory())
.setTransport(new NetHttpTransport())
.setTokenServerUrl(new GenericUrl("https://accounts.google.com/o/oauth2/token"))
.setClientAuthentication(new ClientParametersAuthentication(CLIENT_ID, CLIENT_SECRET))
.build();
@SuppressWarnings("unchecked")
DataStore<StoredCredential> dataStore = mock(DataStore.class);
@ -59,11 +85,18 @@ public class AuthModuleTest {
return result;
}
}
@Before
public void setUp() throws Exception {
fakeCredential.setRefreshToken(REFRESH_TOKEN);
when(dataStore.get(CLIENT_ID + " scope1"))
.thenReturn(new StoredCredential(fakeCredential));
}
@Test
public void test_clientScopeQualifier() {
AuthModule authModule = new AuthModule();
String simpleQualifier =
authModule.provideClientScopeQualifier("client-id", ImmutableSet.of("foo", "bar"));
AuthModule.provideClientScopeQualifier("client-id", ImmutableList.of("foo", "bar"));
// If we change the way we encode client id and scopes, this assertion will break. That's
// probably ok and you can just change the text. The things you have to be aware of are:
@ -73,59 +106,81 @@ public class AuthModuleTest {
assertThat(simpleQualifier).isEqualTo("client-id bar foo");
// Verify order independence.
assertThat(simpleQualifier).isEqualTo(
authModule.provideClientScopeQualifier("client-id", ImmutableSet.of("bar", "foo")));
assertThat(simpleQualifier)
.isEqualTo(
AuthModule.provideClientScopeQualifier("client-id", ImmutableList.of("bar", "foo")));
// Verify changing client id produces a different value.
assertThat(simpleQualifier).isNotEqualTo(
authModule.provideClientScopeQualifier("new-client", ImmutableSet.of("bar", "foo")));
assertThat(simpleQualifier)
.isNotEqualTo(
AuthModule.provideClientScopeQualifier("new-client", ImmutableList.of("bar", "foo")));
// Verify that adding/deleting/modifying scopes produces a different value.
assertThat(simpleQualifier).isNotEqualTo(
authModule.provideClientScopeQualifier("client id", ImmutableSet.of("bar", "foo", "baz")));
assertThat(simpleQualifier).isNotEqualTo(
authModule.provideClientScopeQualifier("client id", ImmutableSet.of("barx", "foo")));
assertThat(simpleQualifier).isNotEqualTo(
authModule.provideClientScopeQualifier("client id", ImmutableSet.of("bar", "foox")));
assertThat(simpleQualifier).isNotEqualTo(
authModule.provideClientScopeQualifier("client id", ImmutableSet.of("bar")));
assertThat(simpleQualifier)
.isNotEqualTo(
AuthModule.provideClientScopeQualifier(
"client id", ImmutableList.of("bar", "foo", "baz")));
assertThat(simpleQualifier)
.isNotEqualTo(
AuthModule.provideClientScopeQualifier("client id", ImmutableList.of("barx", "foo")));
assertThat(simpleQualifier)
.isNotEqualTo(
AuthModule.provideClientScopeQualifier("client id", ImmutableList.of("bar", "foox")));
assertThat(simpleQualifier)
.isNotEqualTo(AuthModule.provideClientScopeQualifier("client id", ImmutableList.of("bar")));
// Verify that delimiting works.
assertThat(simpleQualifier).isNotEqualTo(
authModule.provideClientScopeQualifier("client-id", ImmutableSet.of("barf", "oo")));
assertThat(simpleQualifier).isNotEqualTo(
authModule.provideClientScopeQualifier("client-idb", ImmutableSet.of("ar", "foo")));
assertThat(simpleQualifier)
.isNotEqualTo(
AuthModule.provideClientScopeQualifier("client-id", ImmutableList.of("barf", "oo")));
assertThat(simpleQualifier)
.isNotEqualTo(
AuthModule.provideClientScopeQualifier("client-idb", ImmutableList.of("ar", "foo")));
}
private Credential getCredential() {
// Reconstruct the entire dependency graph, injecting FakeDatastoreFactory and credential
// parameters.
AuthModule authModule = new AuthModule();
JacksonFactory jsonFactory = new JacksonFactory();
GoogleClientSecrets clientSecrets =
authModule.provideClientSecrets(TEST_CLIENT_SECRET_FILENAME, jsonFactory);
ImmutableSet<String> scopes = ImmutableSet.of("scope1");
return authModule.provideCredential(
authModule.provideAuthorizationCodeFlow(
GoogleClientSecrets clientSecrets = getSecrets();
ImmutableList<String> scopes = ImmutableList.of("scope1");
return AuthModule.provideCredential(
AuthModule.provideAuthorizationCodeFlow(
jsonFactory, clientSecrets, scopes, new FakeDataStoreFactory()),
authModule.provideClientScopeQualifier(authModule.provideClientId(clientSecrets), scopes));
AuthModule.provideClientScopeQualifier(AuthModule.provideClientId(clientSecrets), scopes));
}
private GoogleClientSecrets getSecrets() {
return AuthModule.provideClientSecrets(TEST_CLIENT_SECRET_FILENAME, new JacksonFactory());
}
@Test
public void test_provideCredential() throws Exception {
when(dataStore.get("UNITTEST-CLIENT-ID scope1")).thenReturn(
new StoredCredential(FAKE_CREDENTIAL));
public void test_provideLocalCredentialStream() {
InputStream jsonStream =
AuthModule.provideLocalCredentialStream(getSecrets(), getCredential()).get();
Map<String, String> jsonMap =
new Gson()
.fromJson(
new InputStreamReader(jsonStream, UTF_8),
new TypeToken<Map<String, String>>() {}.getType());
assertThat(jsonMap.get("type")).isEqualTo("authorized_user");
assertThat(jsonMap.get("client_secret")).isEqualTo(CLIENT_SECRET);
assertThat(jsonMap.get("client_id")).isEqualTo(CLIENT_ID);
assertThat(jsonMap.get("refresh_token")).isEqualTo(REFRESH_TOKEN);
}
@Test
public void test_provideCredential() {
Credential cred = getCredential();
assertThat(cred.getAccessToken()).isEqualTo(FAKE_CREDENTIAL.getAccessToken());
assertThat(cred.getRefreshToken()).isEqualTo(FAKE_CREDENTIAL.getRefreshToken());
assertThat(cred.getAccessToken()).isEqualTo(fakeCredential.getAccessToken());
assertThat(cred.getRefreshToken()).isEqualTo(fakeCredential.getRefreshToken());
assertThat(cred.getExpirationTimeMilliseconds()).isEqualTo(
FAKE_CREDENTIAL.getExpirationTimeMilliseconds());
fakeCredential.getExpirationTimeMilliseconds());
}
@Test
public void test_provideCredential_notStored() {
// Doing this without the mock setup should cause us to throw an exception because the
// credential has not been stored.
public void test_provideCredential_notStored() throws IOException {
when(dataStore.get(CLIENT_ID + " scope1")).thenReturn(null);
assertThrows(AuthModule.LoginRequiredException.class, this::getCredential);
}
}