All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.auth.oauth2.ServiceAccountCredentials Maven / Gradle / Ivy

There is a newer version: 1.30.0
Show newest version
/*
 * Copyright 2015, Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *    * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *
 *    * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.google.auth.oauth2;

import static com.google.common.base.MoreObjects.firstNonNull;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpBackOffIOExceptionHandler;
import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler;
import com.google.api.client.http.HttpContent;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.UrlEncodedContent;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.api.client.json.webtoken.JsonWebToken;
import com.google.api.client.util.ExponentialBackOff;
import com.google.api.client.util.GenericData;
import com.google.api.client.util.Joiner;
import com.google.api.client.util.Preconditions;
import com.google.auth.Credentials;
import com.google.auth.RequestMetadataCallback;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.http.AuthHttpConstants;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects.ToStringHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
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.
 *
 * 

By default uses a JSON Web Token (JWT) to fetch access tokens. */ public class ServiceAccountCredentials extends GoogleCredentials implements ServiceAccountSigner, IdTokenProvider, JwtProvider { private static final long serialVersionUID = 7807543542681217978L; private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"; private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. "; private static final int TWELVE_HOURS_IN_SECONDS = 43200; private static final int DEFAULT_LIFETIME_IN_SECONDS = 3600; private final String clientId; private final String clientEmail; private final PrivateKey privateKey; private final String privateKeyId; private final String serviceAccountUser; private final String projectId; private final String transportFactoryClassName; private final URI tokenServerUri; private final Collection scopes; private final Collection defaultScopes; private final int lifetime; private final boolean useJwtAccessWithScope; private final boolean defaultRetriesEnabled; private transient HttpTransportFactory transportFactory; private transient JwtCredentials selfSignedJwtCredentialsWithScope = null; /** * Internal constructor * * @param builder A builder for {@link ServiceAccountCredentials} See {@link * ServiceAccountCredentials.Builder} */ ServiceAccountCredentials(ServiceAccountCredentials.Builder builder) { super(builder); this.clientId = builder.clientId; this.clientEmail = Preconditions.checkNotNull(builder.clientEmail); this.privateKey = Preconditions.checkNotNull(builder.privateKey); this.privateKeyId = builder.privateKeyId; this.scopes = (builder.scopes == null) ? ImmutableSet.of() : ImmutableSet.copyOf(builder.scopes); this.defaultScopes = (builder.defaultScopes == null) ? ImmutableSet.of() : ImmutableSet.copyOf(builder.defaultScopes); this.transportFactory = firstNonNull( builder.transportFactory, getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY)); this.transportFactoryClassName = this.transportFactory.getClass().getName(); this.tokenServerUri = (builder.tokenServerUri == null) ? OAuth2Utils.TOKEN_SERVER_URI : builder.tokenServerUri; this.serviceAccountUser = builder.serviceAccountUser; this.projectId = builder.projectId; if (builder.lifetime > TWELVE_HOURS_IN_SECONDS) { throw new IllegalStateException("lifetime must be less than or equal to 43200"); } this.lifetime = builder.lifetime; this.useJwtAccessWithScope = builder.useJwtAccessWithScope; this.defaultRetriesEnabled = builder.defaultRetriesEnabled; } /** * Returns service account credentials defined by JSON using the format supported by the Google * Developers Console. * * @param json a map from the JSON representing the credentials. * @param transportFactory HTTP transport factory, creates the transport used to get access * tokens. * @return the credentials defined by the JSON. * @throws IOException if the credential cannot be created from the JSON. */ static ServiceAccountCredentials fromJson( Map json, HttpTransportFactory transportFactory) throws IOException { String clientId = (String) json.get("client_id"); String clientEmail = (String) json.get("client_email"); String privateKeyPkcs8 = (String) json.get("private_key"); String privateKeyId = (String) json.get("private_key_id"); String projectId = (String) json.get("project_id"); String tokenServerUriStringFromCreds = (String) json.get("token_uri"); String quotaProjectId = (String) json.get("quota_project_id"); String universeDomain = (String) json.get("universe_domain"); URI tokenServerUriFromCreds = null; try { if (tokenServerUriStringFromCreds != null) { tokenServerUriFromCreds = new URI(tokenServerUriStringFromCreds); } } catch (URISyntaxException e) { throw new IOException("Token server URI specified in 'token_uri' could not be parsed."); } if (clientId == null || clientEmail == null || privateKeyPkcs8 == null || privateKeyId == null) { throw new IOException( "Error reading service account credential from JSON, " + "expecting 'client_id', 'client_email', 'private_key' and 'private_key_id'."); } ServiceAccountCredentials.Builder builder = ServiceAccountCredentials.newBuilder() .setClientId(clientId) .setClientEmail(clientEmail) .setPrivateKeyId(privateKeyId) .setHttpTransportFactory(transportFactory) .setTokenServerUri(tokenServerUriFromCreds) .setProjectId(projectId) .setQuotaProjectId(quotaProjectId) .setUniverseDomain(universeDomain); return fromPkcs8(privateKeyPkcs8, builder); } /** * 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. * @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) throws IOException { ServiceAccountCredentials.Builder builder = ServiceAccountCredentials.newBuilder() .setClientId(clientId) .setClientEmail(clientEmail) .setPrivateKeyId(privateKeyId) .setScopes(scopes); return fromPkcs8(privateKeyPkcs8, builder); } /** * 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 { ServiceAccountCredentials.Builder builder = ServiceAccountCredentials.newBuilder() .setClientId(clientId) .setClientEmail(clientEmail) .setPrivateKeyId(privateKeyId) .setScopes(scopes, defaultScopes); return fromPkcs8(privateKeyPkcs8, builder); } /** * 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 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, HttpTransportFactory transportFactory, URI tokenServerUri) throws IOException { ServiceAccountCredentials.Builder builder = ServiceAccountCredentials.newBuilder() .setClientId(clientId) .setClientEmail(clientEmail) .setPrivateKeyId(privateKeyId) .setScopes(scopes) .setHttpTransportFactory(transportFactory) .setTokenServerUri(tokenServerUri); return fromPkcs8(privateKeyPkcs8, builder); } /** * 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 { ServiceAccountCredentials.Builder builder = ServiceAccountCredentials.newBuilder() .setClientId(clientId) .setClientEmail(clientEmail) .setPrivateKeyId(privateKeyId) .setScopes(scopes, defaultScopes) .setHttpTransportFactory(transportFactory) .setTokenServerUri(tokenServerUri); return fromPkcs8(privateKeyPkcs8, builder); } /** * 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 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, HttpTransportFactory transportFactory, URI tokenServerUri, String serviceAccountUser) throws IOException { ServiceAccountCredentials.Builder builder = ServiceAccountCredentials.newBuilder() .setClientId(clientId) .setClientEmail(clientEmail) .setPrivateKeyId(privateKeyId) .setScopes(scopes) .setHttpTransportFactory(transportFactory) .setTokenServerUri(tokenServerUri) .setServiceAccountUser(serviceAccountUser); return fromPkcs8(privateKeyPkcs8, builder); } /** * 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 { ServiceAccountCredentials.Builder builder = ServiceAccountCredentials.newBuilder() .setClientId(clientId) .setClientEmail(clientEmail) .setPrivateKeyId(privateKeyId) .setScopes(scopes, defaultScopes) .setHttpTransportFactory(transportFactory) .setTokenServerUri(tokenServerUri) .setServiceAccountUser(serviceAccountUser); return fromPkcs8(privateKeyPkcs8, builder); } /** * Internal constructor * * @param privateKeyPkcs8 RSA private key object for the service account in PKCS#8 format. * @param builder A builder for {@link ServiceAccountCredentials} See {@link * ServiceAccountCredentials.Builder} * @return an instance of {@link ServiceAccountCredentials} */ static ServiceAccountCredentials fromPkcs8( String privateKeyPkcs8, ServiceAccountCredentials.Builder builder) throws IOException { PrivateKey privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8); builder.setPrivateKey(privateKey); return new ServiceAccountCredentials(builder); } /** * Returns credentials defined by a Service Account key file in JSON format from the Google * Developers Console. * * @param credentialsStream the stream with the credential definition. * @return the credential defined by the credentialsStream. * @throws IOException if the credential cannot be created from the stream. */ public static ServiceAccountCredentials fromStream(InputStream credentialsStream) throws IOException { return fromStream(credentialsStream, OAuth2Utils.HTTP_TRANSPORT_FACTORY); } /** * Returns credentials defined by a Service Account key file in JSON format from the Google * Developers Console. * * @param credentialsStream the stream with the credential definition. * @param transportFactory HTTP transport factory, creates the transport used to get access * tokens. * @return the credential defined by the credentialsStream. * @throws IOException if the credential cannot be created from the stream. */ public static ServiceAccountCredentials fromStream( InputStream credentialsStream, HttpTransportFactory transportFactory) throws IOException { ServiceAccountCredentials credential = (ServiceAccountCredentials) GoogleCredentials.fromStream(credentialsStream, transportFactory); if (credential == null) { throw new IOException( String.format( "Error reading credentials from stream, ServiceAccountCredentials type is not recognized.")); } return credential; } /** Returns whether the scopes are empty, meaning createScoped must be called before use. */ @Override public boolean createScopedRequired() { return scopes.isEmpty() && defaultScopes.isEmpty(); } /** Returns true if credential is configured domain wide delegation */ @VisibleForTesting boolean isConfiguredForDomainWideDelegation() { return serviceAccountUser != null && serviceAccountUser.length() > 0; } /** * Refreshes the OAuth2 access token by getting a new access token using a JSON Web Token (JWT). */ @Override public AccessToken refreshAccessToken() throws IOException { JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; long currentTime = clock.currentTimeMillis(); String assertion = createAssertion(jsonFactory, currentTime); GenericData tokenRequest = new GenericData(); tokenRequest.set("grant_type", GRANT_TYPE); tokenRequest.set("assertion", assertion); UrlEncodedContent content = new UrlEncodedContent(tokenRequest); HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory(); HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(tokenServerUri), content); if (this.defaultRetriesEnabled) { request.setNumberOfRetries(OAuth2Utils.DEFAULT_NUMBER_OF_RETRIES); } else { request.setNumberOfRetries(0); } request.setParser(new JsonObjectParser(jsonFactory)); ExponentialBackOff backoff = new ExponentialBackOff.Builder() .setInitialIntervalMillis(OAuth2Utils.INITIAL_RETRY_INTERVAL_MILLIS) .setRandomizationFactor(OAuth2Utils.RETRY_RANDOMIZATION_FACTOR) .setMultiplier(OAuth2Utils.RETRY_MULTIPLIER) .build(); request.setUnsuccessfulResponseHandler( new HttpBackOffUnsuccessfulResponseHandler(backoff) .setBackOffRequired( response -> { int code = response.getStatusCode(); return OAuth2Utils.TOKEN_ENDPOINT_RETRYABLE_STATUS_CODES.contains(code); })); request.setIOExceptionHandler(new HttpBackOffIOExceptionHandler(backoff)); HttpResponse response; String errorTemplate = "Error getting access token for service account: %s, iss: %s"; try { response = request.execute(); } catch (HttpResponseException re) { String message = String.format(errorTemplate, re.getMessage(), getIssuer()); throw GoogleAuthException.createWithTokenEndpointResponseException(re, message); } catch (IOException e) { throw GoogleAuthException.createWithTokenEndpointIOException( e, String.format(errorTemplate, e.getMessage(), getIssuer())); } GenericData responseData = response.parseAs(GenericData.class); String accessToken = OAuth2Utils.validateString(responseData, "access_token", PARSE_ERROR_PREFIX); int expiresInSeconds = OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX); long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000L; return new AccessToken(accessToken, new Date(expiresAtMilliseconds)); } /** * Returns a Google ID Token from either the Oauth or IAM Endpoint. For Credentials that are in * the Google Default Universe (googleapis.com), the ID Token will be retrieved from the Oauth * Endpoint. Otherwise, it will be retrieved from the IAM Endpoint. * * @param targetAudience the aud: field the IdToken should include. * @param options list of Credential specific options for the token. Currently, unused for * ServiceAccountCredentials. * @throws IOException if the attempt to get an IdToken failed * @return IdToken object which includes the raw id_token, expiration and audience */ @Override public IdToken idTokenWithAudience(String targetAudience, List

This flow works as follows: * *

    *
  1. Create a self-signed jwt with `https://www.googleapis.com/auth/iam` as the scope. *
  2. Use the self-signed jwt as the access token, and make a POST request to IAM * generateIdToken endpoint. *
  3. If the request is successfully, it will return {"token":"the ID token"}. Extract the ID * token. *
*/ private IdToken getIdTokenIamEndpoint(String targetAudience) throws IOException { JwtCredentials selfSignedJwtCredentials = createSelfSignedJwtCredentials( null, ImmutableList.of("https://www.googleapis.com/auth/iam")); Map> responseMetadata = selfSignedJwtCredentials.getRequestMetadata(null); // JwtCredentials will return a map with one entry ("Authorization" -> List with size 1) String accessToken = responseMetadata.get(AuthHttpConstants.AUTHORIZATION).get(0); // Do not check user options. These params are always set regardless of options configured Map requestParams = ImmutableMap.of("audience", targetAudience, "includeEmail", "true", "useEmailAzp", "true"); GenericData tokenRequest = new GenericData(); requestParams.forEach(tokenRequest::set); UrlEncodedContent content = new UrlEncodedContent(tokenRequest); // Create IAM Token URI in this method instead of in the constructor because // `getUniverseDomain()` throws an IOException that would need to be caught URI iamIdTokenUri = URI.create( String.format( OAuth2Utils.IAM_ID_TOKEN_ENDPOINT_FORMAT, getUniverseDomain(), clientEmail)); HttpRequest request = buildIdTokenRequest(iamIdTokenUri, transportFactory, content); // Use the Access Token from the SSJWT to request the ID Token from IAM Endpoint request.setHeaders(new HttpHeaders().set(AuthHttpConstants.AUTHORIZATION, accessToken)); HttpResponse httpResponse = executeRequest(request); GenericData responseData = httpResponse.parseAs(GenericData.class); // IAM Endpoint returns `token` instead of `id_token` String rawToken = OAuth2Utils.validateString(responseData, "token", PARSE_ERROR_PREFIX); return IdToken.create(rawToken); } // Build a default POST HttpRequest to be used for both Oauth and IAM endpoints private HttpRequest buildIdTokenRequest( URI uri, HttpTransportFactory transportFactory, HttpContent content) throws IOException { JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory(); HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(uri), content); request.setParser(new JsonObjectParser(jsonFactory)); return request; } private HttpResponse executeRequest(HttpRequest request) throws IOException { HttpResponse response; try { response = request.execute(); } catch (IOException e) { throw new IOException( String.format( "Error getting id token for service account: %s, iss: %s", e.getMessage(), getIssuer()), e); } return response; } /** * Clones the service account with the specified default retries. * * @param defaultRetriesEnabled a flag enabling or disabling default retries * @return GoogleCredentials with the specified retry configuration. */ @Override public ServiceAccountCredentials createWithCustomRetryStrategy(boolean defaultRetriesEnabled) { return this.toBuilder().setDefaultRetriesEnabled(defaultRetriesEnabled).build(); } /** * Clones the service account with the specified scopes. * *

Should be called before use for instances with empty scopes. */ @Override public GoogleCredentials createScoped(Collection newScopes) { return createScoped(newScopes, null); } /** * Clones the service account with the specified scopes. The Access Token is invalidated even if * the same scopes are provided. Access Tokens contain information of the internal values (i.e. * scope). If an internal value (scope) is modified, then the existing Access Token is no longer * valid and should not be re-used. * *

Should be called before use for instances with empty scopes. */ @Override public GoogleCredentials createScoped( Collection newScopes, Collection newDefaultScopes) { return this.toBuilder().setScopes(newScopes, newDefaultScopes).setAccessToken(null).build(); } /** * Clones the service account with a new lifetime value. * * @param lifetime life time value in seconds. 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 when creating the * credentials. * @return the cloned service account credentials with the given custom life time */ public ServiceAccountCredentials createWithCustomLifetime(int lifetime) { return this.toBuilder().setLifetime(lifetime).build(); } /** * Clones the service account with a new useJwtAccessWithScope value. This flag will be ignored if * universeDomain field is different from {@link Credentials#GOOGLE_DEFAULT_UNIVERSE}. * * @param useJwtAccessWithScope whether self-signed JWT with scopes should be used * @return the cloned service account credentials with the given useJwtAccessWithScope */ public ServiceAccountCredentials createWithUseJwtAccessWithScope(boolean useJwtAccessWithScope) { return this.toBuilder().setUseJwtAccessWithScope(useJwtAccessWithScope).build(); } @Override public GoogleCredentials createDelegated(String user) { return this.toBuilder().setServiceAccountUser(user).build(); } public final String getClientId() { return clientId; } public final String getClientEmail() { return clientEmail; } public final PrivateKey getPrivateKey() { return privateKey; } public final String getPrivateKeyId() { return privateKeyId; } public final Collection getScopes() { return scopes; } public final Collection getDefaultScopes() { return defaultScopes; } public final String getServiceAccountUser() { return serviceAccountUser; } public final String getProjectId() { return projectId; } public final URI getTokenServerUri() { return tokenServerUri; } private String getIssuer() { return this.clientEmail; } @VisibleForTesting int getLifetime() { return lifetime; } public boolean getUseJwtAccessWithScope() { return useJwtAccessWithScope; } @VisibleForTesting JwtCredentials getSelfSignedJwtCredentialsWithScope() { return selfSignedJwtCredentialsWithScope; } @Override public String getAccount() { return getClientEmail(); } @Override public byte[] sign(byte[] toSign) { try { Signature signer = Signature.getInstance(OAuth2Utils.SIGNATURE_ALGORITHM); signer.initSign(getPrivateKey()); signer.update(toSign); return signer.sign(); } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) { throw new SigningException("Failed to sign the provided bytes", ex); } } /** * Returns a new JwtCredentials instance with modified claims. * * @param newClaims new claims. Any unspecified claim fields will default to the current values. * @return new credentials */ @Override public JwtCredentials jwtWithClaims(JwtClaims newClaims) { JwtClaims.Builder claimsBuilder = JwtClaims.newBuilder().setIssuer(getIssuer()).setSubject(clientEmail); return JwtCredentials.newBuilder() .setPrivateKey(privateKey) .setPrivateKeyId(privateKeyId) .setJwtClaims(claimsBuilder.build().merge(newClaims)) .setClock(clock) .build(); } @Override public int hashCode() { return Objects.hash( clientId, clientEmail, privateKey, privateKeyId, transportFactoryClassName, tokenServerUri, scopes, defaultScopes, lifetime, useJwtAccessWithScope, defaultRetriesEnabled, super.hashCode()); } @Override protected ToStringHelper toStringHelper() { return super.toStringHelper() .add("clientId", clientId) .add("clientEmail", clientEmail) .add("privateKeyId", privateKeyId) .add("transportFactoryClassName", transportFactoryClassName) .add("tokenServerUri", tokenServerUri) .add("scopes", scopes) .add("defaultScopes", defaultScopes) .add("serviceAccountUser", serviceAccountUser) .add("lifetime", lifetime) .add("useJwtAccessWithScope", useJwtAccessWithScope) .add("defaultRetriesEnabled", defaultRetriesEnabled); } @Override public boolean equals(Object obj) { if (!(obj instanceof ServiceAccountCredentials)) { return false; } if (!super.equals(obj)) { return false; } ServiceAccountCredentials other = (ServiceAccountCredentials) obj; return Objects.equals(this.clientId, other.clientId) && Objects.equals(this.clientEmail, other.clientEmail) && Objects.equals(this.privateKey, other.privateKey) && Objects.equals(this.privateKeyId, other.privateKeyId) && Objects.equals(this.transportFactoryClassName, other.transportFactoryClassName) && Objects.equals(this.tokenServerUri, other.tokenServerUri) && Objects.equals(this.scopes, other.scopes) && Objects.equals(this.defaultScopes, other.defaultScopes) && Objects.equals(this.lifetime, other.lifetime) && Objects.equals(this.useJwtAccessWithScope, other.useJwtAccessWithScope) && Objects.equals(this.defaultRetriesEnabled, other.defaultRetriesEnabled); } String createAssertion(JsonFactory jsonFactory, long currentTime) throws IOException { JsonWebSignature.Header header = new JsonWebSignature.Header(); header.setAlgorithm("RS256"); header.setType("JWT"); header.setKeyId(privateKeyId); JsonWebToken.Payload payload = new JsonWebToken.Payload(); payload.setIssuer(getIssuer()); payload.setIssuedAtTimeSeconds(currentTime / 1000); payload.setExpirationTimeSeconds(currentTime / 1000 + this.lifetime); payload.setSubject(serviceAccountUser); if (scopes.isEmpty()) { payload.put("scope", Joiner.on(' ').join(defaultScopes)); } else { payload.put("scope", Joiner.on(' ').join(scopes)); } payload.setAudience(OAuth2Utils.TOKEN_SERVER_URI.toString()); String assertion; try { assertion = JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload); } catch (GeneralSecurityException e) { throw new IOException( "Error signing service account access token request with private key.", e); } return assertion; } @VisibleForTesting String createAssertionForIdToken(long currentTime, String audience, String targetAudience) throws IOException { JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; JsonWebSignature.Header header = new JsonWebSignature.Header(); header.setAlgorithm("RS256"); header.setType("JWT"); header.setKeyId(privateKeyId); JsonWebToken.Payload payload = new JsonWebToken.Payload(); payload.setIssuer(getIssuer()); payload.setIssuedAtTimeSeconds(currentTime / 1000); payload.setExpirationTimeSeconds(currentTime / 1000 + this.lifetime); payload.setSubject(serviceAccountUser); if (audience == null) { payload.setAudience(OAuth2Utils.TOKEN_SERVER_URI.toString()); } else { payload.setAudience(audience); } try { payload.set("target_audience", targetAudience); return JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload); } catch (GeneralSecurityException e) { throw new IOException( "Error signing service account access token request with private key.", e); } } /** * Self-signed JWT uses uri as audience, which should have the "https://{host}/" format. For * instance, if the uri is "https://compute.googleapis.com/compute/v1/projects/", then this * function returns "https://compute.googleapis.com/". */ @VisibleForTesting static URI getUriForSelfSignedJWT(URI uri) { if (uri == null || uri.getScheme() == null || uri.getHost() == null) { return uri; } try { return new URI(uri.getScheme(), uri.getHost(), "/", null); } catch (URISyntaxException unused) { return uri; } } @VisibleForTesting JwtCredentials createSelfSignedJwtCredentials(final URI uri) { return createSelfSignedJwtCredentials(uri, scopes.isEmpty() ? defaultScopes : scopes); } @VisibleForTesting JwtCredentials createSelfSignedJwtCredentials(final URI uri, Collection scopes) { // Create a JwtCredentials for self-signed JWT. See https://google.aip.dev/auth/4111. JwtClaims.Builder claimsBuilder = JwtClaims.newBuilder().setIssuer(clientEmail).setSubject(clientEmail); if (uri == null) { // If uri is null, use scopes. String scopeClaim = Joiner.on(' ').join(scopes); claimsBuilder.setAdditionalClaims(Collections.singletonMap("scope", scopeClaim)); } else { // otherwise, use audience with the uri. claimsBuilder.setAudience(getUriForSelfSignedJWT(uri).toString()); } return JwtCredentials.newBuilder() .setPrivateKey(privateKey) .setPrivateKeyId(privateKeyId) .setJwtClaims(claimsBuilder.build()) .setClock(clock) .build(); } @Override public void getRequestMetadata( final URI uri, Executor executor, final RequestMetadataCallback callback) { // For default universe Self-signed JWT could be explicitly disabled with // {@code ServiceAccountCredentials.useJwtAccessWithScope} flag. // If universe is non-default, it only supports self-signed JWT, and it is always allowed. if (this.useJwtAccessWithScope || !isDefaultUniverseDomain()) { // This will call getRequestMetadata(URI uri), which handles self-signed JWT logic. // Self-signed JWT doesn't use network, so here we do a blocking call to improve // efficiency. executor will be ignored since it is intended for async operation. blockingGetToCallback(uri, callback); } else { super.getRequestMetadata(uri, executor, callback); } } /** Provide the request metadata by putting an access JWT directly in the metadata. */ @Override public Map> getRequestMetadata(URI uri) throws IOException { if (createScopedRequired() && uri == null) { throw new IOException( "Scopes and uri are not configured for service account. Specify the scopes" + " by calling createScoped or passing scopes to constructor or" + " providing uri to getRequestMetadata."); } if (isDefaultUniverseDomain()) { return getRequestMetadataForGdu(uri); } else { return getRequestMetadataForNonGdu(uri); } } private Map> getRequestMetadataForGdu(URI uri) throws IOException { // If scopes are provided, but we cannot use self-signed JWT or domain-wide delegation is // configured then use scopes to get access token. if ((!createScopedRequired() && !useJwtAccessWithScope) || isConfiguredForDomainWideDelegation()) { return super.getRequestMetadata(uri); } return getRequestMetadataWithSelfSignedJwt(uri); } private Map> getRequestMetadataForNonGdu(URI uri) throws IOException { // Self Signed JWT is not supported for domain-wide delegation for non-GDU universes if (isConfiguredForDomainWideDelegation()) { throw new IOException( String.format( "Service Account user is configured for the credential. " + "Domain-wide delegation is not supported in universes different than %s.", Credentials.GOOGLE_DEFAULT_UNIVERSE)); } return getRequestMetadataWithSelfSignedJwt(uri); } /** Provide the access JWT for scopes if provided, for uri as aud otherwise */ @VisibleForTesting private Map> getRequestMetadataWithSelfSignedJwt(URI uri) throws IOException { // If scopes are provided and self-signed JWT can be used, use self-signed JWT with scopes. // Otherwise, use self-signed JWT with uri as the audience. JwtCredentials jwtCredentials; if (!createScopedRequired()) { // Create selfSignedJwtCredentialsWithScope when needed and reuse it for better performance. if (selfSignedJwtCredentialsWithScope == null) { selfSignedJwtCredentialsWithScope = createSelfSignedJwtCredentials(null); } jwtCredentials = selfSignedJwtCredentialsWithScope; } else { // Create JWT credentials with the uri as audience. jwtCredentials = createSelfSignedJwtCredentials(uri); } Map> requestMetadata = jwtCredentials.getRequestMetadata(null); return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata); } @SuppressWarnings("unused") private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { // properly deserialize the transient transportFactory input.defaultReadObject(); transportFactory = newInstance(transportFactoryClassName); } public static Builder newBuilder() { return new Builder(); } @Override public Builder toBuilder() { return new Builder(this); } public static class Builder extends GoogleCredentials.Builder { private String clientId; private String clientEmail; private PrivateKey privateKey; private String privateKeyId; private String serviceAccountUser; private String projectId; private URI tokenServerUri; private Collection scopes; private Collection defaultScopes; private HttpTransportFactory transportFactory; private int lifetime = DEFAULT_LIFETIME_IN_SECONDS; private boolean useJwtAccessWithScope = false; private boolean defaultRetriesEnabled = true; protected Builder() {} protected Builder(ServiceAccountCredentials credentials) { super(credentials); this.clientId = credentials.clientId; this.clientEmail = credentials.clientEmail; this.privateKey = credentials.privateKey; this.privateKeyId = credentials.privateKeyId; this.scopes = credentials.scopes; this.defaultScopes = credentials.defaultScopes; this.transportFactory = credentials.transportFactory; this.tokenServerUri = credentials.tokenServerUri; this.serviceAccountUser = credentials.serviceAccountUser; this.projectId = credentials.projectId; this.lifetime = credentials.lifetime; this.useJwtAccessWithScope = credentials.useJwtAccessWithScope; this.defaultRetriesEnabled = credentials.defaultRetriesEnabled; } @CanIgnoreReturnValue public Builder setClientId(String clientId) { this.clientId = clientId; return this; } @CanIgnoreReturnValue public Builder setClientEmail(String clientEmail) { this.clientEmail = clientEmail; return this; } @CanIgnoreReturnValue public Builder setPrivateKey(PrivateKey privateKey) { this.privateKey = privateKey; return this; } @CanIgnoreReturnValue public Builder setPrivateKeyString(String privateKeyPkcs8) throws IOException { this.privateKey = OAuth2Utils.privateKeyFromPkcs8(privateKeyPkcs8); return this; } @CanIgnoreReturnValue public Builder setPrivateKeyId(String privateKeyId) { this.privateKeyId = privateKeyId; return this; } @CanIgnoreReturnValue public Builder setScopes(Collection scopes) { this.scopes = scopes; this.defaultScopes = ImmutableSet.of(); return this; } @CanIgnoreReturnValue public Builder setScopes(Collection scopes, Collection defaultScopes) { this.scopes = scopes; this.defaultScopes = defaultScopes; return this; } @CanIgnoreReturnValue public Builder setServiceAccountUser(String serviceAccountUser) { this.serviceAccountUser = serviceAccountUser; return this; } @CanIgnoreReturnValue public Builder setProjectId(String projectId) { this.projectId = projectId; return this; } @CanIgnoreReturnValue public Builder setTokenServerUri(URI tokenServerUri) { this.tokenServerUri = tokenServerUri; return this; } @CanIgnoreReturnValue public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { this.transportFactory = transportFactory; return this; } @Override @CanIgnoreReturnValue public Builder setQuotaProjectId(String quotaProjectId) { super.setQuotaProjectId(quotaProjectId); return this; } @CanIgnoreReturnValue public Builder setLifetime(int lifetime) { this.lifetime = lifetime == 0 ? DEFAULT_LIFETIME_IN_SECONDS : lifetime; return this; } /** * Sets the useJwtAccessWithScope flag. This flag will be ignored if universeDomain field is * different from {@link Credentials#GOOGLE_DEFAULT_UNIVERSE}. */ @CanIgnoreReturnValue public Builder setUseJwtAccessWithScope(boolean useJwtAccessWithScope) { this.useJwtAccessWithScope = useJwtAccessWithScope; return this; } @CanIgnoreReturnValue public Builder setDefaultRetriesEnabled(boolean defaultRetriesEnabled) { this.defaultRetriesEnabled = defaultRetriesEnabled; return this; } public Builder setUniverseDomain(String universeDomain) { super.universeDomain = universeDomain; return this; } public String getClientId() { return clientId; } public String getClientEmail() { return clientEmail; } public PrivateKey getPrivateKey() { return privateKey; } public String getPrivateKeyId() { return privateKeyId; } public Collection getScopes() { return scopes; } public Collection getDefaultScopes() { return defaultScopes; } public String getServiceAccountUser() { return serviceAccountUser; } public String getProjectId() { return projectId; } public URI getTokenServerUri() { return tokenServerUri; } public HttpTransportFactory getHttpTransportFactory() { return transportFactory; } public int getLifetime() { return lifetime; } public boolean getUseJwtAccessWithScope() { return useJwtAccessWithScope; } public boolean isDefaultRetriesEnabled() { return defaultRetriesEnabled; } @Override public ServiceAccountCredentials build() { return new ServiceAccountCredentials(this); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy