File

src/auth/jwtclient.ts

Extends

RefreshOptions

Index

Properties

Properties

additionalClaims
additionalClaims: literal type
Type : literal type
Optional
email
email: string
Type : string
Optional
key
key: string
Type : string
Optional
keyFile
keyFile: string
Type : string
Optional
keyId
keyId: string
Type : string
Optional
scopes
scopes: string | string[]
Type : string | string[]
Optional
subject
subject: string
Type : string
Optional
import {GoogleToken} from 'gtoken';
import * as stream from 'stream';

import * as messages from '../messages';
import {CredentialBody, Credentials, JWTInput} from './credentials';
import {JWTAccess} from './jwtaccess';
import {
  GetTokenResponse,
  OAuth2Client,
  RefreshOptions,
  RequestMetadataResponse,
} from './oauth2client';

export interface JWTOptions extends RefreshOptions {
  email?: string;
  keyFile?: string;
  key?: string;
  keyId?: string;
  scopes?: string | string[];
  subject?: string;
  additionalClaims?: {};
}

export class JWT extends OAuth2Client {
  email?: string;
  keyFile?: string;
  key?: string;
  keyId?: string;
  scopes?: string | string[];
  scope?: string;
  subject?: string;
  gtoken?: GoogleToken;
  additionalClaims?: {};

  private access?: JWTAccess;

  /**
   * JWT service account credentials.
   *
   * Retrieve access token using gtoken.
   *
   * @param email service account email address.
   * @param keyFile path to private key file.
   * @param key value of key
   * @param scopes list of requested scopes or a single scope.
   * @param subject impersonated account's email address.
   * @param key_id the ID of the key
   */
  constructor(options: JWTOptions);
  constructor(
    email?: string,
    keyFile?: string,
    key?: string,
    scopes?: string | string[],
    subject?: string,
    keyId?: string
  );
  constructor(
    optionsOrEmail?: string | JWTOptions,
    keyFile?: string,
    key?: string,
    scopes?: string | string[],
    subject?: string,
    keyId?: string
  ) {
    const opts =
      optionsOrEmail && typeof optionsOrEmail === 'object'
        ? optionsOrEmail
        : {email: optionsOrEmail, keyFile, key, keyId, scopes, subject};
    super({eagerRefreshThresholdMillis: opts.eagerRefreshThresholdMillis});
    this.email = opts.email;
    this.keyFile = opts.keyFile;
    this.key = opts.key;
    this.keyId = opts.keyId;
    this.scopes = opts.scopes;
    this.subject = opts.subject;
    this.additionalClaims = opts.additionalClaims;
    this.credentials = {refresh_token: 'jwt-placeholder', expiry_date: 1};
  }

  /**
   * Creates a copy of the credential with the specified scopes.
   * @param scopes List of requested scopes or a single scope.
   * @return The cloned instance.
   */
  createScoped(scopes?: string | string[]) {
    return new JWT({
      email: this.email,
      keyFile: this.keyFile,
      key: this.key,
      keyId: this.keyId,
      scopes,
      subject: this.subject,
      additionalClaims: this.additionalClaims,
    });
  }

  /**
   * Obtains the metadata to be sent with the request.
   *
   * @param url the URI being authorized.
   */
  protected async getRequestMetadataAsync(
    url?: string | null
  ): Promise<RequestMetadataResponse> {
    if (!this.apiKey && !this.hasScopes() && url) {
      if (
        this.additionalClaims &&
        (this.additionalClaims as {
          target_audience: string;
        }).target_audience
      ) {
        const {tokens} = await this.refreshToken();
        return {headers: {Authorization: `Bearer ${tokens.id_token}`}};
      } else {
        // no scopes have been set, but a uri has been provided. Use JWTAccess
        // credentials.
        if (!this.access) {
          this.access = new JWTAccess(this.email, this.key, this.keyId);
        }
        const headers = await this.access.getRequestHeaders(
          url,
          this.additionalClaims
        );
        return {headers};
      }
    } else {
      return super.getRequestMetadataAsync(url);
    }
  }

  /**
   * Indicates whether the credential requires scopes to be created by calling
   * createScoped before use.
   * @deprecated
   * @return false if createScoped does not need to be called.
   */
  createScopedRequired() {
    messages.warn(messages.JWT_CREATE_SCOPED_DEPRECATED);
    return !this.hasScopes();
  }

  /**
   * Determine if there are currently scopes available.
   */
  private hasScopes() {
    if (!this.scopes) {
      return false;
    }
    // For arrays, check the array length.
    if (this.scopes instanceof Array) {
      return this.scopes.length > 0;
    }
    // For others, convert to a string and check the length.
    return String(this.scopes).length > 0;
  }

  /**
   * Get the initial access token using gToken.
   * @param callback Optional callback.
   * @returns Promise that resolves with credentials
   */
  authorize(): Promise<Credentials>;
  authorize(callback: (err: Error | null, result?: Credentials) => void): void;
  authorize(
    callback?: (err: Error | null, result?: Credentials) => void
  ): Promise<Credentials> | void {
    if (callback) {
      this.authorizeAsync().then(r => callback(null, r), callback);
    } else {
      return this.authorizeAsync();
    }
  }

  private async authorizeAsync(): Promise<Credentials> {
    const result = await this.refreshToken();
    if (!result) {
      throw new Error('No result returned');
    }
    this.credentials = result.tokens;
    this.credentials.refresh_token = 'jwt-placeholder';
    this.key = this.gtoken!.key;
    this.email = this.gtoken!.iss;
    return result.tokens;
  }

  /**
   * Refreshes the access token.
   * @param refreshToken ignored
   * @private
   */
  protected async refreshTokenNoCache(
    refreshToken?: string | null
  ): Promise<GetTokenResponse> {
    const gtoken = this.createGToken();
    const token = await gtoken.getToken();
    const tokens = {
      access_token: token,
      token_type: 'Bearer',
      expiry_date: gtoken.expiresAt,
      // tslint:disable-next-line no-any
      id_token: (gtoken.rawToken! as any).id_token,
    };
    this.emit('tokens', tokens);
    return {res: null, tokens};
  }

  /**
   * Create a gToken if it doesn't already exist.
   */
  private createGToken(): GoogleToken {
    if (!this.gtoken) {
      this.gtoken = new GoogleToken({
        iss: this.email,
        sub: this.subject,
        scope: this.scopes,
        keyFile: this.keyFile,
        key: this.key,
        additionalClaims: this.additionalClaims,
      });
    }
    return this.gtoken;
  }

  /**
   * Create a JWT credentials instance using the given input options.
   * @param json The input object.
   */
  fromJSON(json: JWTInput): void {
    if (!json) {
      throw new Error(
        'Must pass in a JSON object containing the service account auth settings.'
      );
    }
    if (!json.client_email) {
      throw new Error(
        'The incoming JSON object does not contain a client_email field'
      );
    }
    if (!json.private_key) {
      throw new Error(
        'The incoming JSON object does not contain a private_key field'
      );
    }
    // Extract the relevant information from the json key file.
    this.email = json.client_email;
    this.key = json.private_key;
    this.keyId = json.private_key_id;
    this.projectId = json.project_id;
  }

  /**
   * Create a JWT credentials instance using the given input stream.
   * @param inputStream The input stream.
   * @param callback Optional callback.
   */
  fromStream(inputStream: stream.Readable): Promise<void>;
  fromStream(
    inputStream: stream.Readable,
    callback: (err?: Error | null) => void
  ): void;
  fromStream(
    inputStream: stream.Readable,
    callback?: (err?: Error | null) => void
  ): void | Promise<void> {
    if (callback) {
      this.fromStreamAsync(inputStream).then(r => callback(), callback);
    } else {
      return this.fromStreamAsync(inputStream);
    }
  }

  private fromStreamAsync(inputStream: stream.Readable) {
    return new Promise<void>((resolve, reject) => {
      if (!inputStream) {
        throw new Error(
          'Must pass in a stream containing the service account auth settings.'
        );
      }
      let s = '';
      inputStream
        .setEncoding('utf8')
        .on('error', reject)
        .on('data', chunk => (s += chunk))
        .on('end', () => {
          try {
            const data = JSON.parse(s);
            this.fromJSON(data);
            resolve();
          } catch (e) {
            reject(e);
          }
        });
    });
  }

  /**
   * Creates a JWT credentials instance using an API Key for authentication.
   * @param apiKey The API Key in string form.
   */
  fromAPIKey(apiKey: string): void {
    if (typeof apiKey !== 'string') {
      throw new Error('Must provide an API Key string.');
    }
    this.apiKey = apiKey;
  }

  /**
   * Using the key or keyFile on the JWT client, obtain an object that contains
   * the key and the client email.
   */
  async getCredentials(): Promise<CredentialBody> {
    if (this.key) {
      return {private_key: this.key, client_email: this.email};
    } else if (this.keyFile) {
      const gtoken = this.createGToken();
      const creds = await gtoken.getCredentials(this.keyFile);
      return {private_key: creds.privateKey, client_email: creds.clientEmail};
    }
    throw new Error('A key or a keyFile must be provided to getCredentials.');
  }
}

result-matching ""

    No results matching ""