Source: token.js

const jose = require('node-jose');
const utils = require('./utils');

const VIEW_IDENTIFIERS_CLAIM_NAME = 'http://reflect.io/s/v3/vid';
const PARAMETERS_CLAIM_NAME = 'http://reflect.io/s/v3/p';
const ATTRIBUTES_CLAIM_NAME = 'http://reflect.io/s/v3/a';

/**
 * Builder for encrypted tokens.
 *
 * Provides a way to create secure, reusable tokens that enable particular
 * functionality in the Reflect API while disallowing tampering.
 *
 * @constructor
 * @param {string} accessKey The access key that identifies the project of
 *                           this token.
 */
function ProjectTokenBuilder(accessKey) {
  this.accessKey = accessKey;

  this._expiration = null;
  this._claims = {
    viewIdentifiers: [],
    parameters: [],
    attributes: Object.create(null),
  };
}

/**
 * Sets the expiration for the constructed token to the given time.
 *
 * After this time, the token will no longer be valid. All requests made
 * using an expired token will fail.
 *
 * @param {Date} when The time at which the token will expire.
 * @return {ProjectTokenBuilder} This token builder.
 */
ProjectTokenBuilder.prototype.expiration = function expiration(when) {
  this._expiration = when;

  return this;
};

/**
 * Adds the given view identifier to the list of view identifiers permitted
 * by this token.
 *
 * If no view identifiers are added to this builder, all views in the given
 * access key's project will be able to be loaded. Otherwise, only those
 * added will be able to be loaded.
 *
 * @param {string} id The view identifier to restrict to.
 * @return {ProjectTokenBuilder} This token builder.
 */
ProjectTokenBuilder.prototype.addViewIdentifier = function addViewIdentifier(id) {
  this._claims.viewIdentifiers.push(id);

  return this;
};

/**
 * Adds a data-filtering parameter to this token.
 *
 * @param {{field: string, op: string, value: (string|string[])}} parameter
 * @return {ProjectTokenBuilder} This token builder.
 */
ProjectTokenBuilder.prototype.addParameter = function addParameter(parameter) {
  this._claims.parameters.push(parameter);

  return this;
};

/**
 * Sets the given attribute in this token.
 *
 * @param {string} name The attribute slug.
 * @param {*} value The attribute's value, which must be serializable to JSON.
 */
ProjectTokenBuilder.prototype.setAttribute = function setAttribute(name, value) {
  this._claims.attributes[name] = value;

  return this;
};

/**
 * @callback ProjectTokenBuilder.buildCallback
 * @param {*} err If not null, an error indicating why generating the token
 *                failed.
 * @param {string} token The generated token.
 */

/**
 * Builds a final copy of the token using the given secret key.
 *
 * @param {string} secretKey The secret key that corresponds to this builder's
 *                           access key.
 * @param {ProjectTokenBuilder.buildCallback} callback
 *   A callback to invoke with the constructed token when it is ready.
 */
ProjectTokenBuilder.prototype.build = function build(secretKey, callback) {
  const secretKeyBuffer = utils.secretKeyFromUUID(secretKey);

  const options = {
    format: 'compact',
    zip: true,
    fields: {
      cty: 'JWT',
    },
  };

  jose.JWK.asKey({
    kty: 'oct',
    alg: 'A128GCM',
    use: 'enc',
    kid: this.accessKey,
    k: secretKeyBuffer,
  }).then((key) => {
    const encrypter = jose.JWE.createEncrypt(options, key);

    const now = Math.floor(Date.now() / 1000);

    const payload = {
      iat: now,
      nbf: now,
    };

    if (this._expiration !== null) {
      payload.exp = Math.floor(this._expiration / 1000);
    }

    if (this._claims.viewIdentifiers.length) {
      payload[VIEW_IDENTIFIERS_CLAIM_NAME] = this._claims.viewIdentifiers;
    }

    if (this._claims.parameters.length) {
      payload[PARAMETERS_CLAIM_NAME] = this._claims.parameters.map((parameter) => {
        const mapping = {
          field: parameter.field,
          op: parameter.op,
        };

        if (parameter.any) {
          mapping.any = parameter.any.slice();
        } else if (Array.isArray(parameter.value)) {
          mapping.any = parameter.value.slice();
        } else {
          mapping.value = parameter.value;
        }

        return mapping;
      });
    }

    if (Object.getOwnPropertyNames(this._claims.attributes).length) {
      payload[ATTRIBUTES_CLAIM_NAME] = this._claims.attributes;
    }

    encrypter.update(JSON.stringify(payload)).final().then((token) => {
      callback(null, token);
    }).catch((err) => {
      callback(err);
    });
  }).catch((err) => {
    callback(err);
  });
};

exports.ProjectTokenBuilder = ProjectTokenBuilder;