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

nl.topicus.jdbc.shaded.com.google.auth.oauth2.ServiceAccountCredentials Maven / Gradle / Ivy

There is a newer version: 1.1.6
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 nl.topicus.jdbc.shaded.com.google.auth.oauth2;

import static nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.MoreObjects.firstNonNull;

import nl.topicus.jdbc.shaded.com.google.api.client.http.GenericUrl;
import nl.topicus.jdbc.shaded.com.google.api.client.http.HttpBackOffIOExceptionHandler;
import nl.topicus.jdbc.shaded.com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler;
import nl.topicus.jdbc.shaded.com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler.BackOffRequired;
import nl.topicus.jdbc.shaded.com.google.api.client.http.HttpRequest;
import nl.topicus.jdbc.shaded.com.google.api.client.http.HttpRequestFactory;
import nl.topicus.jdbc.shaded.com.google.api.client.http.HttpResponse;
import nl.topicus.jdbc.shaded.com.google.api.client.http.UrlEncodedContent;
import nl.topicus.jdbc.shaded.com.google.api.client.json.GenericJson;
import nl.topicus.jdbc.shaded.com.google.api.client.json.JsonFactory;
import nl.topicus.jdbc.shaded.com.google.api.client.json.JsonObjectParser;
import nl.topicus.jdbc.shaded.com.google.api.client.json.webtoken.JsonWebSignature;
import nl.topicus.jdbc.shaded.com.google.api.client.json.webtoken.JsonWebToken;
import nl.topicus.jdbc.shaded.com.google.api.client.util.ExponentialBackOff;
import nl.topicus.jdbc.shaded.com.google.api.client.util.GenericData;
import nl.topicus.jdbc.shaded.com.google.api.client.util.Joiner;
import nl.topicus.jdbc.shaded.com.google.api.client.util.PemReader;
import nl.topicus.jdbc.shaded.com.google.api.client.util.PemReader.Section;
import nl.topicus.jdbc.shaded.com.google.api.client.util.Preconditions;
import nl.topicus.jdbc.shaded.com.google.api.client.util.SecurityUtils;
import nl.topicus.jdbc.shaded.com.google.auth.ServiceAccountSigner;
import nl.topicus.jdbc.shaded.com.google.auth.http.HttpTransportFactory;
import nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.base.MoreObjects;
import nl.topicus.jdbc.shaded.com.google.nl.topicus.jdbc.shaded.com.on.collect.ImmutableSet;

import java.nl.topicus.jdbc.shaded.io.IOException;
import java.nl.topicus.jdbc.shaded.io.InputStream;
import java.nl.topicus.jdbc.shaded.io.Reader;
import java.nl.topicus.jdbc.shaded.io.StringReader;
import java.nl.topicus.jdbc.shaded.net.URI;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.sql.Date;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;

/**
 * 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 { 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 final String clientId; private final String clientEmail; private final PrivateKey privateKey; private final String privateKeyId; private final String serviceAccountUser; private final String transportFactoryClassName; private final URI tokenServerUri; private final Collection scopes; private transient HttpTransportFactory transportFactory; /** * Constructor with minimum identifying information. * * @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. */ public ServiceAccountCredentials( String clientId, String clientEmail, PrivateKey privateKey, String privateKeyId, Collection scopes) { this(clientId, clientEmail, privateKey, privateKeyId, scopes, null, null, 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 transportFactory HTTP transport factory, creates the transport used to get access * tokens. * @param tokenServerUri URI of the end point that provides tokens. */ public ServiceAccountCredentials( String clientId, String clientEmail, PrivateKey privateKey, String privateKeyId, Collection scopes, HttpTransportFactory transportFactory, URI tokenServerUri) { this(clientId, clientEmail, privateKey, privateKeyId, scopes, transportFactory, tokenServerUri, 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 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 * authority to the service account. */ ServiceAccountCredentials( String clientId, String clientEmail, PrivateKey privateKey, String privateKeyId, Collection scopes, HttpTransportFactory transportFactory, URI tokenServerUri, String serviceAccountUser) { this.clientId = clientId; this.clientEmail = Preconditions.checkNotNull(clientEmail); this.privateKey = Preconditions.checkNotNull(privateKey); this.privateKeyId = privateKeyId; this.scopes = (scopes == null) ? ImmutableSet.of() : ImmutableSet.copyOf(scopes); this.transportFactory = firstNonNull(transportFactory, getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY)); this.transportFactoryClassName = this.transportFactory.getClass().getName(); this.tokenServerUri = (tokenServerUri == null) ? OAuth2Utils.TOKEN_SERVER_URI : tokenServerUri; this.serviceAccountUser = serviceAccountUser; } /** * Returns service account crentials 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"); 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'."); } return fromPkcs8(clientId, clientEmail, privateKeyPkcs8, privateKeyId, null, transportFactory, null); } /** * Factory with miniumum 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 emptt collection, * which results in a credential that must have createScoped called before use. */ public static ServiceAccountCredentials fromPkcs8( String clientId, String clientEmail, String privateKeyPkcs8, String privateKeyId, Collection scopes) throws IOException { return fromPkcs8(clientId, clientEmail, privateKeyPkcs8, privateKeyId, scopes, null, null, null); } /** * Factory with miniumum 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 emptt 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. */ public static ServiceAccountCredentials fromPkcs8( String clientId, String clientEmail, String privateKeyPkcs8, String privateKeyId, Collection scopes, HttpTransportFactory transportFactory, URI tokenServerUri) throws IOException { return fromPkcs8(clientId, clientEmail, privateKeyPkcs8, privateKeyId, scopes, transportFactory, tokenServerUri, null); } /** * Factory with miniumum 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 emptt 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. */ public static ServiceAccountCredentials fromPkcs8( String clientId, String clientEmail, String privateKeyPkcs8, String privateKeyId, Collection scopes, HttpTransportFactory transportFactory, URI tokenServerUri, String serviceAccountUser) throws IOException { PrivateKey privateKey = privateKeyFromPkcs8(privateKeyPkcs8); return new ServiceAccountCredentials( clientId, clientEmail, privateKey, privateKeyId, scopes, transportFactory, tokenServerUri, serviceAccountUser); } /** * Helper to convert from a PKCS#8 String to an RSA private key */ static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException { Reader reader = new StringReader(privateKeyPkcs8); Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY"); if (section == null) { throw new IOException("Invalid PKCS#8 data."); } byte[] bytes = section.getBase64DecodedBytes(); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes); Exception unexpectedException; try { KeyFactory keyFactory = SecurityUtils.getRsaKeyFactory(); return keyFactory.generatePrivate(keySpec); } catch (NoSuchAlgorithmException | InvalidKeySpecException exception) { unexpectedException = exception; } throw new IOException("Unexpected exception reading PKCS#8 data", unexpectedException); } /** * 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 { Preconditions.checkNotNull(credentialsStream); Preconditions.checkNotNull(transportFactory); JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY; JsonObjectParser parser = new JsonObjectParser(jsonFactory); GenericJson fileContents = parser.parseAndClose( credentialsStream, OAuth2Utils.UTF_8, GenericJson.class); String fileType = (String) fileContents.get("type"); if (fileType == null) { throw new IOException("Error reading credentials from stream, 'type' field not specified."); } if (SERVICE_ACCOUNT_FILE_TYPE.equals(fileType)) { return fromJson(fileContents, transportFactory); } throw new IOException(String.format( "Error reading credentials from stream, 'type' value '%s' not recognized." + " Expecting '%s'.", fileType, SERVICE_ACCOUNT_FILE_TYPE)); } /** * Refreshes the OAuth2 access token by getting a new access token using a JSON Web Token (JWT). */ @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); 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); request.setParser(new JsonObjectParser(jsonFactory)); request.setIOExceptionHandler(new HttpBackOffIOExceptionHandler(new ExponentialBackOff())); request.setUnsuccessfulResponseHandler( new HttpBackOffUnsuccessfulResponseHandler(new ExponentialBackOff()).setBackOffRequired( new BackOffRequired() { public boolean isRequired(HttpResponse response) { int code = response.getStatusCode(); return ( // Server error --- includes timeout errors, which use 500 instead of 408 code / 100 == 5 // Forbidden error --- for historical reasons, used for rate_limit_exceeded // errors instead of 429, but there currently seems no robust automatic way to // distinguish these cases: see // https://github.nl.topicus.jdbc.shaded.com.google/google-api-java-client/issues/662 || code == 403); } })); HttpResponse response; try { response = request.execute(); } catch (IOException e) { throw new IOException("Error getting access token for service account: ", e); } 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 * 1000; return new AccessToken(accessToken, new Date(expiresAtMilliseconds)); } /** * Returns whether the scopes are empty, meaning createScoped must be called before use. */ @Override public boolean createScopedRequired() { return scopes.isEmpty(); } /** * 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 new ServiceAccountCredentials(clientId, clientEmail, privateKey, privateKeyId, newScopes, transportFactory, tokenServerUri, serviceAccountUser); } @Override public GoogleCredentials createDelegated(String user) { return new ServiceAccountCredentials(clientId, clientEmail, privateKey, privateKeyId, scopes, transportFactory, tokenServerUri, user); } 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 String getServiceAccountUser() { return serviceAccountUser; } @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); } } @Override public int hashCode() { return Objects.hash(clientId, clientEmail, privateKey, privateKeyId, transportFactoryClassName, tokenServerUri, scopes); } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("clientId", clientId) .add("clientEmail", clientEmail) .add("privateKeyId", privateKeyId) .add("transportFactoryClassName", transportFactoryClassName) .add("tokenServerUri", tokenServerUri) .add("scopes", scopes) .add("serviceAccountUser", serviceAccountUser) .toString(); } @Override public boolean equals(Object obj) { if (!(obj instanceof ServiceAccountCredentials)) { 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); } 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(clientEmail); payload.setAudience(OAuth2Utils.TOKEN_SERVER_URI.toString()); payload.setIssuedAtTimeSeconds(currentTime / 1000); payload.setExpirationTimeSeconds(currentTime / 1000 + 3600); payload.setSubject(serviceAccountUser); payload.put("scope", Joiner.on(' ').join(scopes)); 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy