Skip to content

Commit a372cff

Browse files
committed
Ensure refreshing ID tokens persists
- Add new method updateUser to the auth object, so that users can 'phone home' about ID token changes - Move ID token refresh into a publicly-exposed method - Make sure token refresh phones home
1 parent ae9f893 commit a372cff

File tree

5 files changed

+253
-58
lines changed

5 files changed

+253
-58
lines changed

API.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Only `MockFirebase` methods are included here. For details on normal Firebase AP
1515
- [`changeAuthState(user)`](#changeauthstateuser---undefined)
1616
- [`getUserByEmail(email)`](#getuserbyemailemail---promiseobject)
1717
- [`getUser(uid)`](#getuseruid---promiseobject)
18+
- [`updateUser(user)`](#updateuseruser---promiseobject)
1819
- [Server Timestamps](#server-timestamps)
1920
- [`setClock(fn)`](#firebasesetclockfn---undefined)
2021
- [`restoreClock()`](#firebasesetclockfn---undefined)
@@ -220,6 +221,12 @@ Finds a user previously created with [`createUser`](https://www.firebase.com/doc
220221

221222
Finds a user previously created with [`createUser`](https://www.firebase.com/docs/web/api/firebase/createuser.html). If no user was created with the specified `email`, the promise is rejected.
222223

224+
##### `updateUser(user)` -> `Promise<Object>`
225+
226+
Replace the existing user with a new one, by matching uid. Throws an
227+
error If no user exists whose uid matches the given user's uid. Returns
228+
the updated user.
229+
223230
## Server Timestamps
224231

225232
MockFirebase allow you to simulate the behavior of [server timestamps](https://www.firebase.com/docs/web/api/servervalue/timestamp.html) when using a real Firebase instance. Unless you use `Firebase.setClock`, `Firebase.ServerValue.TIMESTAMP` will be transformed to the current date (`Date.now()`) when your data change is flushed.

src/firebase-auth.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,7 @@ FirebaseAuth.prototype.getUser = function (uid, onComplete) {
7575
var user = null;
7676
err = err || self._validateExistingUid(uid);
7777
if (!err) {
78-
user = _.find(self._auth.users, function(u) {
79-
return u.uid == uid;
80-
});
78+
user = self._getUser(uid);
8179
if (onComplete) {
8280
onComplete(err, user.clone());
8381
}
@@ -91,6 +89,21 @@ FirebaseAuth.prototype.getUser = function (uid, onComplete) {
9189
});
9290
};
9391

92+
FirebaseAuth.prototype.updateUser = function (newUser) {
93+
const self = this;
94+
return new Promise((resolve, reject) => {
95+
this._defer('updateUser', _.toArray(arguments), () => {
96+
const i = _.findIndex(self._auth.users, u => u.uid === newUser.uid);
97+
if (i === -1) {
98+
return reject(new Error('Tried to update a nonexistent user'));
99+
} else {
100+
self._auth.users[i] = newUser.clone();
101+
return resolve(newUser);
102+
}
103+
});
104+
});
105+
};
106+
94107
// number of arguments
95108
var authMethods = {
96109
authWithCustomToken: 2,
@@ -205,6 +218,12 @@ FirebaseAuth.prototype._triggerAuthEvent = function () {
205218
});
206219
};
207220

221+
FirebaseAuth.prototype._getUser = function (uid) {
222+
return _.find(this._auth.users, function(u) {
223+
return u.uid === uid;
224+
});
225+
};
226+
208227
FirebaseAuth.prototype.getAuth = function () {
209228
return this.currentUser;
210229
};
@@ -279,7 +298,8 @@ FirebaseAuth.prototype._createUser = function (method, credentials, onComplete)
279298
phoneNumber: credentials.phoneNumber,
280299
emailVerified: credentials.emailVerified,
281300
displayName: credentials.displayName,
282-
photoURL: credentials.photoURL
301+
photoURL: credentials.photoURL,
302+
_tokenValidity: credentials._tokenValidity
283303
});
284304
self._auth.users.push(user);
285305
if (onComplete) {

src/user.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ MockFirebaseUser.prototype._refreshIdToken = function () {
142142
this._tokenValidity.issuedAtTime = new Date();
143143
this._tokenValidity.expirationTime = defaultExpirationTime(new Date());
144144
this._idtoken = Math.random().toString();
145+
return this._auth.updateUser(this)
146+
.then(() => this.getIdTokenResult())
147+
.catch(() => this.getIdTokenResult());
145148
};
146149

147150
/** Create a user's internal token validity store

test/unit/auth.js

Lines changed: 129 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -377,18 +377,33 @@ describe('Auth', function () {
377377
});
378378

379379
it('creates a new user with preset additional attributes', function () {
380-
ref.createUser({
381-
uid: 'uid1',
380+
const uid = 'uid1';
381+
const now = new Date().getTime();
382+
const authTime = new Date(now - 1);
383+
const issuedAtTime = new Date(now);
384+
const expirationTime = new Date(now + 1);
385+
ref.autoFlush();
386+
return ref.createUser({
387+
uid: uid,
382388
383389
password: 'new1',
384390
displayName: 'new user 1',
385391
emailVerified: true,
386-
}, spy);
387-
ref.flush();
388-
return Promise.all([
389-
expect(ref.getUser('uid1')).to.eventually.have.property('displayName', 'new user 1'),
390-
expect(ref.getUser('uid1')).to.eventually.have.property('emailVerified', true),
391-
]);
392+
_tokenValidity: {
393+
authTime: authTime,
394+
issuedAtTime: issuedAtTime,
395+
expirationTime: expirationTime,
396+
},
397+
}).then(() => Promise.all([
398+
expect(ref.getUser(uid)).to.eventually.have.property('displayName', 'new user 1'),
399+
expect(ref.getUser(uid)).to.eventually.have.property('emailVerified', true),
400+
expect(ref.getUser(uid).then(u => u.getIdTokenResult()).then(t => t.authTime))
401+
.to.eventually.equal(authTime.toISOString()),
402+
expect(ref.getUser(uid).then(u => u.getIdTokenResult()).then(t => t.issuedAtTime))
403+
.to.eventually.equal(issuedAtTime.toISOString()),
404+
expect(ref.getUser(uid).then(u => u.getIdTokenResult()).then(t => t.expirationTime))
405+
.to.eventually.equal(expirationTime.toISOString()),
406+
]));
392407
});
393408

394409
it('fails if credentials is not an object', function () {
@@ -860,7 +875,113 @@ describe('Auth', function () {
860875
})).to.be.rejectedWith(Error, 'custom error');
861876
});
862877
});
878+
});
879+
880+
881+
describe('#updateUser', () => {
882+
883+
beforeEach(() => {
884+
ref.autoFlush();
885+
});
886+
887+
it('should replace the existing user', () => {
888+
const uid = 123;
889+
const newUser = new User(ref, {
890+
uid: uid,
891+
892+
providerId: 'test-provider-id',
893+
});
894+
return ref.createUser({
895+
uid: uid,
896+
897+
}).then(() => ref.updateUser(newUser))
898+
.then(() => ref.getUser(uid))
899+
.then(updatedUser => expect(updatedUser).to.deep.equal(newUser));
900+
});
863901

902+
it('should select by uid', () => {
903+
const uid = 456;
904+
const email = '[email protected]';
905+
return ref.createUser({
906+
uid: 123,
907+
908+
}).then(u1 =>
909+
ref.createUser({
910+
uid: uid,
911+
912+
}).then(() => ref.updateUser(new User(ref, {
913+
uid: uid,
914+
email: email,
915+
}))).then(() => Promise.all([
916+
expect(ref.getUser(123)).to.eventually.deep.equal(u1),
917+
expect(ref.getUser(uid).then(u => u.email))
918+
.to.eventually.deep.equal(email),
919+
]))
920+
);
921+
});
922+
923+
it('should return the updated user', () => {
924+
const uid = 123;
925+
const email = '[email protected]';
926+
const newUser = new User(ref, {
927+
uid: uid,
928+
email: email,
929+
});
930+
return ref.createUser({
931+
uid: uid,
932+
933+
}).then(() => ref.updateUser(newUser))
934+
.then(rtn => expect(rtn).to.deep.equal(newUser));
935+
});
936+
937+
it('should store a referentially different user from the argument', () => {
938+
const uid = 123;
939+
const email = '[email protected]';
940+
const arg = new User(ref, {
941+
uid: uid,
942+
email: email,
943+
});
944+
return ref.createUser({
945+
uid: uid,
946+
947+
}).then(() => ref.updateUser(arg))
948+
.then(() => {
949+
arg.email = '[email protected]';
950+
return ref.getUser(uid);
951+
})
952+
.then(newUser => newUser.email)
953+
.then(newEmail => expect(newEmail).to.equal(email));
954+
});
955+
956+
it('should reject if the user does not exist', () => {
957+
return expect(ref.updateUser(new User(ref, {
958+
uid: 123,
959+
960+
}))).to.be.rejectedWith('Tried to update a nonexistent user');
961+
});
962+
963+
it('should wait for flush', () => {
964+
const uid = 123;
965+
const oldEmail = '[email protected]';
966+
const newEmail = '[email protected]';
967+
const newUser = new User(ref, {
968+
uid: uid,
969+
email: newEmail,
970+
});
971+
ref.createUser({
972+
uid: uid,
973+
email: oldEmail,
974+
});
975+
ref.autoFlush(false);
976+
ref.updateUser(newUser);
977+
const emailBeforeFlush = ref.getUser(uid).then(u => u.email);
978+
ref.flush();
979+
const emailAfterFlush = ref.getUser(uid).then(u => u.email);
980+
return Promise.all([
981+
expect(emailBeforeFlush).to.eventually.equal(oldEmail),
982+
expect(emailAfterFlush).to.eventually.equal(newEmail),
983+
]);
984+
});
864985
});
865986

866987
});

test/unit/user.js

Lines changed: 90 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -357,61 +357,105 @@ describe('User', function() {
357357
});
358358

359359
describe('with forceRefresh', () => {
360-
it('persists the new token', () => {
361-
const user = new User(auth, {
362-
_tokenValidity: {
363-
authTime: new Date(randomPastTimestamp()),
364-
},
365-
});
366-
return expect(user.getIdTokenResult(true)
367-
.then(_t1 => {
368-
const t1 = _cloneDeep(_t1);
369-
return user.getIdTokenResult(false).then(t2 =>
370-
_isEqual(t1, t2)
371-
);
372-
})
373-
).to.eventually.equal(true);
360+
361+
it('should refresh the ID token', () => {
362+
const user = new User(auth, {_tokenValidity: {},});
363+
const refreshIdToken = sinon.spy(user, '_refreshIdToken');
364+
return user.getIdTokenResult(true)
365+
.then(() => expect(refreshIdToken.called).to.be.true);
374366
});
367+
});
368+
});
375369

376-
it('should use authTime from previous token', () => {
377-
const authTime = new Date(randomPastTimestamp());
378-
const user = new User(auth, {
379-
_tokenValidity:
380-
{authTime: authTime},
381-
});
382-
return expect(user.getIdTokenResult(true).then(r => r.authTime))
383-
.to.eventually.equal(authTime.toISOString());
370+
describe('#_refreshIdToken', () => {
371+
372+
let now;
373+
let clock;
374+
375+
beforeEach(() => {
376+
auth.autoFlush();
377+
now = randomTimestamp();
378+
clock = sinon.useFakeTimers(now);
379+
});
380+
381+
afterEach(() => {
382+
clock.restore();
383+
});
384+
385+
it('should return a new token result', () => {
386+
const user = new User(auth, {_tokenValidity: {},});
387+
return expect(user.getIdToken(false)
388+
.then(oldToken => user._refreshIdToken()
389+
.then(newTokenResult => oldToken === newTokenResult.token)
390+
)
391+
).to.eventually.equal(false);
392+
});
393+
394+
it('should persist the new token result', () => {
395+
const user = new User(auth, {
396+
_tokenValidity: {
397+
authTime: new Date(randomPastTimestamp()),
398+
},
384399
});
400+
return expect(user._refreshIdToken()
401+
.then(_t1 => {
402+
const t1 = _cloneDeep(_t1);
403+
return user.getIdTokenResult().then(t2 =>
404+
_isEqual(t1, t2)
405+
);
406+
})
407+
).to.eventually.equal(true);
408+
});
385409

386-
it('should use current time as issuance time', () => {
387-
const user = new User(auth, {
388-
_tokenValidity: {
389-
authTime: new Date(randomPastTimestamp()),
390-
}
391-
});
392-
return expect(user.getIdTokenResult(true).then(r => r.issuedAtTime))
393-
.to.eventually.equal(now.toISOString());
410+
it('should use the previous token\'s authTime by default', () => {
411+
const authTime = new Date(randomPastTimestamp());
412+
const user = new User(auth, {
413+
_tokenValidity: {
414+
authTime: authTime,
415+
},
394416
});
417+
return expect(user._refreshIdToken().then(r => r.authTime))
418+
.to.eventually.equal(authTime.toISOString());
419+
});
395420

396-
it('should expire one hour after issuance', () => {
397-
const user = new User(auth, {
398-
_tokenValidity: {
399-
authTime: new Date(randomPastTimestamp()),
400-
},
401-
});
402-
const expTime = new Date(now.getTime() + 3600000);
403-
return expect(user.getIdTokenResult(true).then(r => r.expirationTime))
404-
.to.eventually.equal(expTime.toISOString());
421+
it('should use current time as new issuance time by default', () => {
422+
const authTime = new Date(randomPastTimestamp());
423+
const user = new User(auth, {
424+
_tokenValidity: {
425+
authTime: authTime,
426+
},
405427
});
428+
return expect(user._refreshIdToken().then(r => r.issuedAtTime))
429+
.to.eventually.equal(new Date(now).toISOString());
430+
});
406431

407-
it('should generate a new token', () => {
408-
const user = new User(auth, {_tokenValidity: {},});
409-
return expect(user.getIdToken(false)
410-
.then(oldToken => user.getIdTokenResult(true)
411-
.then(newTokenResult => oldToken === newTokenResult.token)
412-
)
413-
).to.eventually.equal(false);
432+
it('should expire one hour after issuance by default', () => {
433+
const authTime = new Date(randomPastTimestamp());
434+
const user = new User(auth, {
435+
_tokenValidity: {
436+
authTime: authTime,
437+
},
414438
});
439+
return expect(user._refreshIdToken().then(r => r.expirationTime))
440+
.to.eventually.equal(new Date(now + 3600000).toISOString());
441+
});
442+
443+
444+
it('should update the upstream user if there is one', () => {
445+
const uid = 123;
446+
return auth.createUser({
447+
uid: uid,
448+
449+
}).then(user =>
450+
expect(user._refreshIdToken()
451+
.then(() => auth.getUser(uid))
452+
).to.eventually.deep.equal(user)
453+
);
454+
});
455+
456+
it('should accept missing upstream users', () => {
457+
const user = new User(auth, {_tokenValidity: {},});
458+
return expect(user._refreshIdToken()).not.to.be.rejected;
415459
});
416460
});
417461
});

0 commit comments

Comments
 (0)