/*
 * 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.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.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects.ToStringHelper;
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.
 *
 * <p>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 static final int INITIAL_RETRY_INTERVAL_MILLIS = 1000;
  private static final double RETRY_RANDOMIZATION_FACTOR = 0.1;
  private static final double RETRY_MULTIPLIER = 2;
  static final int DEFAULT_NUMBER_OF_RETRIES = 3;

  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<String> scopes;
  private final Collection<String> 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.<String>of() : ImmutableSet.copyOf(builder.scopes);
    this.defaultScopes =
        (builder.defaultScopes == null)
            ? ImmutableSet.<String>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<String, Object> 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<String> 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<String> scopes,
      Collection<String> 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<String> 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<String> scopes,
      Collection<String> 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<String> 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<String> scopes,
      Collection<String> 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(DEFAULT_NUMBER_OF_RETRIES);
    } else {
      request.setNumberOfRetries(0);
    }
    request.setParser(new JsonObjectParser(jsonFactory));

    ExponentialBackOff backoff =
        new ExponentialBackOff.Builder()
            .setInitialIntervalMillis(INITIAL_RETRY_INTERVAL_MILLIS)
            .setRandomizationFactor(RETRY_RANDOMIZATION_FACTOR)
            .setMultiplier(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 the metadata server on ComputeEngine.
   *
   * @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<Option> options)
      throws IOException {

    JsonFactory jsonFactory = OAuth2Utils.JSON_FACTORY;
    long currentTime = clock.currentTimeMillis();
    String assertion =
        createAssertionForIdToken(
            jsonFactory, currentTime, tokenServerUri.toString(), targetAudience);

    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));
    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);
    }

    GenericData responseData = response.parseAs(GenericData.class);
    String rawToken = OAuth2Utils.validateString(responseData, "id_token", PARSE_ERROR_PREFIX);

    return IdToken.create(rawToken);
  }

  /**
   * 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.
   *
   * <p>Should be called before use for instances with empty scopes.
   */
  @Override
  public GoogleCredentials createScoped(Collection<String> newScopes) {
    return createScoped(newScopes, null);
  }

  /**
   * Clones the service account with the specified scopes.
   *
   * <p>Should be called before use for instances with empty scopes.
   */
  @Override
  public GoogleCredentials createScoped(
      Collection<String> newScopes, Collection<String> newDefaultScopes) {
    return this.toBuilder().setScopes(newScopes, newDefaultScopes).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<String> getScopes() {
    return scopes;
  }

  public final Collection<String> 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(
      JsonFactory jsonFactory, long currentTime, String audience, String targetAudience)
      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 (audience == null) {
      payload.setAudience(OAuth2Utils.TOKEN_SERVER_URI.toString());
    } else {
      payload.setAudience(audience);
    }

    try {
      payload.set("target_audience", targetAudience);

      String assertion =
          JsonWebSignature.signUsingRsaSha256(privateKey, jsonFactory, header, payload);
      return assertion;
    } 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) {
    // 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 = "";
      if (!scopes.isEmpty()) {
        scopeClaim = Joiner.on(' ').join(scopes);
      } else {
        scopeClaim = Joiner.on(' ').join(defaultScopes);
      }
      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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String> scopes;
    private Collection<String> 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<String> scopes) {
      this.scopes = scopes;
      this.defaultScopes = ImmutableSet.<String>of();
      return this;
    }

    @CanIgnoreReturnValue
    public Builder setScopes(Collection<String> scopes, Collection<String> 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<String> getScopes() {
      return scopes;
    }

    public Collection<String> 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);
    }
  }
}
