Store credentials under scope-qualified name

Store the auth credentials under a name qualified by the set of OAuth scopes
as well as the client id.  This is implemented as the base64 encoded SHA1 hash
of the concatenation of client id and sorted auth scopes.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=148127911
This commit is contained in:
mmuller 2017-02-21 12:17:51 -08:00 committed by Ben McIlwain
parent b3b4bba9aa
commit 68bac57da5
2 changed files with 62 additions and 15 deletions

View file

@ -29,15 +29,20 @@ import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.AbstractDataStoreFactory; import com.google.api.client.util.store.AbstractDataStoreFactory;
import com.google.api.client.util.store.FileDataStoreFactory; import com.google.api.client.util.store.FileDataStoreFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
import dagger.Binds; import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.util.Collections; import java.util.Collection;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
import javax.inject.Qualifier; import javax.inject.Qualifier;
@ -60,10 +65,6 @@ import javax.inject.Singleton;
@Module @Module
class DefaultRequestFactoryModule { class DefaultRequestFactoryModule {
// TODO(mmuller): Use @Config("requiredOauthScopes")
private static final String DEFAULT_SCOPE =
"https://www.googleapis.com/auth/userinfo.email";
private static final File DATA_STORE_DIR = private static final File DATA_STORE_DIR =
new File(System.getProperty("user.home"), ".config/nomulus/credentials"); new File(System.getProperty("user.home"), ".config/nomulus/credentials");
@ -124,8 +125,8 @@ class DefaultRequestFactoryModule {
* Module for providing HttpRequestFactory. * Module for providing HttpRequestFactory.
* *
* <p>Localhost connections go to the App Engine dev server. The dev server differs from most HTTP * <p>Localhost connections go to the App Engine dev server. The dev server differs from most HTTP
* connections in that the types whose annotations affect the use of annotaty don't require * connections in that it doesn't require OAuth2 credentials, but instead requires a special
* OAuth2 credentials, but instead require a special cookie. * cookie.
*/ */
@Module @Module
abstract static class RequestFactoryModule { abstract static class RequestFactoryModule {
@ -155,25 +156,23 @@ class DefaultRequestFactoryModule {
static class AuthorizerModule { static class AuthorizerModule {
@Provides @Provides
public Authorizer provideAuthorizer( public Authorizer provideAuthorizer(
final JsonFactory jsonFactory, final AbstractDataStoreFactory dataStoreFactory) { final JsonFactory jsonFactory,
final AbstractDataStoreFactory dataStoreFactory,
@Config("requiredOauthScopes") final ImmutableSet<String> requiredOauthScopes) {
return new Authorizer() { return new Authorizer() {
@Override @Override
public Credential authorize(GoogleClientSecrets clientSecrets) { public Credential authorize(GoogleClientSecrets clientSecrets) {
try { try {
// Run a new auth flow. // Run a new auth flow.
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder( GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
new NetHttpTransport(), jsonFactory, clientSecrets, new NetHttpTransport(), jsonFactory, clientSecrets, requiredOauthScopes)
Collections.singleton(DEFAULT_SCOPE))
.setDataStoreFactory(dataStoreFactory) .setDataStoreFactory(dataStoreFactory)
.build(); .build();
// TODO(mmuller): "credentials" directory needs to be qualified with the scopes and
// client id.
// We pass client id to the authorize method so we can safely persist credentials for
// multiple client ids.
return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()) return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver())
.authorize(clientSecrets.getDetails().getClientId()); .authorize(createClientScopeQualifier(
clientSecrets.getDetails().getClientId(), requiredOauthScopes));
} catch (Exception ex) { } catch (Exception ex) {
throw new RuntimeException(ex); throw new RuntimeException(ex);
} }
@ -182,6 +181,15 @@ class DefaultRequestFactoryModule {
} }
} }
/**
* Create a unique identifier for a given client id and collection of scopes, to be used as an
* identifier for a credential.
*/
@VisibleForTesting
static String createClientScopeQualifier(String clientId, Collection<String> scopes) {
return clientId + " " + Joiner.on(" ").join(Ordering.natural().sortedCopy(scopes));
}
/** /**
* Interface that encapsulates the authorization logic to produce a credential for the user, * Interface that encapsulates the authorization logic to produce a credential for the user,
* allowing us to override the behavior for unit tests. * allowing us to override the behavior for unit tests.

View file

@ -15,6 +15,7 @@
package google.registry.tools; package google.registry.tools;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static google.registry.tools.DefaultRequestFactoryModule.createClientScopeQualifier;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -27,6 +28,7 @@ import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.util.store.AbstractDataStoreFactory; import com.google.api.client.util.store.AbstractDataStoreFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.net.HostAndPort; import com.google.common.net.HostAndPort;
import google.registry.testing.Providers; import google.registry.testing.Providers;
import java.io.IOException; import java.io.IOException;
@ -106,4 +108,41 @@ public class DefaultRequestFactoryModuleTest {
credentialProvider); credentialProvider);
assertThat(factory.getInitializer()).isSameAs(FAKE_CREDENTIAL); assertThat(factory.getInitializer()).isSameAs(FAKE_CREDENTIAL);
} }
@Test
public void test_createClientScopeQualifier() {
String simpleQualifier =
createClientScopeQualifier("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:
// - Names in the new encoding should have a low risk of collision with the old encoding.
// - Changing the encoding will force all OAuth users of the nomulus tool to do a new login
// (existing credentials will not be used).
assertThat(simpleQualifier).isEqualTo("client-id bar foo");
// Verify order independence.
assertThat(simpleQualifier).isEqualTo(
createClientScopeQualifier("client-id", ImmutableList.of("bar", "foo")));
// Verify changing client id produces a different value.
assertThat(simpleQualifier).isNotEqualTo(
createClientScopeQualifier("new-client", ImmutableList.of("bar", "foo")));
// Verify that adding/deleting/modifying scopes produces a different value.
assertThat(simpleQualifier).isNotEqualTo(
createClientScopeQualifier("client id", ImmutableList.of("bar", "foo", "baz")));
assertThat(simpleQualifier).isNotEqualTo(
createClientScopeQualifier("client id", ImmutableList.of("barx", "foo")));
assertThat(simpleQualifier).isNotEqualTo(
createClientScopeQualifier("client id", ImmutableList.of("bar", "foox")));
assertThat(simpleQualifier).isNotEqualTo(
createClientScopeQualifier("client id", ImmutableList.of("bar")));
// Verify that delimiting works.
assertThat(simpleQualifier).isNotEqualTo(
createClientScopeQualifier("client-id", ImmutableList.of("barf", "oo")));
assertThat(simpleQualifier).isNotEqualTo(
createClientScopeQualifier("client-idb", ImmutableList.of("ar", "foo")));
}
} }