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

@ -26,11 +26,15 @@ import com.google.api.client.json.JsonFactory;
import com.google.api.client.util.store.AbstractDataStoreFactory;
import com.google.api.client.util.store.FileDataStoreFactory;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Ordering;
import com.google.gson.Gson;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@ -51,9 +55,8 @@ public class AuthModule {
new File(System.getProperty("user.home"), ".config/nomulus/credentials");
@Provides
public Credential provideCredential(
GoogleAuthorizationCodeFlow flow,
@ClientScopeQualifier String clientScopeQualifier) {
public static Credential provideCredential(
GoogleAuthorizationCodeFlow flow, @ClientScopeQualifier String clientScopeQualifier) {
try {
// Try to load the credentials, throw an exception if we fail.
Credential credential = flow.loadCredential(clientScopeQualifier);
@ -67,10 +70,10 @@ public class AuthModule {
}
@Provides
GoogleAuthorizationCodeFlow provideAuthorizationCodeFlow(
public static GoogleAuthorizationCodeFlow provideAuthorizationCodeFlow(
JsonFactory jsonFactory,
GoogleClientSecrets clientSecrets,
@Config("requiredOauthScopes") ImmutableSet<String> requiredOauthScopes,
@Config("localCredentialOauthScopes") ImmutableList<String> requiredOauthScopes,
AbstractDataStoreFactory dataStoreFactory) {
try {
return new GoogleAuthorizationCodeFlow.Builder(
@ -83,17 +86,17 @@ public class AuthModule {
}
@Provides
AuthorizationCodeInstalledApp provideAuthorizationCodeInstalledApp(
public static AuthorizationCodeInstalledApp provideAuthorizationCodeInstalledApp(
GoogleAuthorizationCodeFlow flow) {
return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver());
}
@Provides
GoogleClientSecrets provideClientSecrets(
public static GoogleClientSecrets provideClientSecrets(
@Config("clientSecretFilename") String clientSecretFilename, JsonFactory jsonFactory) {
try {
// Load the client secrets file.
InputStream secretResourceStream = getClass().getResourceAsStream(clientSecretFilename);
InputStream secretResourceStream = AuthModule.class.getResourceAsStream(clientSecretFilename);
if (secretResourceStream == null) {
throw new RuntimeException("No client secret file found: " + clientSecretFilename);
}
@ -105,19 +108,39 @@ public class AuthModule {
}
@Provides
@OAuthClientId String provideClientId(GoogleClientSecrets clientSecrets) {
@LocalCredentialStream
public static Supplier<InputStream> provideLocalCredentialStream(
GoogleClientSecrets clientSecrets, Credential credential) {
String json =
new Gson()
.toJson(
ImmutableMap.<String, String>builder()
.put("type", "authorized_user")
.put("client_id", clientSecrets.getDetails().getClientId())
.put("client_secret", clientSecrets.getDetails().getClientSecret())
.put("refresh_token", credential.getRefreshToken())
.build());
// A supplier is provided so that each binding gets a fresh stream, to avoid contention.
return () -> new ByteArrayInputStream(json.getBytes(UTF_8));
}
@Provides
@OAuthClientId
static String provideClientId(GoogleClientSecrets clientSecrets) {
return clientSecrets.getDetails().getClientId();
}
@Provides
@ClientScopeQualifier String provideClientScopeQualifier(
@OAuthClientId String clientId, @Config("requiredOauthScopes") ImmutableSet<String> scopes) {
@ClientScopeQualifier
static String provideClientScopeQualifier(
@OAuthClientId String clientId,
@Config("localCredentialOauthScopes") ImmutableList<String> scopes) {
return clientId + " " + Joiner.on(" ").join(Ordering.natural().sortedCopy(scopes));
}
@Provides
@Singleton
public AbstractDataStoreFactory provideDataStoreFactory() {
public static AbstractDataStoreFactory provideDataStoreFactory() {
try {
return new FileDataStoreFactory(DATA_STORE_DIR);
} catch (IOException ex) {
@ -125,31 +148,26 @@ public class AuthModule {
}
}
/** Wrapper class to hold the login() function. */
public static class Authorizer {
/** Initiate the login flow. */
public static void login(
GoogleAuthorizationCodeFlow flow,
@ClientScopeQualifier String clientScopeQualifier) throws IOException {
new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver())
.authorize(clientScopeQualifier);
}
/** Raised when we need a user login. */
static class LoginRequiredException extends RuntimeException {
LoginRequiredException() {}
}
/** Raised when we need a user login. */
public static class LoginRequiredException extends RuntimeException {
public LoginRequiredException() {}
}
/** Dagger qualifier for the JSON stream used to create the local credential. */
@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface LocalCredentialStream {}
/** Dagger qualifier for the credential qualifier consisting of client and scopes. */
@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface ClientScopeQualifier {}
@interface ClientScopeQualifier {}
/** Dagger qualifier for the OAuth2 client id. */
@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface OAuthClientId {}
@interface OAuthClientId {}
}