Add Client-side OAuth2 to HTTP connections

Implement client-side OAuth in non-local HTTP connections.  Also add tests to
verify that the different modes of connection are set up correctly.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=147636222
This commit is contained in:
mmuller 2017-02-15 13:22:54 -08:00 committed by Ben McIlwain
parent 32b236e940
commit 177bf4a5f1
9 changed files with 303 additions and 6 deletions

View file

@ -16,6 +16,7 @@ package google.registry.tools;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.net.HostAndPort;
import dagger.Module;
import dagger.Provides;
@ -31,7 +32,15 @@ import google.registry.config.RegistryConfig;
class AppEngineConnectionFlags {
@Parameter(names = "--server", description = "HOST[:PORT] to which remote commands are sent.")
private static HostAndPort server = RegistryConfig.getServer();
private HostAndPort server = RegistryConfig.getServer();
/** Provided for testing. */
@VisibleForTesting
AppEngineConnectionFlags(HostAndPort server) {
this.server = server;
}
AppEngineConnectionFlags() {}
HostAndPort getServer() {
return server;

View file

@ -26,6 +26,11 @@ java_library(
resources = glob([
"*.properties",
"sql/*.sql",
# These are example client secret files. You'll need to obtain your
# own for every environment you use and install them in this
# directory.
"resources/client_secret*.json",
]),
visibility = [":allowed-tools"],
deps = [
@ -65,6 +70,9 @@ java_library(
"@com_google_guava",
"@com_google_http_client",
"@com_google_http_client_jackson2",
"@com_google_oauth_client",
"@com_google_oauth_client_java6",
"@com_google_oauth_client_jetty",
"@com_google_re2j",
"@com_googlecode_json_simple",
"@io_bazel_rules_closure//closure/templates",

View file

@ -14,14 +14,34 @@
package google.registry.tools;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
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.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.AbstractDataStoreFactory;
import com.google.api.client.util.store.FileDataStoreFactory;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.annotation.Documented;
import java.util.Collections;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Qualifier;
import javax.inject.Singleton;
/**
* Module for providing the default HttpRequestFactory.
@ -40,9 +60,50 @@ import javax.inject.Named;
@Module
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 =
new File(System.getProperty("user.home"), ".config/nomulus/credentials");
// TODO(mmuller): replace with a config parameter.
private static final String CLIENT_SECRET_FILENAME =
"/google/registry/tools/resources/client_secret.json";
@Provides
@ClientSecretFilename
String provideClientSecretFilename() {
return CLIENT_SECRET_FILENAME;
}
/** Returns the credential object for the user. */
@Provides
Credential provideCredential(
AbstractDataStoreFactory dataStoreFactory,
Authorizer authorizer,
@ClientSecretFilename String clientSecretFilename) {
try {
// Load the client secrets file.
JacksonFactory jsonFactory = new JacksonFactory();
InputStream secretResourceStream = getClass().getResourceAsStream(clientSecretFilename);
if (secretResourceStream == null) {
throw new RuntimeException("No client secret file found: " + clientSecretFilename);
}
GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(jsonFactory,
new InputStreamReader(secretResourceStream, UTF_8));
return authorizer.authorize(clientSecrets);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
@Provides
@Named("default")
public HttpRequestFactory provideHttpRequestFactory(AppEngineConnectionFlags connectionFlags) {
public HttpRequestFactory provideHttpRequestFactory(
AppEngineConnectionFlags connectionFlags,
Provider<Credential> credentialProvider) {
if (connectionFlags.getServer().getHost().equals("localhost")) {
return new NetHttpTransport()
.createRequestFactory(
@ -55,7 +116,7 @@ class DefaultRequestFactoryModule {
}
});
} else {
return new NetHttpTransport().createRequestFactory();
return new NetHttpTransport().createRequestFactory(credentialProvider.get());
}
}
@ -63,14 +124,77 @@ class DefaultRequestFactoryModule {
* Module for providing HttpRequestFactory.
*
* <p>Localhost connections go to the App Engine dev server. The dev server differs from most HTTP
* connections in that they don't require OAuth2 credentials, but instead require a special
* cookie.
* connections in that the types whose annotations affect the use of annotaty don't require
* OAuth2 credentials, but instead require a special cookie.
*/
@Module
abstract class RequestFactoryModule {
abstract static class RequestFactoryModule {
@Binds
public abstract HttpRequestFactory provideHttpRequestFactory(
@Named("default") HttpRequestFactory requestFactory);
}
@Module
static class DataStoreFactoryModule {
@Provides
@Singleton
public AbstractDataStoreFactory provideDataStoreFactory() {
try {
return new FileDataStoreFactory(DATA_STORE_DIR);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
/**
* Module to create the Authorizer used by DefaultRequestFactoryModule.
*/
@Module
static class AuthorizerModule {
@Provides
public Authorizer provideAuthorizer(
final JsonFactory jsonFactory, final AbstractDataStoreFactory dataStoreFactory) {
return new Authorizer() {
@Override
public Credential authorize(GoogleClientSecrets clientSecrets) {
try {
// Run a new auth flow.
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
new NetHttpTransport(), jsonFactory, clientSecrets,
Collections.singleton(DEFAULT_SCOPE))
.setDataStoreFactory(dataStoreFactory)
.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())
.authorize(clientSecrets.getDetails().getClientId());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
};
}
}
/**
* Interface that encapsulates the authorization logic to produce a credential for the user,
* allowing us to override the behavior for unit tests.
*/
interface Authorizer {
Credential authorize(GoogleClientSecrets clientSecrets);
}
/** Dagger qualifier for the client secret filename.
*
* <p>TODO(mmuller): move this to config.
*/
@Qualifier
@Documented
public @interface ClientSecretFilename {}
}

View file

@ -43,6 +43,8 @@ import javax.inject.Singleton;
DatastoreServiceModule.class,
CloudDnsWriterModule.class,
DefaultRequestFactoryModule.class,
DefaultRequestFactoryModule.AuthorizerModule.class,
DefaultRequestFactoryModule.DataStoreFactoryModule.class,
DefaultRequestFactoryModule.RequestFactoryModule.class,
DnsUpdateWriterModule.class,
DummyKeyringModule.class,

View file

@ -0,0 +1,18 @@
# Adding Client Secrets
To use the nomulus tool to administer a nomulus instance, you will need to
obtain OAuth client ids for each of your environment. There's no reason you
can't use the same client id for all of your environments.
To obtain a client id, go to your project's ["credentials"
page](https://console.developers.google.com/apis/credentials) in the Developer's
Console. Click "Create credentials" and select "OAuth client Id" from the
dropdown. In the create credentials window, select an application type of
"Other."
When you return to the main credentials page, click the download icon to the
right of the client id that you just created. This will download a json file
that you should copy to this directory for all of the environments that you
want to use. Don't copy over the "UNITTEST" secret, otherwise your unit tests
will break.

View file

@ -0,0 +1,11 @@
{
"installed": {
"client_id":"SEE-README.md-IN_THIS_DIRECTORY.apps.googleusercontent.com",
"project_id":"your-registry-server",
"auth_uri":"https://accounts.google.com/o/oauth2/auth",
"token_uri":"https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
"client_secret":"YOUR-CLIENT-SECRET",
"redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]
}
}

View file

@ -0,0 +1,11 @@
{
"installed": {
"client_id":"UNITTEST-CLIENT-ID",
"project_id":"DO NOT CHANGE",
"auth_uri":"https://accounts.google.com/o/oauth2/auth",
"token_uri":"https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
"client_secret":"TBj4EcP5c0609ojiy2DIG6wE",
"redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]
}
}