diff --git a/oauth2_http/java/com/google/auth/oauth2/AppEngineCredentials.java b/oauth2_http/java/com/google/auth/oauth2/AppEngineCredentials.java index baa2f6530..861adba16 100644 --- a/oauth2_http/java/com/google/auth/oauth2/AppEngineCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/AppEngineCredentials.java @@ -79,18 +79,32 @@ class AppEngineCredentials extends GoogleCredentials implements ServiceAccountSi private transient Method getSignature; private transient String account; - AppEngineCredentials(Collection scopes) throws IOException { - this.scopes = scopes == null ? ImmutableSet.of() : ImmutableList.copyOf(scopes); + AppEngineCredentials(Collection scopes, Collection defaultScopes) + throws IOException { + // Use defaultScopes only when scopes don't exist. + if (scopes == null || scopes.isEmpty()) { + this.scopes = + defaultScopes == null ? ImmutableList.of() : ImmutableList.copyOf(defaultScopes); + } else { + this.scopes = ImmutableList.copyOf(scopes); + } this.scopesRequired = this.scopes.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); + // Use defaultScopes only when scopes don't exist. + if (scopes == null || scopes.isEmpty()) { + this.scopes = + defaultScopes == null ? ImmutableSet.of() : ImmutableList.copyOf(defaultScopes); + } else { + this.scopes = ImmutableList.copyOf(scopes); + } this.scopesRequired = this.scopes.isEmpty(); } @@ -145,7 +159,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 diff --git a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index ede42cee9..01988d1f4 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -109,14 +109,22 @@ 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. Default scopes are ignored if scopes are provided. */ private ComputeEngineCredentials( - HttpTransportFactory transportFactory, Collection scopes) { + HttpTransportFactory transportFactory, + Collection scopes, + Collection defaultScopes) { this.transportFactory = firstNonNull( transportFactory, getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY)); this.transportFactoryClassName = this.transportFactory.getClass().getName(); + // Use defaultScopes only when scopes don't exist. + if (scopes == null || scopes.isEmpty()) { + scopes = defaultScopes; + } if (scopes == null) { this.scopes = ImmutableSet.of(); } else { @@ -129,7 +137,14 @@ private ComputeEngineCredentials( /** 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,7 +153,7 @@ 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() { @@ -465,7 +480,7 @@ public Collection getScopes() { } public ComputeEngineCredentials build() { - return new ComputeEngineCredentials(transportFactory, scopes); + return new ComputeEngineCredentials(transportFactory, scopes, null); } } } 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 3e61e5d60..5a215322f 100644 --- a/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/GoogleCredentials.java @@ -235,6 +235,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..aa9043611 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,27 +106,29 @@ 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. * - * @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 privateKey RSA private key object for the service account. - * @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 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 privateKey RSA private key object for the service account + * @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. + * @param defaultScopes default scope strings for the APIs to be called. May be null or an empty. * @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 Email of the user account to impersonate, if delegating domain-wide + * @param serviceAccountUser email of the user account to impersonate, if delegating domain-wide * authority to the service account. * @param projectId the project used for billing - * @param quotaProjectId The project used for quota and billing purposes. May be null. + * @param quotaProjectId the project used for quota and billing purposes. May be null. * @param lifetime number of seconds the access token should be valid for. The value should be at * most 43200 (12 hours). If the token is used for calling a Google API, then the value should * be at most 3600 (1 hour). If the given value is 0, then the default value 3600 will be used @@ -136,6 +140,7 @@ public class ServiceAccountCredentials extends GoogleCredentials PrivateKey privateKey, String privateKeyId, Collection scopes, + Collection defaultScopes, HttpTransportFactory transportFactory, URI tokenServerUri, String serviceAccountUser, @@ -147,6 +152,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 +167,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 +223,7 @@ static ServiceAccountCredentials fromJson( privateKeyPkcs8, privateKeyId, null, + null, transportFactory, tokenServerUriFromCreds, null, @@ -231,7 +251,51 @@ 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. + * @param defaultScopes default scope strings for the APIs to be called. May be null or an empty. + * @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 +329,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 +413,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 +472,7 @@ static ServiceAccountCredentials fromPkcs8( String privateKeyPkcs8, String privateKeyId, Collection scopes, + Collection defaultScopes, HttpTransportFactory transportFactory, URI tokenServerUri, String serviceAccountUser, @@ -332,6 +486,7 @@ static ServiceAccountCredentials fromPkcs8( privateKey, privateKeyId, scopes, + defaultScopes, transportFactory, tokenServerUri, serviceAccountUser, @@ -411,12 +566,6 @@ public static ServiceAccountCredentials fromStream( */ @Override public AccessToken refreshAccessToken() throws IOException { - if (createScopedRequired()) { - throw new IOException( - "Scopes not configured for service account. Scoped should be specified" - + " by calling createScoped or passing scopes to constructor."); - } - JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; long currentTime = clock.currentTimeMillis(); String assertion = createAssertion(jsonFactory, currentTime, tokenServerUri.toString()); @@ -501,10 +650,14 @@ public IdToken idTokenWithAudience(String targetAudience, List