Skip to content

Instantly share code, notes, and snippets.

@therebelrobot
Last active April 10, 2018 23:35
Show Gist options
  • Save therebelrobot/f2ba1c3c05562510bee3191f213e7805 to your computer and use it in GitHub Desktop.
Save therebelrobot/f2ba1c3c05562510bee3191f213e7805 to your computer and use it in GitHub Desktop.
Uservoice SSO Token Generation (Node 8.x)
const crypto = require('crypto');
/**
* Convert a valid Uservoice user object into an SSO token
*
* Uservoice documentation: https://developer.uservoice.com/docs/single-sign-on/single-sign-on/
*
* ##### documented on 4/10/2018, 4:29:17 PM
*
* #### gotchas
* Make sure the user object contains the required fields specified in the documentation
*
* #### example
* ```
* const uvToken = new UserVoiceToken(ssoKey, subdomain)
* const user = {
* guid: guid,
* display_name: displayName,
* email: email,
* expires: moment().utc().add(2, 'minutes').format(YYYY-MM-DDTHH:mm:ss)
* }
* const token = uvToken.generate(user)
* const urlForSSO = `https://feedback.uservoice.com?sso=${token}`
* ```
*/
class UserVoiceToken {
private iv;
private key;
private ssoKey;
private subdomain;
private algorithm = 'aes-128-cbc';
private ivText = 'OpenSSL for Ruby';
constructor(ssoKey, subdomain) {
if (!ssoKey) { throw new Error('No SSO Key provided.'); }
if (typeof ssoKey !== 'string') { throw new Error('SSO Key needs to be a string.'); }
if (!subdomain) { throw new Error('No Subdomain provided.'); }
if (typeof subdomain !== 'string') { throw new Error('Subdomain needs to be a string.'); }
this.ssoKey = ssoKey;
this.subdomain = subdomain;
this.iv = Buffer.from(this.ivText);
this.key = this.generateCypherKey(`${this.ssoKey}${this.subdomain}`);
}
public generate(user) {
const userJSON = JSON.stringify(user);
const encryptedJSON = this.encrypt(userJSON);
const websafeEncryptedJSON = this.webSafe64(encryptedJSON);
return websafeEncryptedJSON;
}
private generateCypherKey (password) {
const hash = crypto.createHash('sha1');
const encodedHash = hash.update(password, 'utf8');
const digest = encodedHash.digest();
return Buffer.from(digest).slice(0, 16);
}
private encrypt (text) {
const cipher = crypto.createCipheriv(this.algorithm, this.key, this.iv);
const xored = Buffer.from(text);
for (let i = 0; i < 16; xored[i] = xored[i] ^ this.iv[i++]); // xor the iv into the first 16 bytes. This is documented literally nowhere.
const encrypted = cipher.update(xored, null, 'base64') + cipher.final('base64');
return encrypted;
}
private webSafe64 (base64) {
return base64.split('/').map(encodeURIComponent).join('/');
}
}
module.exports = UserVoiceToken;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment