Skip to content

Typescript version, or at least typings #6

Description

@jhalborg

Hey,

Thanks a lot for the example implementation! I've gone ahead and implemented it pretty much 1:1 in Typescript, using the now built-in version of the crypto library in newer versions of Node instead of using the old dependency - so there is no need to npm install crypto.

I'm just going to leave it here for anyone needing it in the future:

import * as crypto from 'crypto';

export interface ShopifyMultipassTokenData {
  email: string;
  return_to?: string;
}

/**
 * We require only the customer email at the moment as defined by the interface above
 * but we add a created_at to the token to be able to determine when it was issued.
 * The created_at field, along with email, are the two mandatory fields for token generation
 *
 * @export
 * @interface ShopifyMultipassToken
 * @extends {ShopifyMultipassTokenData}
 */
export interface ShopifyMultipassToken extends ShopifyMultipassTokenData {
  created_at: string;
}

const BLOCK_SIZE = 16;
const SHOPIFY_DOMAIN_URL = 'https://yourdomain.myshopify.com';
/**
 * Utility class for generating an access token for Shopify on behalf of a user to help them log in
 *
 * @export
 * @class ShopifyMultipass
 */
export class ShopifyMultipass {
  private encryptionKey: Buffer;
  private signingKey: Buffer;
  constructor(shopifySecret: string) {
    const hash = crypto
      .createHash('sha256')
      .update(shopifySecret)
      .digest();
    this.encryptionKey = hash.slice(0, BLOCK_SIZE);
    // tslint:disable-next-line:no-magic-numbers
    this.signingKey = hash.slice(BLOCK_SIZE, 32);
  }

  public getUrl(customerData: ShopifyMultipassTokenData) {
    const token = this.encodeCustomerData(customerData);
    return `${SHOPIFY_DOMAIN_URL}/account/login/multipass/${token}`;
  }

  private encodeCustomerData(customerData: ShopifyMultipassTokenData) {
    const data: ShopifyMultipassToken = {
      created_at: new Date().toISOString(),
      email: customerData.email,
      return_to: customerData.return_to,
    };

    const cipherText = this.encrypt(JSON.stringify(data));

    // Create a signature (message authentication code) of the ciphertext
    // and encode everything using URL-safe Base64 (RFC 4648)
    let token = Buffer.concat([cipherText, this.sign(cipherText)]).toString('base64');
    token = token
      .replace(/\+/g, '-') // Replace + with -
      .replace(/\//g, '_'); // Replace / with _

    return token;
  }

  private encrypt(text: string) {
    // Use a random IV
    const iv = crypto.randomBytes(BLOCK_SIZE);

    // Shopify uses the aes-128-cbc algorithm. It is a valid option, in spite of the Node typings not recognizing it at the moment
    const cipher = crypto.createCipheriv('aes-128-cbc', this.encryptionKey, iv);

    // Use IV as first block of ciphertext
    return Buffer.concat([iv, cipher.update(text, 'utf8'), cipher.final()]);
  }

  private sign(data: any) {
    return crypto
      .createHmac('SHA256', this.signingKey)
      .update(data)
      .digest();
  }
}

@beaucoo - If you just want to add the typings as a .d.ts file to this repo, this is the resulting definition file from the code above:

export interface ShopifyMultipassTokenData {
    email: string;
    return_to?: string;
}
/**
 * We require only the customer email at the moment as defined by the interface above
 * but we add a created_at to the token to be able to determine when it was issued.
 * The created_at field, along with email, are the two mandatory fields for token generation
 *
 * @export
 * @interface ShopifyMultipassToken
 * @extends {ShopifyMultipassTokenData}
 */
export interface ShopifyMultipassToken extends ShopifyMultipassTokenData {
    created_at: string;
}
/**
 * Utility class for generating an access token for Shopify on behalf of a user to help them log in
 *
 * @export
 * @class ShopifyMultipass
 */
export declare class ShopifyMultipass {
    private encryptionKey;
    private signingKey;
    constructor(shopifySecret: string);
    getUrl(customerData: ShopifyMultipassTokenData): string;
    private encodeCustomerData;
    private encrypt;
    private sign;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions