File

src/auth/jwtaccess.ts

Indexable

[index: string]: string
import * as jws from 'jws';
import * as LRU from 'lru-cache';
import * as stream from 'stream';

import * as messages from '../messages';
import {JWTInput} from './credentials';
import {Headers, RequestMetadataResponse} from './oauth2client';

const DEFAULT_HEADER: jws.Header = {
  alg: 'RS256',
  typ: 'JWT',
};

export interface Claims {
  [index: string]: string;
}

export class JWTAccess {
  email?: string | null;
  key?: string | null;
  keyId?: string | null;
  projectId?: string;

  private cache = new LRU<string, Headers>({max: 500, maxAge: 60 * 60 * 1000});

  /**
   * JWTAccess service account credentials.
   *
   * Create a new access token by using the credential to create a new JWT token
   * that's recognized as the access token.
   *
   * @param email the service account email address.
   * @param key the private key that will be used to sign the token.
   * @param keyId the ID of the private key used to sign the token.
   */
  constructor(
    email?: string | null,
    key?: string | null,
    keyId?: string | null
  ) {
    this.email = email;
    this.key = key;
    this.keyId = keyId;
  }

  /**
   * Indicates whether the credential requires scopes to be created by calling
   * createdScoped before use.
   * @deprecated
   * @return always false
   */
  createScopedRequired(): boolean {
    // JWT Header authentication does not use scopes.
    messages.warn(messages.JWT_ACCESS_CREATE_SCOPED_DEPRECATED);
    return false;
  }

  /**
   * Get a non-expired access token, after refreshing if necessary.
   *
   * @param authURI The URI being authorized.
   * @param additionalClaims An object with a set of additional claims to
   * include in the payload.
   * @deprecated Please use `getRequestHeaders` instead.
   * @returns An object that includes the authorization header.
   */
  getRequestMetadata(
    url: string,
    additionalClaims?: Claims
  ): RequestMetadataResponse {
    messages.warn(messages.JWT_ACCESS_GET_REQUEST_METADATA_DEPRECATED);
    return {headers: this.getRequestHeaders(url, additionalClaims)};
  }

  /**
   * Get a non-expired access token, after refreshing if necessary.
   *
   * @param url The URI being authorized.
   * @param additionalClaims An object with a set of additional claims to
   * include in the payload.
   * @returns An object that includes the authorization header.
   */
  getRequestHeaders(url: string, additionalClaims?: Claims): Headers {
    const cachedToken = this.cache.get(url);
    if (cachedToken) {
      return cachedToken;
    }
    const iat = Math.floor(new Date().getTime() / 1000);
    const exp = iat + 3600; // 3600 seconds = 1 hour

    // The payload used for signed JWT headers has:
    // iss == sub == <client email>
    // aud == <the authorization uri>
    const defaultClaims = {
      iss: this.email,
      sub: this.email,
      aud: url,
      exp,
      iat,
    };

    // if additionalClaims are provided, ensure they do not collide with
    // other required claims.
    if (additionalClaims) {
      for (const claim in defaultClaims) {
        if (additionalClaims[claim]) {
          throw new Error(
            `The '${claim}' property is not allowed when passing additionalClaims. This claim is included in the JWT by default.`
          );
        }
      }
    }

    const header = this.keyId
      ? {...DEFAULT_HEADER, kid: this.keyId}
      : DEFAULT_HEADER;
    const payload = Object.assign(defaultClaims, additionalClaims);

    // Sign the jwt and add it to the cache
    const signedJWT = jws.sign({header, payload, secret: this.key});
    const headers = {Authorization: `Bearer ${signedJWT}`};
    this.cache.set(url, headers);
    return headers;
  }

  /**
   * Create a JWTAccess 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 JWTAccess 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) => void
  ): void;
  fromStream(
    inputStream: stream.Readable,
    callback?: (err?: Error) => void
  ): void | Promise<void> {
    if (callback) {
      this.fromStreamAsync(inputStream).then(r => callback(), callback);
    } else {
      return this.fromStreamAsync(inputStream);
    }
  }

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

result-matching ""

    No results matching ""