File

src/auth/identitypoolclient.ts

Description

Url-sourced/file-sourced credentials json interface. This is used for K8s and Azure workloads.

Extends

BaseExternalAccountClientOptions

Index

Properties

Properties

credential_source
credential_source: literal type
Type : literal type
import {GaxiosOptions} from 'gaxios';
import * as fs from 'fs';
import {promisify} from 'util';

import {
  BaseExternalAccountClient,
  BaseExternalAccountClientOptions,
} from './baseexternalclient';
import {RefreshOptions} from './oauth2client';

// fs.readfile is undefined in browser karma tests causing
// `npm run browser-test` to fail as test.oauth2.ts imports this file via
// src/index.ts.
// Fallback to void function to avoid promisify throwing a TypeError.
const readFile = promisify(fs.readFile ?? (() => {}));
const realpath = promisify(fs.realpath ?? (() => {}));
const lstat = promisify(fs.lstat ?? (() => {}));

type SubjectTokenFormatType = 'json' | 'text';

interface SubjectTokenJsonResponse {
  [key: string]: string;
}

/**
 * Url-sourced/file-sourced credentials json interface.
 * This is used for K8s and Azure workloads.
 */
export interface IdentityPoolClientOptions
  extends BaseExternalAccountClientOptions {
  credential_source: {
    file?: string;
    url?: string;
    headers?: {
      [key: string]: string;
    };
    format?: {
      type: SubjectTokenFormatType;
      subject_token_field_name?: string;
    };
  };
}

/**
 * Defines the Url-sourced and file-sourced external account clients mainly
 * used for K8s and Azure workloads.
 */
export class IdentityPoolClient extends BaseExternalAccountClient {
  private readonly file?: string;
  private readonly url?: string;
  private readonly headers?: {[key: string]: string};
  private readonly formatType: SubjectTokenFormatType;
  private readonly formatSubjectTokenFieldName?: string;

  /**
   * Instantiate an IdentityPoolClient instance using the provided JSON
   * object loaded from an external account credentials file.
   * An error is thrown if the credential is not a valid file-sourced or
   * url-sourced 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: IdentityPoolClientOptions,
    additionalOptions?: RefreshOptions
  ) {
    super(options, additionalOptions);
    this.file = options.credential_source.file;
    this.url = options.credential_source.url;
    this.headers = options.credential_source.headers;
    if (!this.file && !this.url) {
      throw new Error('No valid Identity Pool "credential_source" provided');
    }
    // Text is the default format type.
    this.formatType = options.credential_source.format?.type || 'text';
    this.formatSubjectTokenFieldName =
      options.credential_source.format?.subject_token_field_name;
    if (this.formatType !== 'json' && this.formatType !== 'text') {
      throw new Error(`Invalid credential_source format "${this.formatType}"`);
    }
    if (this.formatType === 'json' && !this.formatSubjectTokenFieldName) {
      throw new Error(
        'Missing subject_token_field_name for JSON credential_source format'
      );
    }
  }

  /**
   * Triggered when a 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 either retrieves the local credential from a file location (k8s
   * workload) or by sending a GET request to a local metadata server (Azure
   * workloads).
   * @return A promise that resolves with the external subject token.
   */
  async retrieveSubjectToken(): Promise<string> {
    if (this.file) {
      return await this.getTokenFromFile(
        this.file!,
        this.formatType,
        this.formatSubjectTokenFieldName
      );
    }
    return await this.getTokenFromUrl(
      this.url!,
      this.formatType,
      this.formatSubjectTokenFieldName,
      this.headers
    );
  }

  /**
   * Looks up the external subject token in the file path provided and
   * resolves with that token.
   * @param file The file path where the external credential is located.
   * @param formatType The token file or URL response type (JSON or text).
   * @param formatSubjectTokenFieldName For JSON response types, this is the
   *   subject_token field name. For Azure, this is access_token. For text
   *   response types, this is ignored.
   * @return A promise that resolves with the external subject token.
   */
  private async getTokenFromFile(
    filePath: string,
    formatType: SubjectTokenFormatType,
    formatSubjectTokenFieldName?: string
  ): Promise<string> {
    // Make sure there is a file at the path. lstatSync will throw if there is
    // nothing there.
    try {
      // Resolve path to actual file in case of symlink. Expect a thrown error
      // if not resolvable.
      filePath = await realpath(filePath);

      if (!(await lstat(filePath)).isFile()) {
        throw new Error();
      }
    } catch (err) {
      err.message = `The file at ${filePath} does not exist, or it is not a file. ${err.message}`;
      throw err;
    }

    let subjectToken: string | undefined;
    const rawText = await readFile(filePath, {encoding: 'utf8'});
    if (formatType === 'text') {
      subjectToken = rawText;
    } else if (formatType === 'json' && formatSubjectTokenFieldName) {
      const json = JSON.parse(rawText) as SubjectTokenJsonResponse;
      subjectToken = json[formatSubjectTokenFieldName];
    }
    if (!subjectToken) {
      throw new Error(
        'Unable to parse the subject_token from the credential_source file'
      );
    }
    return subjectToken;
  }

  /**
   * Sends a GET request to the URL provided and resolves with the returned
   * external subject token.
   * @param url The URL to call to retrieve the subject token. This is typically
   *   a local metadata server.
   * @param formatType The token file or URL response type (JSON or text).
   * @param formatSubjectTokenFieldName For JSON response types, this is the
   *   subject_token field name. For Azure, this is access_token. For text
   *   response types, this is ignored.
   * @param headers The optional additional headers to send with the request to
   *   the metadata server url.
   * @return A promise that resolves with the external subject token.
   */
  private async getTokenFromUrl(
    url: string,
    formatType: SubjectTokenFormatType,
    formatSubjectTokenFieldName?: string,
    headers?: {[key: string]: string}
  ): Promise<string> {
    const opts: GaxiosOptions = {
      url,
      method: 'GET',
      headers,
      responseType: formatType,
    };
    let subjectToken: string | undefined;
    if (formatType === 'text') {
      const response = await this.transporter.request<string>(opts);
      subjectToken = response.data;
    } else if (formatType === 'json' && formatSubjectTokenFieldName) {
      const response = await this.transporter.request<SubjectTokenJsonResponse>(
        opts
      );
      subjectToken = response.data[formatSubjectTokenFieldName];
    }
    if (!subjectToken) {
      throw new Error(
        'Unable to parse the subject_token from the credential_source URL'
      );
    }
    return subjectToken;
  }
}

result-matching ""

    No results matching ""