From 4d8eca8b0e2b1947dec46930c70d6006d2530d27 Mon Sep 17 00:00:00 2001 From: arithmetic1728 Date: Sat, 13 Feb 2021 22:52:49 -0800 Subject: [PATCH 01/10] feat: add self signed jwt support --- .../auth/oauth2/AppEngineCredentials.java | 36 ++- .../auth/oauth2/ComputeEngineCredentials.java | 55 +++- .../oauth2/DefaultCredentialsProvider.java | 3 +- .../google/auth/oauth2/GoogleCredentials.java | 14 + .../oauth2/ServiceAccountCredentials.java | 240 +++++++++++++++++- .../auth/oauth2/AppEngineCredentialsTest.java | 25 +- .../oauth2/ComputeEngineCredentialsTest.java | 17 ++ .../oauth2/ServiceAccountCredentialsTest.java | 95 ++++++- 8 files changed, 457 insertions(+), 28 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/AppEngineCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AppEngineCredentials.java index baa2f6530..92f1fde8f 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AppEngineCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AppEngineCredentials.java @@ -69,6 +69,7 @@ class AppEngineCredentials extends GoogleCredentials implements ServiceAccountSi private static final String GET_SIGNATURE_METHOD = "getSignature"; private final Collection scopes; + private final Collection defaultScopes; private final boolean scopesRequired; private transient Object appIdentityService; @@ -79,19 +80,25 @@ class AppEngineCredentials extends GoogleCredentials implements ServiceAccountSi private transient Method getSignature; private transient String account; - AppEngineCredentials(Collection scopes) throws IOException { + AppEngineCredentials(Collection scopes, Collection defaultScopes) + throws IOException { this.scopes = scopes == null ? ImmutableSet.of() : ImmutableList.copyOf(scopes); - this.scopesRequired = this.scopes.isEmpty(); + this.defaultScopes = + defaultScopes == null ? ImmutableSet.of() : ImmutableList.copyOf(defaultScopes); + this.scopesRequired = this.scopes.isEmpty() && this.defaultScopes.isEmpty(); init(); } - AppEngineCredentials(Collection scopes, AppEngineCredentials unscoped) { + AppEngineCredentials( + Collection scopes, Collection defaultScopes, AppEngineCredentials unscoped) { this.appIdentityService = unscoped.appIdentityService; this.getAccessToken = unscoped.getAccessToken; this.getAccessTokenResult = unscoped.getAccessTokenResult; this.getExpirationTime = unscoped.getExpirationTime; this.scopes = scopes == null ? ImmutableSet.of() : ImmutableList.copyOf(scopes); - this.scopesRequired = this.scopes.isEmpty(); + this.defaultScopes = + defaultScopes == null ? ImmutableSet.of() : ImmutableList.copyOf(defaultScopes); + this.scopesRequired = this.scopes.isEmpty() && this.defaultScopes.isEmpty(); } private void init() throws IOException { @@ -129,7 +136,11 @@ public AccessToken refreshAccessToken() throws IOException { throw new IOException("AppEngineCredentials requires createScoped call before use."); } try { - Object accessTokenResult = getAccessTokenResult.invoke(appIdentityService, scopes); + Collection scopesToUse = scopes; + if (scopes.isEmpty()) { + scopesToUse = defaultScopes; + } + Object accessTokenResult = getAccessTokenResult.invoke(appIdentityService, scopesToUse); String accessToken = (String) getAccessToken.invoke(accessTokenResult); Date expirationTime = (Date) getExpirationTime.invoke(accessTokenResult); return new AccessToken(accessToken, expirationTime); @@ -145,7 +156,13 @@ public boolean createScopedRequired() { @Override public GoogleCredentials createScoped(Collection scopes) { - return new AppEngineCredentials(scopes, this); + return new AppEngineCredentials(scopes, null, this); + } + + @Override + public GoogleCredentials createScoped( + Collection scopes, Collection defaultScopes) { + return new AppEngineCredentials(scopes, defaultScopes, this); } @Override @@ -165,13 +182,14 @@ public byte[] sign(byte[] toSign) { @Override public int hashCode() { - return Objects.hash(scopes, scopesRequired); + return Objects.hash(scopes, defaultScopes, scopesRequired); } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("scopes", scopes) + .add("defaultScopes", defaultScopes) .add("scopesRequired", scopesRequired) .toString(); } @@ -182,7 +200,9 @@ public boolean equals(Object obj) { return false; } AppEngineCredentials other = (AppEngineCredentials) obj; - return this.scopesRequired == other.scopesRequired && Objects.equals(this.scopes, other.scopes); + return this.scopesRequired == other.scopesRequired + && Objects.equals(this.scopes, other.scopes) + && Objects.equals(this.defaultScopes, other.defaultScopes); } private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { diff --git a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index ede42cee9..b8cd494d9 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -99,6 +99,7 @@ public class ComputeEngineCredentials extends GoogleCredentials private final String transportFactoryClassName; private final Collection scopes; + private final Collection defaultScopes; private transient HttpTransportFactory transportFactory; private transient String serviceAccountEmail; @@ -109,9 +110,13 @@ public class ComputeEngineCredentials extends GoogleCredentials * @param transportFactory HTTP transport factory, creates the transport used to get access * tokens. * @param scopes scope strings for the APIs to be called. May be null or an empty collection. + * @param defaultScopes default scope strings for the APIs to be called. May be null or an empty + * collection. */ private ComputeEngineCredentials( - HttpTransportFactory transportFactory, Collection scopes) { + HttpTransportFactory transportFactory, + Collection scopes, + Collection defaultScopes) { this.transportFactory = firstNonNull( transportFactory, @@ -124,12 +129,26 @@ private ComputeEngineCredentials( scopeList.removeAll(Arrays.asList("", null)); this.scopes = ImmutableSet.copyOf(scopeList); } + if (defaultScopes == null) { + this.defaultScopes = ImmutableSet.of(); + } else { + List scopeList = new ArrayList(defaultScopes); + scopeList.removeAll(Arrays.asList("", null)); + this.defaultScopes = ImmutableSet.copyOf(scopeList); + } } /** Clones the compute engine account with the specified scopes. */ @Override public GoogleCredentials createScoped(Collection newScopes) { - return new ComputeEngineCredentials(this.transportFactory, newScopes); + return new ComputeEngineCredentials(this.transportFactory, newScopes, null); + } + + /** Clones the compute engine account with the specified scopes. */ + @Override + public GoogleCredentials createScoped( + Collection newScopes, Collection newDefaultScopes) { + return new ComputeEngineCredentials(this.transportFactory, newScopes, newDefaultScopes); } /** @@ -138,22 +157,30 @@ public GoogleCredentials createScoped(Collection newScopes) { * @return new ComputeEngineCredentials */ public static ComputeEngineCredentials create() { - return new ComputeEngineCredentials(null, null); + return new ComputeEngineCredentials(null, null, null); } public final Collection getScopes() { return scopes; } + public final Collection getDefaultScopes() { + return defaultScopes; + } + /** - * If scopes is specified, add "?scopes=comma-separated-list-of-scopes" to the token url. + * If scopes or defaultScopes is specified, add "?scopes=comma-separated-list-of-scopes" to the + * token url. defaultScopes is not used unless scopes is empty. * - * @return token url with the given scopes + * @return token url with the given scopes or defaultScopes. defaultScopes is not used unless + * scopes is emtpy. */ String createTokenUrlWithScopes() { GenericUrl tokenUrl = new GenericUrl(getTokenServerEncodedUrl()); if (!scopes.isEmpty()) { tokenUrl.set("scopes", Joiner.on(',').join(scopes)); + } else if (!defaultScopes.isEmpty()) { + tokenUrl.set("scopes", Joiner.on(',').join(defaultScopes)); } return tokenUrl.toString(); } @@ -345,7 +372,8 @@ public boolean equals(Object obj) { } ComputeEngineCredentials other = (ComputeEngineCredentials) obj; return Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName) - && Objects.equals(this.scopes, other.scopes); + && Objects.equals(this.scopes, other.scopes) + && Objects.equals(this.defaultScopes, other.defaultScopes); } private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { @@ -438,12 +466,14 @@ private String getDefaultServiceAccount() throws IOException { public static class Builder extends GoogleCredentials.Builder { private HttpTransportFactory transportFactory; private Collection scopes; + private Collection defaultScopes; protected Builder() {} protected Builder(ComputeEngineCredentials credentials) { this.transportFactory = credentials.transportFactory; this.scopes = credentials.scopes; + this.defaultScopes = credentials.defaultScopes; } public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { @@ -453,6 +483,13 @@ public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { public Builder setScopes(Collection scopes) { this.scopes = scopes; + this.defaultScopes = null; + return this; + } + + public Builder setScopes(Collection scopes, Collection defaultScopes) { + this.scopes = scopes; + this.defaultScopes = defaultScopes; return this; } @@ -464,8 +501,12 @@ public Collection getScopes() { return scopes; } + public Collection getDefaultScopes() { + return defaultScopes; + } + public ComputeEngineCredentials build() { - return new ComputeEngineCredentials(transportFactory, scopes); + return new ComputeEngineCredentials(transportFactory, scopes, defaultScopes); } } } diff --git a/oauth2_http/java/com/google/auth/oauth2/DefaultCredentialsProvider.java b/oauth2_http/java/com/google/auth/oauth2/DefaultCredentialsProvider.java index 78ad73066..12fff6a37 100644 --- a/oauth2_http/java/com/google/auth/oauth2/DefaultCredentialsProvider.java +++ b/oauth2_http/java/com/google/auth/oauth2/DefaultCredentialsProvider.java @@ -301,7 +301,8 @@ private GoogleCredentials tryGetAppEngineCredential() throws IOException { if (!onAppEngine) { return null; } - return new AppEngineCredentials(Collections.emptyList()); + return new AppEngineCredentials( + Collections.emptyList(), Collections.emptyList()); } private final GoogleCredentials tryGetComputeCredentials(HttpTransportFactory transportFactory) { diff --git a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java index c9ea810fb..dc6e033f1 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java @@ -232,6 +232,20 @@ public GoogleCredentials createScoped(Collection scopes) { return this; } + /** + * If the credentials support scopes, creates a copy of the the identity with the specified scopes + * and default scopes; otherwise, returns the same instance. This is mainly used by client + * libraries. + * + * @param scopes Collection of scopes to request. + * @param defaultScopes Collection of default scopes to request. + * @return GoogleCredentials with requested scopes. + */ + public GoogleCredentials createScoped( + Collection scopes, Collection defaultScopes) { + return this; + } + /** * If the credentials support scopes, creates a copy of the the identity with the specified * scopes; otherwise, returns the same instance. diff --git a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java index e12f8d412..2fc546cd0 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ServiceAccountCredentials.java @@ -53,6 +53,7 @@ import com.google.api.client.util.PemReader.Section; import com.google.api.client.util.Preconditions; import com.google.api.client.util.SecurityUtils; +import com.google.auth.RequestMetadataCallback; import com.google.auth.ServiceAccountSigner; import com.google.auth.http.HttpTransportFactory; import com.google.common.annotations.VisibleForTesting; @@ -80,6 +81,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.Executor; /** * OAuth2 credentials representing a Service Account for calling Google APIs. @@ -104,10 +106,12 @@ public class ServiceAccountCredentials extends GoogleCredentials private final String transportFactoryClassName; private final URI tokenServerUri; private final Collection scopes; + private final Collection defaultScopes; private final String quotaProjectId; private final int lifetime; private transient HttpTransportFactory transportFactory; + private transient ServiceAccountJwtAccessCredentials jwtCredentials = null; /** * Constructor with minimum identifying information and custom HTTP transport. @@ -118,6 +122,8 @@ public class ServiceAccountCredentials extends GoogleCredentials * @param privateKeyId Private key identifier for the service account. May be null. * @param scopes Scope strings for the APIs to be called. May be null or an empty collection, * which results in a credential that must have createScoped called before use. + * @param defaultScopes Default scope strings for the APIs to be called. May be null or an empty + * collection, which results in a credential that must have createScoped called before use. * @param transportFactory HTTP transport factory, creates the transport used to get access * tokens. * @param tokenServerUri URI of the end point that provides tokens. @@ -136,6 +142,7 @@ public class ServiceAccountCredentials extends GoogleCredentials PrivateKey privateKey, String privateKeyId, Collection scopes, + Collection defaultScopes, HttpTransportFactory transportFactory, URI tokenServerUri, String serviceAccountUser, @@ -147,6 +154,8 @@ public class ServiceAccountCredentials extends GoogleCredentials this.privateKey = Preconditions.checkNotNull(privateKey); this.privateKeyId = privateKeyId; this.scopes = (scopes == null) ? ImmutableSet.of() : ImmutableSet.copyOf(scopes); + this.defaultScopes = + (defaultScopes == null) ? ImmutableSet.of() : ImmutableSet.copyOf(defaultScopes); this.transportFactory = firstNonNull( transportFactory, @@ -160,6 +169,18 @@ public class ServiceAccountCredentials extends GoogleCredentials throw new IllegalStateException("lifetime must be less than or equal to 43200"); } this.lifetime = lifetime; + + // Use self signed JWT if scopes is not set, see https://google.aip.dev/auth/4111. + if (this.scopes.isEmpty()) { + jwtCredentials = + new ServiceAccountJwtAccessCredentials.Builder() + .setClientEmail(clientEmail) + .setClientId(clientId) + .setPrivateKey(privateKey) + .setPrivateKeyId(privateKeyId) + .setQuotaProjectId(quotaProjectId) + .build(); + } } /** @@ -204,6 +225,7 @@ static ServiceAccountCredentials fromJson( privateKeyPkcs8, privateKeyId, null, + null, transportFactory, tokenServerUriFromCreds, null, @@ -231,7 +253,53 @@ public static ServiceAccountCredentials fromPkcs8( Collection scopes) throws IOException { return fromPkcs8( - clientId, clientEmail, privateKeyPkcs8, privateKeyId, scopes, null, null, null, null, null); + clientId, + clientEmail, + privateKeyPkcs8, + privateKeyId, + scopes, + null, + null, + null, + null, + null, + null); + } + + /** + * Factory with minimum identifying information using PKCS#8 for the private key. + * + * @param clientId Client ID of the service account from the console. May be null. + * @param clientEmail Client email address of the service account from the console. + * @param privateKeyPkcs8 RSA private key object for the service account in PKCS#8 format. + * @param privateKeyId Private key identifier for the service account. May be null. + * @param scopes Scope strings for the APIs to be called. May be null or an empty collection, + * which results in a credential that must have createScoped called before use. + * @param defaultScopes Default scope strings for the APIs to be called. May be null or an empty + * collection, which results in a credential that must have createScoped called before use. + * @return New ServiceAccountCredentials created from a private key. + * @throws IOException if the credential cannot be created from the private key. + */ + public static ServiceAccountCredentials fromPkcs8( + String clientId, + String clientEmail, + String privateKeyPkcs8, + String privateKeyId, + Collection scopes, + Collection defaultScopes) + throws IOException { + return fromPkcs8( + clientId, + clientEmail, + privateKeyPkcs8, + privateKeyId, + scopes, + defaultScopes, + null, + null, + null, + null, + null); } /** @@ -265,6 +333,49 @@ public static ServiceAccountCredentials fromPkcs8( privateKeyPkcs8, privateKeyId, scopes, + null, + transportFactory, + tokenServerUri, + null, + null, + null); + } + + /** + * Factory with minimum identifying information and custom transport using PKCS#8 for the private + * key. + * + * @param clientId Client ID of the service account from the console. May be null. + * @param clientEmail Client email address of the service account from the console. + * @param privateKeyPkcs8 RSA private key object for the service account in PKCS#8 format. + * @param privateKeyId Private key identifier for the service account. May be null. + * @param scopes Scope strings for the APIs to be called. May be null or an empty collection, + * which results in a credential that must have createScoped called before use. + * @param defaultScopes Default scope strings for the APIs to be called. May be null or an empty + * collection, which results in a credential that must have createScoped called before use. + * @param transportFactory HTTP transport factory, creates the transport used to get access + * tokens. + * @param tokenServerUri URI of the end point that provides tokens. + * @return New ServiceAccountCredentials created from a private key. + * @throws IOException if the credential cannot be created from the private key. + */ + public static ServiceAccountCredentials fromPkcs8( + String clientId, + String clientEmail, + String privateKeyPkcs8, + String privateKeyId, + Collection scopes, + Collection defaultScopes, + HttpTransportFactory transportFactory, + URI tokenServerUri) + throws IOException { + return fromPkcs8( + clientId, + clientEmail, + privateKeyPkcs8, + privateKeyId, + scopes, + defaultScopes, transportFactory, tokenServerUri, null, @@ -306,6 +417,52 @@ public static ServiceAccountCredentials fromPkcs8( privateKeyPkcs8, privateKeyId, scopes, + null, + transportFactory, + tokenServerUri, + serviceAccountUser, + null, + null); + } + + /** + * Factory with minimum identifying information and custom transport using PKCS#8 for the private + * key. + * + * @param clientId Client ID of the service account from the console. May be null. + * @param clientEmail Client email address of the service account from the console. + * @param privateKeyPkcs8 RSA private key object for the service account in PKCS#8 format. + * @param privateKeyId Private key identifier for the service account. May be null. + * @param scopes Scope strings for the APIs to be called. May be null or an empty collection, + * which results in a credential that must have createScoped called before use. + * @param defaultScopes Default scope strings for the APIs to be called. May be null or an empty + * collection, which results in a credential that must have createScoped called before use. + * @param transportFactory HTTP transport factory, creates the transport used to get access + * tokens. + * @param tokenServerUri URI of the end point that provides tokens. + * @param serviceAccountUser The email of the user account to impersonate, if delegating + * domain-wide authority to the service account. + * @return New ServiceAccountCredentials created from a private key. + * @throws IOException if the credential cannot be created from the private key. + */ + public static ServiceAccountCredentials fromPkcs8( + String clientId, + String clientEmail, + String privateKeyPkcs8, + String privateKeyId, + Collection scopes, + Collection defaultScopes, + HttpTransportFactory transportFactory, + URI tokenServerUri, + String serviceAccountUser) + throws IOException { + return fromPkcs8( + clientId, + clientEmail, + privateKeyPkcs8, + privateKeyId, + scopes, + defaultScopes, transportFactory, tokenServerUri, serviceAccountUser, @@ -319,6 +476,7 @@ static ServiceAccountCredentials fromPkcs8( String privateKeyPkcs8, String privateKeyId, Collection scopes, + Collection defaultScopes, HttpTransportFactory transportFactory, URI tokenServerUri, String serviceAccountUser, @@ -332,6 +490,7 @@ static ServiceAccountCredentials fromPkcs8( privateKey, privateKeyId, scopes, + defaultScopes, transportFactory, tokenServerUri, serviceAccountUser, @@ -504,7 +663,7 @@ public IdToken idTokenWithAudience(String targetAudience, List