Skip to content

Commit 4bd0497

Browse files
authored
Merge pull request #173 from intercom/jess/user-data
Add a user-data class for generating intercomSettings
2 parents 34b1ac4 + ef96cba commit 4bd0497

File tree

4 files changed

+231
-1
lines changed

4 files changed

+231
-1
lines changed

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export Client from './client';
22
export User from './user';
33
export Snippet from './snippet';
4+
export UserData from './user-data';
45

56
import crypto from 'crypto';
67

lib/user-data.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { IdentityVerification } from './index';
2+
import htmlencode from 'htmlencode';
3+
4+
export default class UserData {
5+
constructor(settings) {
6+
this.loggedOut = !settings.user_id && !settings.email;
7+
8+
if (!settings.app_id) {
9+
throw new Error('You must provide an app_id in your Intercom settings');
10+
}
11+
if (!this.loggedOut && !settings.verificationSecret) {
12+
throw new Error('You must provide your verification secret in your Intercom settings');
13+
}
14+
15+
this.settings = settings;
16+
}
17+
json() {
18+
this.setUserHash();
19+
return this.escapedSettings(this.settings);
20+
}
21+
getVerificationSecret() {
22+
const { verificationSecret } = this.settings;
23+
delete this.settings.verificationSecret;
24+
return verificationSecret;
25+
}
26+
setUserHash() {
27+
if (this.loggedOut || this.settings.user_hash !== undefined) {
28+
return;
29+
}
30+
31+
const { verificationSecret } = this.settings;
32+
delete this.settings.verificationSecret;
33+
const identifier = this.settings.user_id ? this.settings.user_id.toString() : this.settings.email;
34+
35+
36+
const userHash = IdentityVerification.userHash({
37+
secretKey: verificationSecret,
38+
identifier
39+
});
40+
41+
this.settings.user_hash = userHash;
42+
}
43+
escapedSettings(settings) {
44+
const intercomSettings = {};
45+
Object.keys(settings).map(key => {
46+
if (typeof settings[key] === 'object' && settings[key] !== null) {
47+
intercomSettings[key] = this.escapedSettings(settings[key]);
48+
} else {
49+
const escapedKey = this.escapeString(key);
50+
const value = this.escapeString(settings[key]);
51+
intercomSettings[escapedKey] = value;
52+
}
53+
});
54+
return intercomSettings;
55+
}
56+
escapeString(string) {
57+
if (typeof string === 'string') {
58+
string = htmlencode.htmlEncode(string).replace(/\"/gi, '\\"');
59+
}
60+
return string;
61+
}
62+
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
],
2020
"dependencies": {
2121
"bluebird": "^3.3.4",
22+
"htmlencode": "^0.0.4",
2223
"request": "^2.83.0",
23-
"htmlencode": "^0.0.4"
24+
"sinon": "^4.1.3"
2425
},
2526
"devDependencies": {
2627
"babel-core": "^6.7.4",

test/user-data.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import UserData from '../lib/user-data';
2+
import { IdentityVerification } from '../lib/index';
3+
import assert from 'assert';
4+
import sinon from 'sinon';
5+
6+
describe('userData', () => {
7+
it('should be able to grab the verification secret', () => {
8+
const settings = {
9+
verificationSecret: 'abc123',
10+
app_id: 'xyz789',
11+
user_id: 1
12+
};
13+
const userData = new UserData(settings);
14+
assert.equal(userData.getVerificationSecret(), 'abc123');
15+
});
16+
17+
it('should grab the user_id as the identifier', () => {
18+
const settings = {
19+
verificationSecret: 'abc123',
20+
app_id: 'xyz789',
21+
user_id: 1,
22+
23+
};
24+
const userData = new UserData(settings);
25+
const userHash = sinon.spy(IdentityVerification, 'userHash');
26+
userData.json();
27+
userHash.restore();
28+
sinon.assert.calledWith(userHash, {
29+
secretKey: 'abc123',
30+
identifier: '1'
31+
});
32+
});
33+
34+
it('should grab the email as the identifier if no user_id', () => {
35+
const settings = {
36+
verificationSecret: 'abc123',
37+
app_id: 'xyz789',
38+
39+
};
40+
const userData = new UserData(settings);
41+
const userHash = sinon.spy(IdentityVerification, 'userHash');
42+
userData.json();
43+
userHash.restore();
44+
sinon.assert.calledWith(userHash, {
45+
secretKey: 'abc123',
46+
identifier: '[email protected]'
47+
});
48+
});
49+
50+
it('should throw an error if there\'s no verification secret', () => {
51+
const settings = {
52+
app_id: 'xyz789',
53+
user_id: 1
54+
};
55+
assert.throws(() => new UserData(settings), Error);
56+
});
57+
58+
it('should error if there\'s no app_id', () => {
59+
const settings = {
60+
verificationSecret: 'abc123',
61+
user_id: 1
62+
};
63+
assert.throws(() => new UserData(settings), Error);
64+
});
65+
66+
it('should return the logged out userData if no identifier', () => {
67+
const settings = {
68+
app_id: 'xyz789'
69+
};
70+
const userData = new UserData(settings);
71+
assert.equal(JSON.stringify(userData.json()), JSON.stringify({ app_id: 'xyz789'}));
72+
});
73+
74+
it('should escape bad stuff in values', () => {
75+
const settings = {
76+
verificationSecret: 'abc123',
77+
app_id: 'xyz789',
78+
email: 'jess@"<script>alert(1)</script>intercom.io'
79+
};
80+
const userData = new UserData(settings);
81+
assert.equal(userData.json().email, 'jess@\\"&lt;script&gt;alert(1)&lt;/script&gt;intercom.io');
82+
});
83+
84+
it('should escape bad stuff in object keys', () => {
85+
const settings = {
86+
verificationSecret: 'abc123',
87+
app_id: 'xyz789',
88+
user_id: '1'
89+
};
90+
settings['<script>doSomethingEvil();</script>'] = 'jess@"<script>alert(1)</script>intercom.io';
91+
const userData = new UserData(settings);
92+
const result = userData.json();
93+
assert.equal(result['&lt;script&gt;doSomethingEvil();&lt;/script&gt;'], 'jess@\\\"&lt;script&gt;alert(1)&lt;/script&gt;intercom.io');
94+
});
95+
96+
it('should escape bad stuff in next object keys and values', () => {
97+
const settings = {
98+
verificationSecret: 'abc123',
99+
app_id: 'xyz789',
100+
user_id: '1'
101+
};
102+
settings.maliciousSettings = {
103+
'<script>doSomethingEvil();</script>': 'jess@"<script>alert(1)</script>intercom.io'
104+
};
105+
const userData = new UserData(settings);
106+
const result = userData.json();
107+
assert.equal(result.maliciousSettings['&lt;script&gt;doSomethingEvil();&lt;/script&gt;'], 'jess@\\"&lt;script&gt;alert(1)&lt;/script&gt;intercom.io');
108+
});
109+
110+
it('should not include the verification secret in the userData', () => {
111+
const settings = {
112+
verificationSecret: 'abc123',
113+
app_id: 'xyz789',
114+
user_id: 1,
115+
116+
name: 'Jess OB',
117+
company: {
118+
id: 123,
119+
name: 'Intercom'
120+
}
121+
};
122+
const userData = new UserData(settings);
123+
const result = userData.json();
124+
assert.equal(Object.keys(result).indexOf(settings.verificationSecret), -1);
125+
});
126+
127+
it('should skip setUserHash if user_hash is already defined', () => {
128+
const settings = {
129+
verificationSecret: 'abc123',
130+
app_id: 'xyz789',
131+
user_id: 1
132+
};
133+
const userData = new UserData(settings);
134+
const userHash = sinon.spy(IdentityVerification, 'userHash');
135+
userData.json();
136+
userData.json();
137+
userHash.restore();
138+
sinon.assert.calledOnce(userHash);
139+
});
140+
141+
it('should return the userData', () => {
142+
const settings = {
143+
verificationSecret: 'abc123',
144+
app_id: 'xyz789',
145+
user_id: 1,
146+
147+
name: 'Jess OB',
148+
company: {
149+
id: 123,
150+
name: 'Intercom'
151+
}
152+
};
153+
const userData = new UserData(settings);
154+
assert.equal(JSON.stringify(userData.json()), JSON.stringify({
155+
app_id: 'xyz789',
156+
user_id: 1,
157+
158+
name: 'Jess OB',
159+
company: {
160+
id: 123,
161+
name: 'Intercom'
162+
},
163+
user_hash: 'f02877f24c9dd37542268a28627ebaf2e07d0d114d9482abcdc20f60874b40b3'
164+
}));
165+
});
166+
});

0 commit comments

Comments
 (0)