Skip to content

Commit e3633c0

Browse files
committed
Add getIdTokenResult method to mock User
This commit ensures mock users are created with a correspondng IdTokenResult and adds the proper Firebase SDK method to retrieve it. I've taken care to synchronize the new method with 'getIdToken'.
1 parent 92f2fba commit e3633c0

File tree

2 files changed

+347
-12
lines changed

2 files changed

+347
-12
lines changed

src/user.js

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ var Promise = require('rsvp').Promise;
66
function MockFirebaseUser(ref, data) {
77
this._auth = ref;
88
this._idtoken = Math.random().toString();
9-
this.customClaims = {};
9+
this._tokenValidity = _tokenValidity(
10+
data._tokenValidity ? data._tokenValidity : {}
11+
);
12+
this.customClaims = data.customClaims || {};
1013
this.uid = data.uid;
1114
this.email = data.email;
1215
this.password = data.password;
@@ -21,6 +24,15 @@ function MockFirebaseUser(ref, data) {
2124
this.refreshToken = data.refreshToken;
2225
}
2326

27+
MockFirebaseUser.msg_tokenExpiresBeforeIssuance =
28+
'Auth token expires before it is issued';
29+
MockFirebaseUser.msg_tokenIssuedBeforeAuth =
30+
'Auth token was issued before the user authenticated';
31+
MockFirebaseUser.msg_tokenAuthedInTheFuture =
32+
'Auth token shows user authenticating in the future';
33+
MockFirebaseUser.msg_tokenIssuedInTheFuture =
34+
'Auth token was issued in the future';
35+
2436
MockFirebaseUser.prototype.clone = function () {
2537
var user = new MockFirebaseUser(this._auth, this);
2638
user._idtoken = this._idtoken;
@@ -105,10 +117,62 @@ MockFirebaseUser.prototype.getIdToken = function (forceRefresh) {
105117
var self = this;
106118
return new Promise(function(resolve) {
107119
if (forceRefresh) {
108-
self._idtoken = Math.random().toString();
120+
self._refreshIdToken();
109121
}
110122
resolve(self._idtoken);
111123
});
112124
};
113125

126+
MockFirebaseUser.prototype.getIdTokenResult = function (forceRefresh) {
127+
if (forceRefresh) {
128+
this._refreshIdToken();
129+
}
130+
131+
return Promise.resolve({
132+
authTime: this._tokenValidity.authTime.toISOString(),
133+
issuedAtTime: this._tokenValidity.issuedAtTime.toISOString(),
134+
expirationTime: this._tokenValidity.expirationTime.toISOString(),
135+
signInProvider: this.providerId || null,
136+
claims: this.customClaims,
137+
token: this._idtoken,
138+
});
139+
};
140+
141+
MockFirebaseUser.prototype._refreshIdToken = function () {
142+
this._tokenValidity.issuedAtTime = new Date();
143+
this._tokenValidity.expirationTime = defaultExpirationTime(new Date());
144+
this._idtoken = Math.random().toString();
145+
};
146+
147+
/** Create a user's internal token validity store
148+
*
149+
* @param data the `data.idTokenResult` object from the User constructor
150+
* @return object that is to become the User's _tokenValidity member
151+
*/
152+
function _tokenValidity(data) {
153+
const now = new Date();
154+
const authTime = data.authTime ?
155+
data.authTime : new Date();
156+
const issuedTime = data.issuedAtTime || new Date(authTime.getTime());
157+
const expirationTime = data.expirationTime ?
158+
data.expirationTime : defaultExpirationTime(issuedTime);
159+
if (expirationTime < issuedTime) {
160+
throw new Error(MockFirebaseUser.msg_tokenExpiresBeforeIssuance);
161+
} else if (issuedTime < authTime) {
162+
throw new Error(MockFirebaseUser.msg_tokenIssuedBeforeAuth);
163+
} else if (now < authTime) {
164+
throw new Error(MockFirebaseUser.msg_tokenAuthedInTheFuture);
165+
} else if (now < issuedTime) {
166+
throw new Error(MockFirebaseUser.msg_tokenIssuedInTheFuture);
167+
} else return {
168+
authTime: authTime,
169+
issuedAtTime: issuedTime,
170+
expirationTime: expirationTime,
171+
};
172+
}
173+
174+
function defaultExpirationTime(issuedTime) {
175+
return new Date(issuedTime.getTime() + 3600000);
176+
}
177+
114178
module.exports = MockFirebaseUser;

test/unit/user.js

Lines changed: 281 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,73 @@
11
'use strict';
22

3-
var expect = require('chai').use(require('sinon-chai')).expect;
4-
var sinon = require('sinon');
5-
var User = require('../../src/user');
6-
var Firebase = require('../../').MockFirebase;
7-
8-
describe('User', function () {
9-
var auth;
10-
beforeEach(function () {
3+
const expect = require('chai').use(require('sinon-chai')).expect;
4+
const sinon = require('sinon');
5+
const User = require('../../src/user');
6+
const Firebase = require('../../').MockFirebase;
7+
const _isEqual = require('lodash.isequal');
8+
const _cloneDeep = require('lodash.clonedeep');
9+
10+
describe('User', function() {
11+
let auth;
12+
let now;
13+
let clock;
14+
15+
beforeEach(function() {
1116
auth = new Firebase().child('data');
1217
auth.autoFlush();
18+
now = new Date(randomTimestamp());
19+
clock = sinon.useFakeTimers(now);
20+
});
21+
22+
afterEach(() => {
23+
clock.restore();
24+
});
25+
26+
describe('#constructor', function() {
27+
28+
it('should reject ID tokens that expire before the issuance time', () => {
29+
expect(() => {
30+
const t = randomTimestamp();
31+
new User(auth, {
32+
_tokenValidity: {
33+
authTime: new Date(t - 2),
34+
issuedAtTime: new Date(t),
35+
expirationTime: new Date(t - 1),
36+
}
37+
});
38+
}).to.throw(User.msg_tokenExpiresBeforeIssuance);
39+
});
40+
41+
it('should reject ID tokens that are issued before the auth time', () => {
42+
expect(() => {
43+
const t = randomTimestamp();
44+
new User(auth, {
45+
_tokenValidity: {
46+
authTime: new Date(t),
47+
issuedAtTime: new Date(t - 1),
48+
}
49+
});
50+
}).to.throw(User.msg_tokenIssuedBeforeAuth);
51+
});
52+
53+
it('should reject ID tokens that are issued in the future', () => {
54+
expect(() => new User(auth, {
55+
_tokenValidity: {
56+
authTime: new Date(now.getTime() - 1),
57+
issuedAtTime: new Date(now.getTime() + 1),
58+
},
59+
})).to.throw(User.msg_tokenIssuedInTheFuture);
60+
});
61+
62+
it('should reject ID tokens that show the user authenticating in the future', () => {
63+
expect(() => {
64+
new User(auth, {
65+
_tokenValidity: {
66+
authTime: new Date(now.getTime() + 1),
67+
},
68+
});
69+
}).to.throw(User.msg_tokenAuthedInTheFuture);
70+
});
1371
});
1472

1573
describe('#delete', function() {
@@ -135,9 +193,222 @@ describe('User', function () {
135193
});
136194

137195
it('should refresh token', function() {
138-
var user = new User(auth, {});
139-
var token = user._idtoken;
196+
const user = new User(auth, {});
197+
const token = user._idtoken;
140198
return expect(user.getIdToken(true)).to.eventually.not.equal(token);
141199
});
200+
201+
it('should refresh token result', function() {
202+
const authTime = new Date(randomPastTimestamp());
203+
const user = new User(auth, {
204+
_tokenValidity: {
205+
authTime: authTime,
206+
issuedAtTime: authTime,
207+
},
208+
});
209+
return expect(user.getIdToken(true)
210+
.then(() => user.getIdTokenResult(false))
211+
.then(r =>
212+
r.issuedAtTime === now.toISOString() &&
213+
r.expirationTime === new Date(now.getTime() + 3600000).toISOString()
214+
)
215+
).to.eventually.equal(true);
216+
});
217+
});
218+
219+
describe('#getIdTokenResult', function() {
220+
221+
it('should use defaults if the id token result param is omitted', () => {
222+
expect(new User(auth, {}).getIdTokenResult())
223+
.to.eventually.deep.equal(new User(auth, {_tokenValidity: {}}));
224+
});
225+
226+
describe('without forceRefresh', () => {
227+
describe('.authTime', () => {
228+
it('should use provided auth time', () => {
229+
const authTime = new Date(randomPastTimestamp());
230+
const user = new User(auth, {
231+
_tokenValidity: {
232+
authTime: authTime,
233+
},
234+
});
235+
return expect(user.getIdTokenResult().then(r => r.authTime))
236+
.to.eventually.equal(authTime.toISOString());
237+
});
238+
239+
it('should default auth time to current time', () => {
240+
const user = new User(auth, {_tokenValidity: {}});
241+
return expect(user.getIdTokenResult().then(r => r.authTime))
242+
.to.eventually.equal(now.toISOString());
243+
});
244+
});
245+
246+
describe('.issuedAtTime', () => {
247+
it('should use provided issued-at time', () => {
248+
const issuedTs = randomPastTimestamp();
249+
const issuedTime = new Date(issuedTs);
250+
const user = new User(auth, {
251+
_tokenValidity: {
252+
authTime: new Date(issuedTs - 1),
253+
issuedAtTime: issuedTime,
254+
},
255+
});
256+
return expect(user.getIdTokenResult().then(r => r.issuedAtTime))
257+
.to.eventually.equal(issuedTime.toISOString());
258+
});
259+
260+
it('should default to auth time', () => {
261+
const authTime = new Date(randomPastTimestamp());
262+
const user = new User(auth, {
263+
_tokenValidity: {
264+
authTime: authTime,
265+
},
266+
});
267+
return expect(user.getIdTokenResult().then(r => r.issuedAtTime))
268+
.to.eventually.equal(authTime.toISOString());
269+
});
270+
});
271+
272+
describe('.expirationTime', () => {
273+
it('should use provided expiration time', () => {
274+
const expTime = new Date(now.getTime() + 1);
275+
const user = new User(auth, {
276+
_tokenValidity: {
277+
expirationTime: expTime,
278+
},
279+
});
280+
return expect(user.getIdTokenResult().then(r => r.expirationTime))
281+
.to.eventually.equal(expTime.toISOString());
282+
});
283+
284+
it('should default to issued at time plus 1 hour', () => {
285+
const authTime = new Date(now.getTime() - 1);
286+
const issuedTime = new Date(now.getTime());
287+
const expTime = new Date(now.getTime() + 3600000);
288+
const user = new User(auth, {
289+
_tokenValidity: {
290+
authTime: authTime,
291+
issuedAtTime: issuedTime,
292+
},
293+
});
294+
return expect(user.getIdTokenResult().then(r => r.expirationTime))
295+
.to.eventually.equal(expTime.toISOString());
296+
});
297+
});
298+
299+
describe('.signInProvider', () => {
300+
it('should use User\'s providerId string', () => {
301+
const providerName = 'google';
302+
const user = new User(auth, {
303+
providerId: providerName,
304+
_tokenValidity: {},
305+
});
306+
return expect(user.getIdTokenResult()
307+
.then(r => r.signInProvider)
308+
).to.eventually.equal(providerName);
309+
});
310+
311+
it('should default to null', () => {
312+
const user = new User(auth, {_tokenValidity: {}});
313+
return expect(user.getIdTokenResult()
314+
.then(r => r.signInProvider)
315+
).to.eventually.equal(null);
316+
});
317+
});
318+
319+
describe('.claims', () => {
320+
it('should use user\'s customClaims object', () => {
321+
const claims = {'testclaim': 'abcd'};
322+
const user = new User(auth, {
323+
_tokenValidity: {},
324+
customClaims: claims,
325+
});
326+
return expect(user.getIdTokenResult().then(r => r.claims))
327+
.to.eventually.deep.equal(claims);
328+
});
329+
330+
it('should default to empty object', () => {
331+
const user = new User(auth, {_tokenValidity: {},});
332+
return expect(user.getIdTokenResult().then(r => r.claims))
333+
.to.eventually.deep.equal({});
334+
});
335+
});
336+
337+
describe('.token', () => {
338+
it('should be the same as returned from getIdToken', () => {
339+
const user = new User(auth, {_tokenValidity: {},});
340+
return expect(Promise.all([
341+
user.getIdTokenResult().then(r => r.token),
342+
user.getIdToken()
343+
]).then(([t1, t2]) => t1 === t2)).to.eventually.equal(true);
344+
});
345+
});
346+
});
347+
348+
describe('with forceRefresh', () => {
349+
it('persists the new token', () => {
350+
const user = new User(auth, {
351+
_tokenValidity: {
352+
authTime: new Date(randomPastTimestamp()),
353+
},
354+
});
355+
return expect(user.getIdTokenResult(true)
356+
.then(_t1 => {
357+
const t1 = _cloneDeep(_t1);
358+
return user.getIdTokenResult(false).then(t2 =>
359+
_isEqual(t1, t2)
360+
);
361+
})
362+
).to.eventually.equal(true);
363+
});
364+
365+
it('should use authTime from previous token', () => {
366+
const authTime = new Date(randomPastTimestamp());
367+
const user = new User(auth, {
368+
_tokenValidity:
369+
{authTime: authTime},
370+
});
371+
return expect(user.getIdTokenResult(true).then(r => r.authTime))
372+
.to.eventually.equal(authTime.toISOString());
373+
});
374+
375+
it('should use current time as issuance time', () => {
376+
const user = new User(auth, {
377+
_tokenValidity: {
378+
authTime: new Date(randomPastTimestamp()),
379+
}
380+
});
381+
return expect(user.getIdTokenResult(true).then(r => r.issuedAtTime))
382+
.to.eventually.equal(now.toISOString());
383+
});
384+
385+
it('should expire one hour after issuance', () => {
386+
const user = new User(auth, {
387+
_tokenValidity: {
388+
authTime: new Date(randomPastTimestamp()),
389+
},
390+
});
391+
const expTime = new Date(now.getTime() + 3600000);
392+
return expect(user.getIdTokenResult(true).then(r => r.expirationTime))
393+
.to.eventually.equal(expTime.toISOString());
394+
});
395+
396+
it('should generate a new token', () => {
397+
const user = new User(auth, {_tokenValidity: {},});
398+
return expect(user.getIdToken(false)
399+
.then(oldToken => user.getIdTokenResult(true)
400+
.then(newTokenResult => oldToken === newTokenResult.token)
401+
)
402+
).to.eventually.equal(false);
403+
});
404+
});
142405
});
143406
});
407+
408+
function randomTimestamp() {
409+
return Math.floor(Math.random() * 4000000000000);
410+
}
411+
412+
function randomPastTimestamp() {
413+
return Math.floor(Math.random() * Date.now());
414+
}

0 commit comments

Comments
 (0)