Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export Client from './client';
export User from './user';
export Snippet from './snippet';
export UserData from './user-data';

import crypto from 'crypto';

Expand Down
62 changes: 62 additions & 0 deletions lib/user-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { IdentityVerification } from './index';
import htmlencode from 'htmlencode';

export default class UserData {
constructor(settings) {
this.loggedOut = !settings.user_id && !settings.email;

if (!settings.app_id) {
throw new Error('You must provide an app_id in your Intercom settings');
}
if (!this.loggedOut && !settings.verificationSecret) {
throw new Error('You must provide your verification secret in your Intercom settings');
}

this.settings = settings;
}
json() {
this.setUserHash();
return this.escapedSettings(this.settings);
}
getVerificationSecret() {
const { verificationSecret } = this.settings;
delete this.settings.verificationSecret;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to delete the secret, we should ensure json can be called multiple times by skipping setUserHash if user_hash is already set in this.settings

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

return verificationSecret;
}
setUserHash() {
if (this.loggedOut || this.settings.user_hash !== undefined) {
return;
}

const { verificationSecret } = this.settings;
delete this.settings.verificationSecret;
const identifier = this.settings.user_id ? this.settings.user_id.toString() : this.settings.email;


const userHash = IdentityVerification.userHash({
secretKey: verificationSecret,
identifier
});

this.settings.user_hash = userHash;
}
escapedSettings(settings) {
const intercomSettings = {};
Object.keys(settings).map(key => {
if (typeof settings[key] === 'object' && settings[key] !== null) {
intercomSettings[key] = this.escapedSettings(settings[key]);
} else {
const escapedKey = this.escapeString(key);
const value = this.escapeString(settings[key]);
intercomSettings[escapedKey] = value;
}
});
return intercomSettings;
}
escapeString(string) {
if (typeof string === 'string') {
string = htmlencode.htmlEncode(string).replace(/\"/gi, '\\"');
}
return string;
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
],
"dependencies": {
"bluebird": "^3.3.4",
"htmlencode": "^0.0.4",
"request": "^2.83.0",
"htmlencode": "^0.0.4"
"sinon": "^4.1.3"
},
"devDependencies": {
"babel-core": "^6.7.4",
Expand Down
166 changes: 166 additions & 0 deletions test/user-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import UserData from '../lib/user-data';
import { IdentityVerification } from '../lib/index';
import assert from 'assert';
import sinon from 'sinon';

describe('userData', () => {
it('should be able to grab the verification secret', () => {
const settings = {
verificationSecret: 'abc123',
app_id: 'xyz789',
user_id: 1
};
const userData = new UserData(settings);
assert.equal(userData.getVerificationSecret(), 'abc123');
});

it('should grab the user_id as the identifier', () => {
const settings = {
verificationSecret: 'abc123',
app_id: 'xyz789',
user_id: 1,
email: 'jess@intercom.io'
};
const userData = new UserData(settings);
const userHash = sinon.spy(IdentityVerification, 'userHash');
userData.json();
userHash.restore();
sinon.assert.calledWith(userHash, {
secretKey: 'abc123',
identifier: '1'
});
});

it('should grab the email as the identifier if no user_id', () => {
const settings = {
verificationSecret: 'abc123',
app_id: 'xyz789',
email: 'jess@intercom.io'
};
const userData = new UserData(settings);
const userHash = sinon.spy(IdentityVerification, 'userHash');
userData.json();
userHash.restore();
sinon.assert.calledWith(userHash, {
secretKey: 'abc123',
identifier: 'jess@intercom.io'
});
});

it('should throw an error if there\'s no verification secret', () => {
const settings = {
app_id: 'xyz789',
user_id: 1
};
assert.throws(() => new UserData(settings), Error);
});

it('should error if there\'s no app_id', () => {
const settings = {
verificationSecret: 'abc123',
user_id: 1
};
assert.throws(() => new UserData(settings), Error);
});

it('should return the logged out userData if no identifier', () => {
const settings = {
app_id: 'xyz789'
};
const userData = new UserData(settings);
assert.equal(JSON.stringify(userData.json()), JSON.stringify({ app_id: 'xyz789'}));
});

it('should escape bad stuff in values', () => {
const settings = {
verificationSecret: 'abc123',
app_id: 'xyz789',
email: 'jess@"<script>alert(1)</script>intercom.io'
};
const userData = new UserData(settings);
assert.equal(userData.json().email, 'jess@\\"&lt;script&gt;alert(1)&lt;/script&gt;intercom.io');
});

it('should escape bad stuff in object keys', () => {
const settings = {
verificationSecret: 'abc123',
app_id: 'xyz789',
user_id: '1'
};
settings['<script>doSomethingEvil();</script>'] = 'jess@"<script>alert(1)</script>intercom.io';
const userData = new UserData(settings);
const result = userData.json();
assert.equal(result['&lt;script&gt;doSomethingEvil();&lt;/script&gt;'], 'jess@\\\"&lt;script&gt;alert(1)&lt;/script&gt;intercom.io');
});

it('should escape bad stuff in next object keys and values', () => {
const settings = {
verificationSecret: 'abc123',
app_id: 'xyz789',
user_id: '1'
};
settings.maliciousSettings = {
'<script>doSomethingEvil();</script>': 'jess@"<script>alert(1)</script>intercom.io'
};
const userData = new UserData(settings);
const result = userData.json();
assert.equal(result.maliciousSettings['&lt;script&gt;doSomethingEvil();&lt;/script&gt;'], 'jess@\\"&lt;script&gt;alert(1)&lt;/script&gt;intercom.io');
});

it('should not include the verification secret in the userData', () => {
const settings = {
verificationSecret: 'abc123',
app_id: 'xyz789',
user_id: 1,
email: 'jess@intercom.io',
name: 'Jess OB',
company: {
id: 123,
name: 'Intercom'
}
};
const userData = new UserData(settings);
const result = userData.json();
assert.equal(Object.keys(result).indexOf(settings.verificationSecret), -1);
});

it('should skip setUserHash if user_hash is already defined', () => {
const settings = {
verificationSecret: 'abc123',
app_id: 'xyz789',
user_id: 1
};
const userData = new UserData(settings);
const userHash = sinon.spy(IdentityVerification, 'userHash');
userData.json();
userData.json();
userHash.restore();
sinon.assert.calledOnce(userHash);
});

it('should return the userData', () => {
const settings = {
verificationSecret: 'abc123',
app_id: 'xyz789',
user_id: 1,
email: 'jess@intercom.io',
name: 'Jess OB',
company: {
id: 123,
name: 'Intercom'
}
};
const userData = new UserData(settings);
assert.equal(JSON.stringify(userData.json()), JSON.stringify({
app_id: 'xyz789',
user_id: 1,
email: 'jess@intercom.io',
name: 'Jess OB',
company: {
id: 123,
name: 'Intercom'
},
user_hash: 'f02877f24c9dd37542268a28627ebaf2e07d0d114d9482abcdc20f60874b40b3'
}));
});
});