File

src/auth/impersonated.ts

Index

Properties

Properties

includeEmail
includeEmail: boolean
Type : boolean

Include the service account email in the token. If set to true, the token will contain email and email_verified claims.

import {GetTokenResponse, OAuth2Client, RefreshOptions} from './oauth2client';
import {AuthClient} from './authclient';
import {IdTokenProvider} from './idtokenclient';
import {GaxiosError} from 'gaxios';

export interface ImpersonatedOptions extends RefreshOptions {
  /**
   * Client used to perform exchange for impersonated client.
   */
  sourceClient?: AuthClient;
  /**
   * The service account to impersonate.
   */
  targetPrincipal?: string;
  /**
   * Scopes to request during the authorization grant.
   */
  targetScopes?: string[];
  /**
   * The chained list of delegates required to grant the final access_token.
   */
  delegates?: string[];
  /**
   * Number of seconds the delegated credential should be valid.
   */
  lifetime?: number | 3600;
  /**
   * API endpoint to fetch token from.
   */
  endpoint?: string;
}

export const IMPERSONATED_ACCOUNT_TYPE = 'impersonated_service_account';

export interface TokenResponse {
  accessToken: string;
  expireTime: string;
}

export interface FetchIdTokenOptions {
  /**
   * Include the service account email in the token.
   * If set to `true`, the token will contain `email` and `email_verified` claims.
   */
  includeEmail: boolean;
}

export interface FetchIdTokenResponse {
  /** The OpenId Connect ID token. */
  token: string;
}

export class Impersonated extends OAuth2Client implements IdTokenProvider {
  private sourceClient: AuthClient;
  private targetPrincipal: string;
  private targetScopes: string[];
  private delegates: string[];
  private lifetime: number;
  private endpoint: string;

  /**
   * Impersonated service account credentials.
   *
   * Create a new access token by impersonating another service account.
   *
   * Impersonated Credentials allowing credentials issued to a user or
   * service account to impersonate another. The source project using
   * Impersonated Credentials must enable the "IAMCredentials" API.
   * Also, the target service account must grant the orginating principal
   * the "Service Account Token Creator" IAM role.
   *
   * @param {object} options - The configuration object.
   * @param {object} [options.sourceClient] the source credential used as to
   * acquire the impersonated credentials.
   * @param {string} [options.targetPrincipal] the service account to
   * impersonate.
   * @param {string[]} [options.delegates] the chained list of delegates
   * required to grant the final access_token. If set, the sequence of
   * identities must have "Service Account Token Creator" capability granted to
   * the preceding identity. For example, if set to [serviceAccountB,
   * serviceAccountC], the sourceCredential must have the Token Creator role on
   * serviceAccountB. serviceAccountB must have the Token Creator on
   * serviceAccountC. Finally, C must have Token Creator on target_principal.
   * If left unset, sourceCredential must have that role on targetPrincipal.
   * @param {string[]} [options.targetScopes] scopes to request during the
   * authorization grant.
   * @param {number} [options.lifetime] number of seconds the delegated
   * credential should be valid for up to 3600 seconds by default, or 43,200
   * seconds by extending the token's lifetime, see:
   * https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-oauth
   * @param {string} [options.endpoint] api endpoint override.
   */
  constructor(options: ImpersonatedOptions = {}) {
    super(options);
    this.credentials = {
      expiry_date: 1,
      refresh_token: 'impersonated-placeholder',
    };
    this.sourceClient = options.sourceClient ?? new OAuth2Client();
    this.targetPrincipal = options.targetPrincipal ?? '';
    this.delegates = options.delegates ?? [];
    this.targetScopes = options.targetScopes ?? [];
    this.lifetime = options.lifetime ?? 3600;
    this.endpoint = options.endpoint ?? 'https://iamcredentials.googleapis.com';
  }

  /**
   * Refreshes the access token.
   * @param refreshToken Unused parameter
   */
  protected async refreshToken(
    refreshToken?: string | null
  ): Promise<GetTokenResponse> {
    try {
      await this.sourceClient.getAccessToken();
      const name = 'projects/-/serviceAccounts/' + this.targetPrincipal;
      const u = `${this.endpoint}/v1/${name}:generateAccessToken`;
      const body = {
        delegates: this.delegates,
        scope: this.targetScopes,
        lifetime: this.lifetime + 's',
      };
      const res = await this.sourceClient.request<TokenResponse>({
        url: u,
        data: body,
        method: 'POST',
      });
      const tokenResponse = res.data;
      this.credentials.access_token = tokenResponse.accessToken;
      this.credentials.expiry_date = Date.parse(tokenResponse.expireTime);
      return {
        tokens: this.credentials,
        res,
      };
    } catch (error) {
      if (!(error instanceof Error)) throw error;

      let status = 0;
      let message = '';

      if (error instanceof GaxiosError) {
        status = error?.response?.data?.error?.status;
        message = error?.response?.data?.error?.message;
      }

      if (status && message) {
        error.message = `${status}: unable to impersonate: ${message}`;
        throw error;
      } else {
        error.message = `unable to impersonate: ${error}`;
        throw error;
      }
    }
  }

  /**
   * Generates an OpenID Connect ID token for a service account.
   *
   * {@link https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken Reference Documentation}
   *
   * @param targetAudience the audience for the fetched ID token.
   * @param options the for the request
   * @return an OpenID Connect ID token
   */
  async fetchIdToken(
    targetAudience: string,
    options?: FetchIdTokenOptions
  ): Promise<string> {
    await this.sourceClient.getAccessToken();

    const name = `projects/-/serviceAccounts/${this.targetPrincipal}`;
    const u = `${this.endpoint}/v1/${name}:generateIdToken`;
    const body = {
      delegates: this.delegates,
      audience: targetAudience,
      includeEmail: options?.includeEmail ?? true,
    };
    const res = await this.sourceClient.request<FetchIdTokenResponse>({
      url: u,
      data: body,
      method: 'POST',
    });

    return res.data.token;
  }
}

results matching ""

    No results matching ""