index.js

/* @flow weak */

'use strict'

const passport = require('passport-strategy')
const crypto   = require('crypto')

class WixAppStrategy extends passport.Strategy {
	/**
	 * `WixAppStrategy` constructor.
	 *
	 * The authentication strategy authenticates requests based on the
	 * `instance` query param, sent by WIX Applications
	 *
	 * @see  https://dev.wix.com/docs/infrastructure/app-instance-id/
	 * @see  https://dev.wix.com/docs/infrastructure/app-instance/#instance-properties
	 *
	 * @example
	 *     passport.use(new LocalStrategy({secret: 'your-wix-secret'},
	 *       function verifyCallback(instance, done) {
	 *         WixApp.findOne({ appId: instance.instanceId }, function (err, wixapp) {
	 *           done(err, wixapp);
	 *         });
	 *       }
	 *     ));
	 *
	 * @param {object}                  options
	 * Options for strategy
	 * @param {string}                  options.secret
	 * Your WIX-secret
	 * @param {boolean|number|function} [options.signDateThreshold=false]
	 * callback for validation of the signDate (passed by WIX).
	 * @param {boolean}                 [options.passReqToCallback=false]
	 * pass Express `req` is the first argument to the verify callback when `true`
	 * @param {Function}                verify
	 * Verification callback
	 *
	 * @returns {WixAppStrategy} WixAppStrategy instance
	 *.
	 * @public
	 */

	constructor(options, verify) {
		super()

		if (typeof options === 'function') {
			verify = options
			options = {}
		}

		if (typeof options.secret !== 'string') {
			throw new TypeError(
				'WixAppStrategy requires the "secret" option (string)'
			)
		}

		this._secret = options.secret
		if (!verify) { throw new TypeError('WixAppStrategy requires a verify callback') }

		this._signDateDiffMax = 10000
		this._isSignDateValid = this._getValidatorForSignDate(options.signDateThreshold)

		this.name = 'wix-app'
		this._verify = verify
		this._passReqToCallback = !!options.passReqToCallback
	}

	_getValidatorForSignDate(value) {
		if (false === value || typeof value === 'undefined') {
			return ()=>true
		}

		if (typeof value === 'function') {
			return value
		}

		if (typeof value === 'number') {
			this._signDateDiffMax = value
		}

		return this._defaultValidatorSignDate
	}

	_defaultValidatorSignDate(signDate) {
		return Math.abs(signDate.valueOf() - new Date().valueOf()) < this._signDateDiffMax
	}

	_urlBase64decode(str, encoding) {
		str = str
			.replace('-', '+')
			.replace('_', '/')

		return new Buffer(str, 'base64').toString(encoding)
	}

	_parseInstance(secret, instance) {
		// split the instance into digest and data
		let _splitVar = instance.split('.')
		let digest = _splitVar[0]
		const data = _splitVar[1]

		// sign the data using hmac-sha1-256
		const hmac = crypto.createHmac('sha256', secret)

		const myDigest = hmac.update(data).digest('base64')
		digest = this._urlBase64decode(digest, 'base64')

		if (myDigest !== digest) {
			return null
		}

		const instanceObj = JSON.parse(this._urlBase64decode(data, 'utf8'))

		instanceObj.aid = instanceObj.aid || null
		instanceObj.uid = instanceObj.uid || null
		instanceObj.permissions = instanceObj.permissions || null

		// Extensions:
		_splitVar = instanceObj.ipAndPort.split('/')
		const ip = _splitVar[0]
		const port = parseInt(_splitVar[1])
		const signDate = new Date(instanceObj.signDate)

		instanceObj.ext = {
			port,
			ip,
			signDate,
		}

		return instanceObj
	}

	/**
	 * Authenticate request based on the contents of a Wix query-parameters.
	 *
	 * @param {Object} req
	 * @package
	 */
	authenticate(req, options) {
		options = options || {}

		const instance = req && req.query && req.query.instance
		if (!instance) {
			return this.fail({ message: options.badRequestMessage || 'Missing WIX-instance query-parameter' }, 401)
		}

		const instanceObj = this._parseInstance(this._secret, instance)
		if (!instanceObj) {
			return this.fail({ message: options.badRequestMessage || 'Invalid WIX-instance'}, 403)
		}

		if (!this._isSignDateValid(instanceObj.ext.signDate)) {
			return this.fail({ message: options.badRequestMessage || 'Expired WIX-instance'}, 403)
		}

		const self = this
		function verifyDone(err, user, info) {
			if (err) {
				return self.error(err)
			}
			if (!user) {
				return self.fail(info)
			}
			self.success(user, info)
		}

		return this._tryAuthenticate(req, instanceObj, verifyDone)
	}

	_tryAuthenticate(req, instanceObj, callback) {
		try {
			if (this._passReqToCallback) {
				return this._verify(req, instanceObj, callback)
			} else {
				return this._verify(instanceObj, callback)
			}
		} catch (ex) {
			return this.error(ex)
		}
	}
}

// Expose `WixAppStrategy` directly from package.
exports = module.exports = WixAppStrategy

// Export constructors.
exports.Strategy = WixAppStrategy
exports.WixAppStrategy = WixAppStrategy