File

src/auth/pluggable-auth-client.ts

Description

Defines the credential source portion of the configuration for PluggableAuthClient.

Command is the only required field. If timeout_millis is not specified, the library will default to a 30-second timeout.

Sample credential source for Pluggable Auth Client:
{
  ...
  "credential_source": {
    "executable": {
      "command": "/path/to/get/credentials.sh --arg1=value1 --arg2=value2",
      "timeout_millis": 5000,
      "output_file": "/path/to/generated/cached/credentials"
    }
  }
}

Extends

BaseExternalAccountClientOptions

Index

Properties

Properties

credential_source
credential_source: literal type
Type : literal type
import {
  BaseExternalAccountClient,
  BaseExternalAccountClientOptions,
} from './baseexternalclient';
import {RefreshOptions} from './oauth2client';
import {
  ExecutableResponse,
  InvalidExpirationTimeFieldError,
} from './executable-response';
import {PluggableAuthHandler} from './pluggable-auth-handler';

/**
 * Defines the credential source portion of the configuration for PluggableAuthClient.
 *
 * <p>Command is the only required field. If timeout_millis is not specified, the library will
 * default to a 30-second timeout.
 *
 * <pre>
 * Sample credential source for Pluggable Auth Client:
 * {
 *   ...
 *   "credential_source": {
 *     "executable": {
 *       "command": "/path/to/get/credentials.sh --arg1=value1 --arg2=value2",
 *       "timeout_millis": 5000,
 *       "output_file": "/path/to/generated/cached/credentials"
 *     }
 *   }
 * }
 * </pre>
 */
export interface PluggableAuthClientOptions
  extends BaseExternalAccountClientOptions {
  credential_source: {
    executable: {
      /**
       * The command used to retrieve the 3rd party token.
       */
      command: string;
      /**
       * The timeout for executable to run in milliseconds. If none is provided it
       * will be set to the default timeout of 30 seconds.
       */
      timeout_millis?: number;
      /**
       * An optional output file location that will be checked for a cached response
       * from a previous run of the executable.
       */
      output_file?: string;
    };
  };
}

/**
 * Error thrown from the executable run by PluggableAuthClient.
 */
export class ExecutableError extends Error {
  /**
   * The exit code returned by the executable.
   */
  readonly code: string;

  constructor(message: string, code: string) {
    super(
      `The executable failed with exit code: ${code} and error message: ${message}.`
    );
    this.code = code;
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

/**
 * The default executable timeout when none is provided, in milliseconds.
 */
const DEFAULT_EXECUTABLE_TIMEOUT_MILLIS = 30 * 1000;
/**
 * The minimum allowed executable timeout in milliseconds.
 */
const MINIMUM_EXECUTABLE_TIMEOUT_MILLIS = 5 * 1000;
/**
 * The maximum allowed executable timeout in milliseconds.
 */
const MAXIMUM_EXECUTABLE_TIMEOUT_MILLIS = 120 * 1000;

/**
 * The environment variable to check to see if executable can be run.
 * Value must be set to '1' for the executable to run.
 */
const GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES =
  'GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES';

/**
 * The maximum currently supported executable version.
 */
const MAXIMUM_EXECUTABLE_VERSION = 1;

/**
 * PluggableAuthClient enables the exchange of workload identity pool external credentials for
 * Google access tokens by retrieving 3rd party tokens through a user supplied executable. These
 * scripts/executables are completely independent of the Google Cloud Auth libraries. These
 * credentials plug into ADC and will call the specified executable to retrieve the 3rd party token
 * to be exchanged for a Google access token.
 *
 * <p>To use these credentials, the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable
 * must be set to '1'. This is for security reasons.
 *
 * <p>Both OIDC and SAML are supported. The executable must adhere to a specific response format
 * defined below.
 *
 * <p>The executable must print out the 3rd party token to STDOUT in JSON format. When an
 * output_file is specified in the credential configuration, the executable must also handle writing the
 * JSON response to this file.
 *
 * <pre>
 * OIDC response sample:
 * {
 *   "version": 1,
 *   "success": true,
 *   "token_type": "urn:ietf:params:oauth:token-type:id_token",
 *   "id_token": "HEADER.PAYLOAD.SIGNATURE",
 *   "expiration_time": 1620433341
 * }
 *
 * SAML2 response sample:
 * {
 *   "version": 1,
 *   "success": true,
 *   "token_type": "urn:ietf:params:oauth:token-type:saml2",
 *   "saml_response": "...",
 *   "expiration_time": 1620433341
 * }
 *
 * Error response sample:
 * {
 *   "version": 1,
 *   "success": false,
 *   "code": "401",
 *   "message": "Error message."
 * }
 * </pre>
 *
 * <p>The "expiration_time" field in the JSON response is only required for successful
 * responses when an output file was specified in the credential configuration
 *
 * <p>The auth libraries will populate certain environment variables that will be accessible by the
 * executable, such as: GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE, GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE,
 * GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE, GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL, and
 * GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE.
 *
 * <p>Please see this repositories README for a complete executable request/response specification.
 */
export class PluggableAuthClient extends BaseExternalAccountClient {
  /**
   * The command used to retrieve the third party token.
   */
  private readonly command: string;
  /**
   * The timeout in milliseconds for running executable,
   * set to default if none provided.
   */
  private readonly timeoutMillis: number;
  /**
   * The path to file to check for cached executable response.
   */
  private readonly outputFile?: string;

  /**
   * Executable and output file handler.
   */
  private readonly handler: PluggableAuthHandler;

  /**
   * Instantiates a PluggableAuthClient instance using the provided JSON
   * object loaded from an external account credentials file.
   * An error is thrown if the credential is not a valid pluggable auth credential.
   * @param options The external account options object typically loaded from
   *   the external account JSON credential file.
   * @param additionalOptions Optional additional behavior customization
   *   options. These currently customize expiration threshold time and
   *   whether to retry on 401/403 API request errors.
   */
  constructor(
    options: PluggableAuthClientOptions,
    additionalOptions?: RefreshOptions
  ) {
    super(options, additionalOptions);
    if (!options.credential_source.executable) {
      throw new Error('No valid Pluggable Auth "credential_source" provided.');
    }
    this.command = options.credential_source.executable.command;
    if (!this.command) {
      throw new Error('No valid Pluggable Auth "credential_source" provided.');
    }
    // Check if the provided timeout exists and if it is valid.
    if (options.credential_source.executable.timeout_millis === undefined) {
      this.timeoutMillis = DEFAULT_EXECUTABLE_TIMEOUT_MILLIS;
    } else {
      this.timeoutMillis = options.credential_source.executable.timeout_millis;
      if (
        this.timeoutMillis < MINIMUM_EXECUTABLE_TIMEOUT_MILLIS ||
        this.timeoutMillis > MAXIMUM_EXECUTABLE_TIMEOUT_MILLIS
      ) {
        throw new Error(
          `Timeout must be between ${MINIMUM_EXECUTABLE_TIMEOUT_MILLIS} and ` +
            `${MAXIMUM_EXECUTABLE_TIMEOUT_MILLIS} milliseconds.`
        );
      }
    }

    this.outputFile = options.credential_source.executable.output_file;

    this.handler = new PluggableAuthHandler({
      command: this.command,
      timeoutMillis: this.timeoutMillis,
      outputFile: this.outputFile,
    });
  }

  /**
   * Triggered when an external subject token is needed to be exchanged for a
   * GCP access token via GCP STS endpoint.
   * This uses the `options.credential_source` object to figure out how
   * to retrieve the token using the current environment. In this case,
   * this calls a user provided executable which returns the subject token.
   * The logic is summarized as:
   * 1. Validated that the executable is allowed to run. The
   *    GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment must be set to
   *    1 for security reasons.
   * 2. If an output file is specified by the user, check the file location
   *    for a response. If the file exists and contains a valid response,
   *    return the subject token from the file.
   * 3. Call the provided executable and return response.
   * @return A promise that resolves with the external subject token.
   */
  async retrieveSubjectToken(): Promise<string> {
    // Check if the executable is allowed to run.
    if (process.env[GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES] !== '1') {
      throw new Error(
        'Pluggable Auth executables need to be explicitly allowed to run by ' +
          'setting the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment ' +
          'Variable to 1.'
      );
    }

    let executableResponse: ExecutableResponse | undefined = undefined;
    // Try to get cached executable response from output file.
    if (this.outputFile) {
      executableResponse = await this.handler.retrieveCachedResponse();
    }
    // If no response from output file, call the executable.
    if (!executableResponse) {
      // Set up environment map with required values for the executable.
      const envMap = new Map();
      envMap.set('GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE', this.audience);
      envMap.set('GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE', this.subjectTokenType);
      // Always set to 0 because interactive mode is not supported.
      envMap.set('GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE', '0');
      if (this.outputFile) {
        envMap.set('GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE', this.outputFile);
      }
      const serviceAccountEmail = this.getServiceAccountEmail();
      if (serviceAccountEmail) {
        envMap.set(
          'GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL',
          serviceAccountEmail
        );
      }
      executableResponse = await this.handler.retrieveResponseFromExecutable(
        envMap
      );
    }

    if (executableResponse.version > MAXIMUM_EXECUTABLE_VERSION) {
      throw new Error(
        `Version of executable is not currently supported, maximum supported version is ${MAXIMUM_EXECUTABLE_VERSION}.`
      );
    }
    // Check that response was successful.
    if (!executableResponse.success) {
      throw new ExecutableError(
        executableResponse.errorMessage as string,
        executableResponse.errorCode as string
      );
    }
    // Check that response contains expiration time if output file was specified.
    if (this.outputFile) {
      if (!executableResponse.expirationTime) {
        throw new InvalidExpirationTimeFieldError(
          'The executable response must contain the `expiration_time` field for successful responses when an output_file has been specified in the configuration.'
        );
      }
    }
    // Check that response is not expired.
    if (executableResponse.isExpired()) {
      throw new Error('Executable response is expired.');
    }
    // Return subject token from response.
    return executableResponse.subjectToken as string;
  }
}

results matching ""

    No results matching ""