Skip to content

Implement cross-window browser events for auth-next #3631

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 25, 2020
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
8 changes: 3 additions & 5 deletions packages-exp/auth-exp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { Auth } from '@firebase/auth-types-exp';
import { initializeAuth } from './src';
import { registerAuth } from './src/core/auth/register';
import { ClientPlatform } from './src/core/util/version';
import { browserLocalPersistence } from './src/platform_browser/persistence/browser';
import { browserLocalPersistence } from './src/platform_browser/persistence/local_storage';
import { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db';
import { browserPopupRedirectResolver } from './src/platform_browser/popup_redirect';

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

// persistence
export {
browserLocalPersistence,
browserSessionPersistence
} from './src/platform_browser/persistence/browser';
export { browserLocalPersistence } from './src/platform_browser/persistence/local_storage';
export { browserSessionPersistence } from './src/platform_browser/persistence/session_storage';
export { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db';

// providers
Expand Down
5 changes: 4 additions & 1 deletion packages-exp/auth-exp/index.webworker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import { indexedDBLocalPersistence } from './src/platform_browser/persistence/in
// Core functionality shared by all clients
export * from './src';

// persistence
export { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db';

registerAuth(ClientPlatform.WORKER);

export function getAuth(app = getApp()): Auth {
Expand All @@ -43,7 +46,7 @@ export function getAuth(app = getApp()): Auth {
// background, meanwhile the auth object may be used by the app.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
_getInstance<Persistence>(indexedDBLocalPersistence)
.isAvailable()
._isAvailable()
.then(avail => {
const deps = avail ? { persistence: indexedDBLocalPersistence } : {};
_initializeAuthInstance(auth, deps);
Expand Down
130 changes: 126 additions & 4 deletions packages-exp/auth-exp/src/core/auth/auth_impl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('core/auth/auth_impl', () => {
return testUser(auth, `${n}`);
});

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

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

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

it('should throw an error if the user is from a different tenant', async () => {
Expand All @@ -118,7 +118,7 @@ describe('core/auth/auth_impl', () => {
it('sets currentUser to null, calls remove', async () => {
await auth.updateCurrentUser(testUser(auth, 'test'));
await auth.signOut();
expect(persistenceStub.remove).to.have.been.called;
expect(persistenceStub._remove).to.have.been.called;
expect(auth.currentUser).to.be.null;
});
});
Expand Down Expand Up @@ -265,4 +265,126 @@ describe('core/auth/auth_impl', () => {
});
});
});

describe('#_onStorageEvent', () => {
let authStateCallback: sinon.SinonSpy;
let idTokenCallback: sinon.SinonSpy;

beforeEach(async () => {
authStateCallback = sinon.spy();
idTokenCallback = sinon.spy();
auth._onAuthStateChanged(authStateCallback);
auth._onIdTokenChanged(idTokenCallback);
await auth.updateCurrentUser(null); // force event handlers to clear out
authStateCallback.resetHistory();
idTokenCallback.resetHistory();
});

context('previously logged out', () => {
context('still logged out', () => {
it('should do nothing', async () => {
await auth._onStorageEvent();

expect(authStateCallback).not.to.have.been.called;
expect(idTokenCallback).not.to.have.been.called;
});
});

context('now logged in', () => {
let user: User;

beforeEach(() => {
user = testUser(auth, 'uid');
persistenceStub._get.returns(Promise.resolve(user.toJSON()));
});

it('should update the current user', async () => {
await auth._onStorageEvent();

expect(auth.currentUser?.toJSON()).to.eql(user.toJSON());
expect(authStateCallback).to.have.been.called;
expect(idTokenCallback).to.have.been.called;
});
});
});

context('previously logged in', () => {
let user: User;

beforeEach(async () => {
user = testUser(auth, 'uid', undefined, true);
await auth.updateCurrentUser(user);
authStateCallback.resetHistory();
idTokenCallback.resetHistory();
});

context('now logged out', () => {
beforeEach(() => {
persistenceStub._get.returns(Promise.resolve(null));
});

it('should log out', async () => {
await auth._onStorageEvent();

expect(auth.currentUser).to.be.null;
expect(authStateCallback).to.have.been.called;
expect(idTokenCallback).to.have.been.called;
});
});

context('still logged in as same user', () => {
it('should do nothing if nothing changed', async () => {
persistenceStub._get.returns(Promise.resolve(user.toJSON()));

await auth._onStorageEvent();

expect(auth.currentUser?.toJSON()).to.eql(user.toJSON());
expect(authStateCallback).not.to.have.been.called;
expect(idTokenCallback).not.to.have.been.called;
});

it('should update fields if they have changed', async () => {
const userObj = user.toJSON();
userObj['displayName'] = 'other-name';
persistenceStub._get.returns(Promise.resolve(userObj));

await auth._onStorageEvent();

expect(auth.currentUser?.uid).to.eq(user.uid);
expect(auth.currentUser?.displayName).to.eq('other-name');
expect(authStateCallback).not.to.have.been.called;
expect(idTokenCallback).not.to.have.been.called;
});

it('should update tokens if they have changed', async () => {
const userObj = user.toJSON();
(userObj['stsTokenManager'] as any)['accessToken'] =
'new-access-token';
persistenceStub._get.returns(Promise.resolve(userObj));

await auth._onStorageEvent();

expect(auth.currentUser?.uid).to.eq(user.uid);
expect(auth.currentUser?.stsTokenManager.accessToken).to.eq(
'new-access-token'
);
expect(authStateCallback).not.to.have.been.called;
expect(idTokenCallback).to.have.been.called;
});
});

context('now logged in as different user', () => {
it('should re-login as the new user', async () => {
const newUser = testUser(auth, 'other-uid', undefined, true);
persistenceStub._get.returns(Promise.resolve(newUser.toJSON()));

await auth._onStorageEvent();

expect(auth.currentUser?.toJSON()).to.eql(newUser.toJSON());
expect(authStateCallback).to.have.been.called;
expect(idTokenCallback).to.have.been.called;
});
});
});
});
});
27 changes: 27 additions & 0 deletions packages-exp/auth-exp/src/core/auth/auth_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,33 @@ export class AuthImplCompat<T extends User> implements Auth, _FirebaseService {
return this._initializationPromise;
}

/**
* If the persistence is changed in another window, the user manager will let us know
*/
async _onStorageEvent(): Promise<void> {
const user = await this.assertedPersistence.getCurrentUser();

if (!this.currentUser && !user) {
// No change, do nothing (was signed out and remained signed out).
return;
}

// If the same user is to be synchronized.
if (this.currentUser && user && this.currentUser.uid === user.uid) {
// Data update, simply copy data changes.
this.currentUser._copy(user);
// If tokens changed from previous user tokens, this will trigger
// notifyAuthListeners_.
await this.currentUser.getIdToken();
return;
}

// Update current Auth state. Either a new login or logout.
await this.updateCurrentUser(user);
// Notify external Auth changes of Auth change event.
this.notifyAuthListeners();
}

_createUser(params: UserParameters): T {
return new this._userProvider(params);
}
Expand Down
12 changes: 5 additions & 7 deletions packages-exp/auth-exp/src/core/auth/firebase_internal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,11 @@ describe('src/core/auth/firebase_internal', () => {
beforeEach(async () => {
user = testUser(auth, 'uid', undefined, true);
await auth.updateCurrentUser(user);
sinon.stub(user.stsTokenManager, 'getToken').returns(
Promise.resolve({
accessToken: 'access-token',
refreshToken: 'refresh-tken',
wasRefreshed: true
})
);
let i = 0;
sinon.stub(user.stsTokenManager, 'getToken').callsFake(async () => {
i += 1;
return `new-access-token-${i}`;
});
sinon
.stub(user, '_startProactiveRefresh')
.callsFake(() => (isProactiveRefresh = true));
Expand Down
26 changes: 13 additions & 13 deletions packages-exp/auth-exp/src/core/persistence/in_memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,28 @@ describe('core/persistence/in_memory', () => {
it('should work with persistence type', async () => {
const key = 'my-super-special-persistence-type';
const value = PersistenceType.LOCAL;
expect(await persistence.get(key)).to.be.null;
await persistence.set(key, value);
expect(await persistence.get(key)).to.be.eq(value);
expect(await persistence.get('other-key')).to.be.null;
await persistence.remove(key);
expect(await persistence.get(key)).to.be.null;
expect(await persistence._get(key)).to.be.null;
await persistence._set(key, value);
expect(await persistence._get(key)).to.be.eq(value);
expect(await persistence._get('other-key')).to.be.null;
await persistence._remove(key);
expect(await persistence._get(key)).to.be.null;
});

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

expect(await persistence.get(key)).to.be.null;
await persistence.set(key, value.toJSON());
expect(await persistence.get(key)).to.eql(value.toJSON());
expect(await persistence.get('other-key')).to.be.null;
await persistence.remove(key);
expect(await persistence.get(key)).to.be.null;
expect(await persistence._get(key)).to.be.null;
await persistence._set(key, value.toJSON());
expect(await persistence._get(key)).to.eql(value.toJSON());
expect(await persistence._get('other-key')).to.be.null;
await persistence._remove(key);
expect(await persistence._get(key)).to.be.null;
});

it('isAvailable returns true', async () => {
expect(await persistence.isAvailable()).to.be.true;
expect(await persistence._isAvailable()).to.be.true;
});
});
25 changes: 20 additions & 5 deletions packages-exp/auth-exp/src/core/persistence/in_memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,44 @@

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

import { Persistence, PersistenceType, PersistenceValue } from '../persistence';
import {
Persistence,
PersistenceType,
PersistenceValue,
StorageEventListener
} from '../persistence';

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

async isAvailable(): Promise<boolean> {
async _isAvailable(): Promise<boolean> {
return true;
}

async set(key: string, value: PersistenceValue): Promise<void> {
async _set(key: string, value: PersistenceValue): Promise<void> {
this.storage[key] = value;
}

async get<T extends PersistenceValue>(key: string): Promise<T | null> {
async _get<T extends PersistenceValue>(key: string): Promise<T | null> {
const value = this.storage[key];
return value === undefined ? null : (value as T);
}

async remove(key: string): Promise<void> {
async _remove(key: string): Promise<void> {
delete this.storage[key];
}

_addListener(_key: string, _listener: StorageEventListener): void {
// Listeners are not supported for in-memory storage since it cannot be shared across windows/workers
return;
}

_removeListener(_key: string, _listener: StorageEventListener): void {
// Listeners are not supported for in-memory storage since it cannot be shared across windows/workers
return;
}
}

export const inMemoryPersistence: externs.Persistence = InMemoryPersistence;
14 changes: 10 additions & 4 deletions packages-exp/auth-exp/src/core/persistence/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@ export type PersistenceValue = PersistedBlob | string;

export const STORAGE_AVAILABLE_KEY = '__sak';

export interface StorageEventListener {
(value: PersistenceValue | null): void;
}

export interface Persistence {
type: PersistenceType;
isAvailable(): Promise<boolean>;
set(key: string, value: PersistenceValue): Promise<void>;
get<T extends PersistenceValue>(key: string): Promise<T | null>;
remove(key: string): Promise<void>;
_isAvailable(): Promise<boolean>;
_set(key: string, value: PersistenceValue): Promise<void>;
_get<T extends PersistenceValue>(key: string): Promise<T | null>;
_remove(key: string): Promise<void>;
_addListener(key: string, listener: StorageEventListener): void;
_removeListener(key: string, listener: StorageEventListener): void;
}
Loading