File

src/auth/pluggable-auth-client.ts

Description

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.

To use these credentials, the GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable must be set to '1'. This is for security reasons.

Both OIDC and SAML are supported. The executable must adhere to a specific response format defined below.

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.

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."
}

The "expiration_time" field in the JSON response is only required for successful responses when an output file was specified in the credential configuration

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.

Please see this repositories README for a complete executable request/response specification.

Extends

BaseExternalAccountClient

Index

Properties
Methods

Constructor

constructor(options: PluggableAuthClientOptions, additionalOptions?: RefreshOptions)

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. the external account JSON credential file. options. These currently customize expiration threshold time and whether to retry on 401/403 API request errors.

Parameters :
Name Type Optional Description
options PluggableAuthClientOptions No

The external account options object typically loaded from the external account JSON credential file.

additionalOptions RefreshOptions Yes

Optional additional behavior customization options. These currently customize expiration threshold time and whether to retry on 401/403 API request errors.

Properties

Public Readonly eagerRefreshThresholdMillis
Type : number
Public Readonly forceRefreshOnFailure
Type : boolean
Public projectId
Type : string | null
Public projectNumber
Type : string | null
Public Optional scopes
Type : string | string[]

OAuth scopes for the GCP access token to use. When not provided, the default https://www.googleapis.com/auth/cloud-platform is used.

Methods

Async retrieveSubjectToken
retrieveSubjectToken()

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.
Returns : Promise<string>

A promise that resolves with the external subject token.

Async getAccessToken
getAccessToken()

response. If the current credential is expired, a new one is retrieved.

Returns : Promise<GetAccessTokenResponse>

A promise that resolves with the current GCP access token response. If the current credential is expired, a new one is retrieved.

Async getProjectId
getProjectId()

current workload identity pool or current workforce pool if determinable. For workforce pool credential, it returns the project ID corresponding to the workforcePoolUserProject. This is introduced to match the current pattern of using the Auth library: const projectId = await auth.getProjectId(); const url = https://dns.googleapis.com/dns/v1/projects/${projectId}; const res = await client.request({ url }); The resource may not have permission (resourcemanager.projects.get) to call this API or the required scopes may not be selected: https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes

Returns : Promise<string | null>

A promise that resolves with the project ID corresponding to the current workload identity pool or current workforce pool if determinable. For workforce pool credential, it returns the project ID corresponding to the workforcePoolUserProject. This is introduced to match the current pattern of using the Auth library: const projectId = await auth.getProjectId(); const url = https://dns.googleapis.com/dns/v1/projects/${projectId}; const res = await client.request({ url }); The resource may not have permission (resourcemanager.projects.get) to call this API or the required scopes may not be selected: https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes

Async getRequestHeaders
getRequestHeaders()

The main authentication interface. It takes an optional url which when present is the endpoint being accessed, and returns a Promise which resolves with authorization header fields.

The result has the form: { Authorization: 'Bearer ' }

Returns : Promise<Headers>
getServiceAccountEmail
getServiceAccountEmail()

The service account email to be impersonated, if available.

Returns : string | null
request
request(opts: GaxiosOptions)
Type parameters :
  • T

Provides a request implementation with OAuth 2.0 flow. In cases of HTTP 401 and 403 responses, it automatically asks for a new access token and replays the unsuccessful request. provided.

Parameters :
Name Type Optional Description
opts GaxiosOptions No

Request options.

Returns : GaxiosPromise<T>

A promise that resolves with the HTTP response when no callback is provided.

setCredentials
setCredentials(credentials: Credentials)

Provides a mechanism to inject GCP access tokens directly. When the provided credential expires, a new credential, using the external account options, is retrieved.

Parameters :
Name Type Optional Description
credentials Credentials No

The Credentials object to set on the current client.

Returns : void
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 ""