Skip to content

Commit 1504aa1

Browse files
authored
Implement cross-window browser events for auth-next (#3631)
* Implement cross-window browser events for auth-next * Split out local & session storage * Add persistence events to indexed DB * Add some tests & fix a couple issues with local storage events * Add tests for AuthImpl._onStorageEvent and fix getIdToken to trigger correctly * Merge conflicts * PR feedback
1 parent c1e9229 commit 1504aa1

36 files changed

+1400
-482
lines changed

packages-exp/auth-exp/index.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { Auth } from '@firebase/auth-types-exp';
2121
import { initializeAuth } from './src';
2222
import { registerAuth } from './src/core/auth/register';
2323
import { ClientPlatform } from './src/core/util/version';
24-
import { browserLocalPersistence } from './src/platform_browser/persistence/browser';
24+
import { browserLocalPersistence } from './src/platform_browser/persistence/local_storage';
2525
import { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db';
2626
import { browserPopupRedirectResolver } from './src/platform_browser/popup_redirect';
2727

@@ -31,10 +31,8 @@ export * from './src';
3131
// Additional DOM dependend functionality
3232

3333
// persistence
34-
export {
35-
browserLocalPersistence,
36-
browserSessionPersistence
37-
} from './src/platform_browser/persistence/browser';
34+
export { browserLocalPersistence } from './src/platform_browser/persistence/local_storage';
35+
export { browserSessionPersistence } from './src/platform_browser/persistence/session_storage';
3836
export { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db';
3937

4038
// providers

packages-exp/auth-exp/index.webworker.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import { indexedDBLocalPersistence } from './src/platform_browser/persistence/in
2929
// Core functionality shared by all clients
3030
export * from './src';
3131

32+
// persistence
33+
export { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db';
34+
3235
registerAuth(ClientPlatform.WORKER);
3336

3437
export function getAuth(app = getApp()): Auth {
@@ -43,7 +46,7 @@ export function getAuth(app = getApp()): Auth {
4346
// background, meanwhile the auth object may be used by the app.
4447
// eslint-disable-next-line @typescript-eslint/no-floating-promises
4548
_getInstance<Persistence>(indexedDBLocalPersistence)
46-
.isAvailable()
49+
._isAvailable()
4750
.then(avail => {
4851
const deps = avail ? { persistence: indexedDBLocalPersistence } : {};
4952
_initializeAuthInstance(auth, deps);

packages-exp/auth-exp/src/core/auth/auth_impl.test.ts

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ describe('core/auth/auth_impl', () => {
8383
return testUser(auth, `${n}`);
8484
});
8585

86-
persistenceStub.set.callsFake(() => {
86+
persistenceStub._set.callsFake(() => {
8787
return new Promise(resolve => {
8888
// Force into the async flow to make this test actually meaningful
8989
setTimeout(() => resolve(), 1);
@@ -92,7 +92,7 @@ describe('core/auth/auth_impl', () => {
9292

9393
await Promise.all(users.map(u => auth.updateCurrentUser(u)));
9494
for (let i = 0; i < 10; i++) {
95-
expect(persistenceStub.set.getCall(i)).to.have.been.calledWith(
95+
expect(persistenceStub._set.getCall(i)).to.have.been.calledWith(
9696
sinon.match.any,
9797
users[i].toJSON()
9898
);
@@ -101,7 +101,7 @@ describe('core/auth/auth_impl', () => {
101101

102102
it('setting to null triggers a remove call', async () => {
103103
await auth.updateCurrentUser(null);
104-
expect(persistenceStub.remove).to.have.been.called;
104+
expect(persistenceStub._remove).to.have.been.called;
105105
});
106106

107107
it('should throw an error if the user is from a different tenant', async () => {
@@ -118,7 +118,7 @@ describe('core/auth/auth_impl', () => {
118118
it('sets currentUser to null, calls remove', async () => {
119119
await auth.updateCurrentUser(testUser(auth, 'test'));
120120
await auth.signOut();
121-
expect(persistenceStub.remove).to.have.been.called;
121+
expect(persistenceStub._remove).to.have.been.called;
122122
expect(auth.currentUser).to.be.null;
123123
});
124124
});
@@ -265,4 +265,126 @@ describe('core/auth/auth_impl', () => {
265265
});
266266
});
267267
});
268+
269+
describe('#_onStorageEvent', () => {
270+
let authStateCallback: sinon.SinonSpy;
271+
let idTokenCallback: sinon.SinonSpy;
272+
273+
beforeEach(async () => {
274+
authStateCallback = sinon.spy();
275+
idTokenCallback = sinon.spy();
276+
auth._onAuthStateChanged(authStateCallback);
277+
auth._onIdTokenChanged(idTokenCallback);
278+
await auth.updateCurrentUser(null); // force event handlers to clear out
279+
authStateCallback.resetHistory();
280+
idTokenCallback.resetHistory();
281+
});
282+
283+
context('previously logged out', () => {
284+
context('still logged out', () => {
285+
it('should do nothing', async () => {
286+
await auth._onStorageEvent();
287+
288+
expect(authStateCallback).not.to.have.been.called;
289+
expect(idTokenCallback).not.to.have.been.called;
290+
});
291+
});
292+
293+
context('now logged in', () => {
294+
let user: User;
295+
296+
beforeEach(() => {
297+
user = testUser(auth, 'uid');
298+
persistenceStub._get.returns(Promise.resolve(user.toJSON()));
299+
});
300+
301+
it('should update the current user', async () => {
302+
await auth._onStorageEvent();
303+
304+
expect(auth.currentUser?.toJSON()).to.eql(user.toJSON());
305+
expect(authStateCallback).to.have.been.called;
306+
expect(idTokenCallback).to.have.been.called;
307+
});
308+
});
309+
});
310+
311+
context('previously logged in', () => {
312+
let user: User;
313+
314+
beforeEach(async () => {
315+
user = testUser(auth, 'uid', undefined, true);
316+
await auth.updateCurrentUser(user);
317+
authStateCallback.resetHistory();
318+
idTokenCallback.resetHistory();
319+
});
320+
321+
context('now logged out', () => {
322+
beforeEach(() => {
323+
persistenceStub._get.returns(Promise.resolve(null));
324+
});
325+
326+
it('should log out', async () => {
327+
await auth._onStorageEvent();
328+
329+
expect(auth.currentUser).to.be.null;
330+
expect(authStateCallback).to.have.been.called;
331+
expect(idTokenCallback).to.have.been.called;
332+
});
333+
});
334+
335+
context('still logged in as same user', () => {
336+
it('should do nothing if nothing changed', async () => {
337+
persistenceStub._get.returns(Promise.resolve(user.toJSON()));
338+
339+
await auth._onStorageEvent();
340+
341+
expect(auth.currentUser?.toJSON()).to.eql(user.toJSON());
342+
expect(authStateCallback).not.to.have.been.called;
343+
expect(idTokenCallback).not.to.have.been.called;
344+
});
345+
346+
it('should update fields if they have changed', async () => {
347+
const userObj = user.toJSON();
348+
userObj['displayName'] = 'other-name';
349+
persistenceStub._get.returns(Promise.resolve(userObj));
350+
351+
await auth._onStorageEvent();
352+
353+
expect(auth.currentUser?.uid).to.eq(user.uid);
354+
expect(auth.currentUser?.displayName).to.eq('other-name');
355+
expect(authStateCallback).not.to.have.been.called;
356+
expect(idTokenCallback).not.to.have.been.called;
357+
});
358+
359+
it('should update tokens if they have changed', async () => {
360+
const userObj = user.toJSON();
361+
(userObj['stsTokenManager'] as any)['accessToken'] =
362+
'new-access-token';
363+
persistenceStub._get.returns(Promise.resolve(userObj));
364+
365+
await auth._onStorageEvent();
366+
367+
expect(auth.currentUser?.uid).to.eq(user.uid);
368+
expect(auth.currentUser?.stsTokenManager.accessToken).to.eq(
369+
'new-access-token'
370+
);
371+
expect(authStateCallback).not.to.have.been.called;
372+
expect(idTokenCallback).to.have.been.called;
373+
});
374+
});
375+
376+
context('now logged in as different user', () => {
377+
it('should re-login as the new user', async () => {
378+
const newUser = testUser(auth, 'other-uid', undefined, true);
379+
persistenceStub._get.returns(Promise.resolve(newUser.toJSON()));
380+
381+
await auth._onStorageEvent();
382+
383+
expect(auth.currentUser?.toJSON()).to.eql(newUser.toJSON());
384+
expect(authStateCallback).to.have.been.called;
385+
expect(idTokenCallback).to.have.been.called;
386+
});
387+
});
388+
});
389+
});
268390
});

packages-exp/auth-exp/src/core/auth/auth_impl.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,33 @@ export class AuthImplCompat<T extends User> implements Auth, _FirebaseService {
107107
return this._initializationPromise;
108108
}
109109

110+
/**
111+
* If the persistence is changed in another window, the user manager will let us know
112+
*/
113+
async _onStorageEvent(): Promise<void> {
114+
const user = await this.assertedPersistence.getCurrentUser();
115+
116+
if (!this.currentUser && !user) {
117+
// No change, do nothing (was signed out and remained signed out).
118+
return;
119+
}
120+
121+
// If the same user is to be synchronized.
122+
if (this.currentUser && user && this.currentUser.uid === user.uid) {
123+
// Data update, simply copy data changes.
124+
this.currentUser._copy(user);
125+
// If tokens changed from previous user tokens, this will trigger
126+
// notifyAuthListeners_.
127+
await this.currentUser.getIdToken();
128+
return;
129+
}
130+
131+
// Update current Auth state. Either a new login or logout.
132+
await this.updateCurrentUser(user);
133+
// Notify external Auth changes of Auth change event.
134+
this.notifyAuthListeners();
135+
}
136+
110137
_createUser(params: UserParameters): T {
111138
return new this._userProvider(params);
112139
}

packages-exp/auth-exp/src/core/auth/firebase_internal.test.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,11 @@ describe('src/core/auth/firebase_internal', () => {
7070
beforeEach(async () => {
7171
user = testUser(auth, 'uid', undefined, true);
7272
await auth.updateCurrentUser(user);
73-
sinon.stub(user.stsTokenManager, 'getToken').returns(
74-
Promise.resolve({
75-
accessToken: 'access-token',
76-
refreshToken: 'refresh-tken',
77-
wasRefreshed: true
78-
})
79-
);
73+
let i = 0;
74+
sinon.stub(user.stsTokenManager, 'getToken').callsFake(async () => {
75+
i += 1;
76+
return `new-access-token-${i}`;
77+
});
8078
sinon
8179
.stub(user, '_startProactiveRefresh')
8280
.callsFake(() => (isProactiveRefresh = true));

packages-exp/auth-exp/src/core/persistence/in_memory.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,28 @@ describe('core/persistence/in_memory', () => {
2828
it('should work with persistence type', async () => {
2929
const key = 'my-super-special-persistence-type';
3030
const value = PersistenceType.LOCAL;
31-
expect(await persistence.get(key)).to.be.null;
32-
await persistence.set(key, value);
33-
expect(await persistence.get(key)).to.be.eq(value);
34-
expect(await persistence.get('other-key')).to.be.null;
35-
await persistence.remove(key);
36-
expect(await persistence.get(key)).to.be.null;
31+
expect(await persistence._get(key)).to.be.null;
32+
await persistence._set(key, value);
33+
expect(await persistence._get(key)).to.be.eq(value);
34+
expect(await persistence._get('other-key')).to.be.null;
35+
await persistence._remove(key);
36+
expect(await persistence._get(key)).to.be.null;
3737
});
3838

3939
it('should work with user', async () => {
4040
const key = 'my-super-special-user';
4141
const auth = await testAuth();
4242
const value = testUser(auth, 'uid');
4343

44-
expect(await persistence.get(key)).to.be.null;
45-
await persistence.set(key, value.toJSON());
46-
expect(await persistence.get(key)).to.eql(value.toJSON());
47-
expect(await persistence.get('other-key')).to.be.null;
48-
await persistence.remove(key);
49-
expect(await persistence.get(key)).to.be.null;
44+
expect(await persistence._get(key)).to.be.null;
45+
await persistence._set(key, value.toJSON());
46+
expect(await persistence._get(key)).to.eql(value.toJSON());
47+
expect(await persistence._get('other-key')).to.be.null;
48+
await persistence._remove(key);
49+
expect(await persistence._get(key)).to.be.null;
5050
});
5151

5252
it('isAvailable returns true', async () => {
53-
expect(await persistence.isAvailable()).to.be.true;
53+
expect(await persistence._isAvailable()).to.be.true;
5454
});
5555
});

packages-exp/auth-exp/src/core/persistence/in_memory.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,44 @@
1717

1818
import * as externs from '@firebase/auth-types-exp';
1919

20-
import { Persistence, PersistenceType, PersistenceValue } from '../persistence';
20+
import {
21+
Persistence,
22+
PersistenceType,
23+
PersistenceValue,
24+
StorageEventListener
25+
} from '../persistence';
2126

2227
export class InMemoryPersistence implements Persistence {
2328
static type: 'NONE' = 'NONE';
2429
readonly type = PersistenceType.NONE;
2530
storage: Record<string, PersistenceValue> = {};
2631

27-
async isAvailable(): Promise<boolean> {
32+
async _isAvailable(): Promise<boolean> {
2833
return true;
2934
}
3035

31-
async set(key: string, value: PersistenceValue): Promise<void> {
36+
async _set(key: string, value: PersistenceValue): Promise<void> {
3237
this.storage[key] = value;
3338
}
3439

35-
async get<T extends PersistenceValue>(key: string): Promise<T | null> {
40+
async _get<T extends PersistenceValue>(key: string): Promise<T | null> {
3641
const value = this.storage[key];
3742
return value === undefined ? null : (value as T);
3843
}
3944

40-
async remove(key: string): Promise<void> {
45+
async _remove(key: string): Promise<void> {
4146
delete this.storage[key];
4247
}
48+
49+
_addListener(_key: string, _listener: StorageEventListener): void {
50+
// Listeners are not supported for in-memory storage since it cannot be shared across windows/workers
51+
return;
52+
}
53+
54+
_removeListener(_key: string, _listener: StorageEventListener): void {
55+
// Listeners are not supported for in-memory storage since it cannot be shared across windows/workers
56+
return;
57+
}
4358
}
4459

4560
export const inMemoryPersistence: externs.Persistence = InMemoryPersistence;

packages-exp/auth-exp/src/core/persistence/index.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,16 @@ export type PersistenceValue = PersistedBlob | string;
3131

3232
export const STORAGE_AVAILABLE_KEY = '__sak';
3333

34+
export interface StorageEventListener {
35+
(value: PersistenceValue | null): void;
36+
}
37+
3438
export interface Persistence {
3539
type: PersistenceType;
36-
isAvailable(): Promise<boolean>;
37-
set(key: string, value: PersistenceValue): Promise<void>;
38-
get<T extends PersistenceValue>(key: string): Promise<T | null>;
39-
remove(key: string): Promise<void>;
40+
_isAvailable(): Promise<boolean>;
41+
_set(key: string, value: PersistenceValue): Promise<void>;
42+
_get<T extends PersistenceValue>(key: string): Promise<T | null>;
43+
_remove(key: string): Promise<void>;
44+
_addListener(key: string, listener: StorageEventListener): void;
45+
_removeListener(key: string, listener: StorageEventListener): void;
4046
}

0 commit comments

Comments
 (0)