Skip to content

Commit d692c45

Browse files
committed
Set key in storage before redirect, only open iframe if that key is present
1 parent e558d3e commit d692c45

File tree

14 files changed

+126
-27
lines changed

14 files changed

+126
-27
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
132132
return;
133133
}
134134

135+
// Initialize the resolver early if necessary (only applicable to web:
136+
// this will cause the iframe to load immediately in certain cases)
137+
if (this._popupRedirectResolver?._shouldInitProactively) {
138+
await this._popupRedirectResolver._initialize(this);
139+
}
140+
135141
await this.initializeCurrentUser(popupRedirectResolver);
136142

137143
if (this._deleted) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ describe('core/auth/initialize', () => {
117117
cb(true);
118118
}
119119
async _originValidation(): Promise<void> {}
120+
_shouldInitProactively = false;
120121
async _completeRedirectFn(
121122
_auth: Auth,
122123
_resolver: PopupRedirectResolver,

packages-exp/auth-exp/src/core/strategies/redirect.test.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
ProviderId
2323
} from '../../model/public_types';
2424
import * as sinon from 'sinon';
25-
import { _getInstance } from '../util/instantiator';
25+
import * as sinonChai from 'sinon-chai';
26+
import { _clearInstanceMap, _getInstance } from '../util/instantiator';
2627
import {
2728
MockPersistenceLayer,
2829
TestAuth,
@@ -39,24 +40,24 @@ import {
3940
PopupRedirectResolverInternal
4041
} from '../../model/popup_redirect';
4142
import { BASE_AUTH_EVENT } from '../../../test/helpers/iframe_event';
42-
import { PersistenceInternal } from '../persistence';
43-
import { InMemoryPersistence } from '../persistence/in_memory';
4443
import { UserCredentialImpl } from '../user/user_credential_impl';
4544
import * as idpTasks from '../strategies/idp';
46-
import { expect } from 'chai';
45+
import { expect, use } from 'chai';
4746
import { AuthErrorCode } from '../errors';
47+
import { RedirectPersistence } from '../../../test/helpers/redirect_persistence';
48+
49+
use(sinonChai);
4850

4951
const MATCHING_EVENT_ID = 'matching-event-id';
5052
const OTHER_EVENT_ID = 'wrong-id';
5153

52-
class RedirectPersistence extends InMemoryPersistence {}
53-
5454
describe('core/strategies/redirect', () => {
5555
let auth: AuthInternal;
5656
let redirectAction: RedirectAction;
5757
let eventManager: AuthEventManager;
5858
let resolver: PopupRedirectResolver;
5959
let idpStubs: sinon.SinonStubbedInstance<typeof idpTasks>;
60+
let redirectPersistence: RedirectPersistence;
6061

6162
beforeEach(async () => {
6263
eventManager = new AuthEventManager(({} as unknown) as TestAuth);
@@ -67,11 +68,16 @@ describe('core/strategies/redirect', () => {
6768
)._redirectPersistence = RedirectPersistence;
6869
auth = await testAuth();
6970
redirectAction = new RedirectAction(auth, _getInstance(resolver), false);
71+
redirectPersistence = _getInstance(RedirectPersistence);
72+
73+
// Default to has redirect for most test
74+
redirectPersistence.hasPendingRedirect = true;
7075
});
7176

7277
afterEach(() => {
7378
sinon.restore();
7479
_clearRedirectOutcomes();
80+
_clearInstanceMap();
7581
});
7682

7783
function iframeEvent(event: Partial<AuthEvent>): void {
@@ -86,16 +92,11 @@ describe('core/strategies/redirect', () => {
8692
}
8793

8894
async function reInitAuthWithRedirectUser(eventId: string): Promise<void> {
89-
const redirectPersistence: PersistenceInternal = _getInstance(
90-
RedirectPersistence
91-
);
9295
const mainPersistence = new MockPersistenceLayer();
9396
const oldAuth = await testAuth();
9497
const user = testUser(oldAuth, 'uid');
9598
user._redirectEventId = eventId;
96-
sinon
97-
.stub(redirectPersistence, '_get')
98-
.returns(Promise.resolve(user.toJSON()));
99+
redirectPersistence.redirectUser = user.toJSON();
99100
sinon.stub(mainPersistence, '_get').returns(Promise.resolve(user.toJSON()));
100101

101102
auth = await testAuth(resolver, mainPersistence);
@@ -194,4 +195,16 @@ describe('core/strategies/redirect', () => {
194195
expect(await promise).to.eq(cred);
195196
expect(await redirectAction.execute()).to.eq(cred);
196197
});
198+
199+
it('bypasses initialization if no key set', async () => {
200+
await reInitAuthWithRedirectUser(MATCHING_EVENT_ID);
201+
const resolverInstance = _getInstance<PopupRedirectResolverInternal>(resolver);
202+
203+
sinon.spy(resolverInstance, '_initialize');
204+
redirectPersistence.hasPendingRedirect = false;
205+
206+
expect(await redirectAction.execute()).to.eq(null);
207+
expect(await redirectAction.execute()).to.eq(null);
208+
expect(resolverInstance._initialize).not.to.have.been.called;
209+
});
197210
});

packages-exp/auth-exp/src/core/strategies/redirect.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,13 @@ import {
2222
PopupRedirectResolverInternal
2323
} from '../../model/popup_redirect';
2424
import { UserCredentialInternal } from '../../model/user';
25+
import { PersistenceInternal } from '../persistence';
26+
import { _persistenceKeyName } from '../persistence/persistence_user_manager';
27+
import { _getInstance } from '../util/instantiator';
2528
import { AbstractPopupRedirectOperation } from './abstract_popup_redirect_operation';
2629

30+
const PENDING_REDIRECT_KEY = 'pendingRedirect';
31+
2732
// We only get one redirect outcome for any one auth, so just store it
2833
// in here.
2934
const redirectOutcomeMap: Map<
@@ -61,7 +66,8 @@ export class RedirectAction extends AbstractPopupRedirectOperation {
6166
let readyOutcome = redirectOutcomeMap.get(this.auth._key());
6267
if (!readyOutcome) {
6368
try {
64-
const result = await super.execute();
69+
const hasPendingRedirect = await _getAndClearPendingRedirectStatus(this.resolver, this.auth);
70+
const result = hasPendingRedirect ? await super.execute() : null;
6571
readyOutcome = () => Promise.resolve(result);
6672
} catch (e) {
6773
readyOutcome = () => Promise.reject(e);
@@ -98,6 +104,25 @@ export class RedirectAction extends AbstractPopupRedirectOperation {
98104
cleanUp(): void {}
99105
}
100106

107+
export async function _getAndClearPendingRedirectStatus(resolver: PopupRedirectResolverInternal, auth: AuthInternal): Promise<boolean> {
108+
const key = pendingRedirectKey(auth);
109+
const hasPendingRedirect = await resolverPersistence(resolver)._get(key) === 'true';
110+
await resolverPersistence(resolver)._remove(key);
111+
return hasPendingRedirect;
112+
}
113+
114+
export async function _setPendingRedirectStatus(resolver: PopupRedirectResolverInternal, auth: AuthInternal): Promise<void> {
115+
return resolverPersistence(resolver)._set(pendingRedirectKey(auth), 'true');
116+
}
117+
101118
export function _clearRedirectOutcomes(): void {
102119
redirectOutcomeMap.clear();
103120
}
121+
122+
function resolverPersistence(resolver: PopupRedirectResolverInternal): PersistenceInternal {
123+
return _getInstance(resolver._redirectPersistence);
124+
}
125+
126+
function pendingRedirectKey(auth: AuthInternal): string {
127+
return _persistenceKeyName(PENDING_REDIRECT_KEY, auth.config.apiKey, auth.name);
128+
}

packages-exp/auth-exp/src/core/util/browser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export function _isFirefox(ua = getUA()): boolean {
9292
return /firefox\//i.test(ua);
9393
}
9494

95-
export function _isSafari(userAgent: string): boolean {
95+
export function _isSafari(userAgent = getUA()): boolean {
9696
const ua = userAgent.toLowerCase();
9797
return (
9898
ua.includes('safari/') &&

packages-exp/auth-exp/src/core/util/instantiator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,7 @@ export function _getInstance<T>(cls: unknown): T {
4646
instanceCache.set(cls, instance);
4747
return instance;
4848
}
49+
50+
export function _clearInstanceMap(): void {
51+
instanceCache.clear();
52+
}

packages-exp/auth-exp/src/model/popup_redirect.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ export interface EventManager {
8080
}
8181

8282
export interface PopupRedirectResolverInternal extends PopupRedirectResolver {
83+
// Whether or not to initialize the event manager early
84+
_shouldInitProactively: boolean;
85+
8386
_initialize(auth: AuthInternal): Promise<EventManager>;
8487
_openPopup(
8588
auth: AuthInternal,

packages-exp/auth-exp/src/platform_browser/auth.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { PopupRedirectResolverInternal } from '../model/popup_redirect';
4646
import { UserCredentialImpl } from '../core/user/user_credential_impl';
4747
import { UserInternal } from '../model/user';
4848
import { _createError } from '../core/util/assert';
49+
import { makeMockPopupRedirectResolver } from '../../test/helpers/mock_popup_redirect_resolver';
4950

5051
use(sinonChai);
5152
use(chaiAsPromised);
@@ -191,6 +192,24 @@ describe('core/auth/initializeAuth', () => {
191192
expect(reload._reloadWithoutSaving).not.to.have.been.called;
192193
});
193194

195+
it('does not early-initialize the resolver', async () => {
196+
const popupRedirectResolver = makeMockPopupRedirectResolver();
197+
const resolverInternal: PopupRedirectResolverInternal = _getInstance(popupRedirectResolver);
198+
sinon.stub(resolverInternal, '_shouldInitProactively').value(false);
199+
sinon.spy(resolverInternal, '_initialize');
200+
await initAndWait(inMemoryPersistence, popupRedirectResolver);
201+
expect(resolverInternal._initialize).not.to.have.been.called;
202+
});
203+
204+
it('does early-initialize the resolver', async () => {
205+
const popupRedirectResolver = makeMockPopupRedirectResolver();
206+
const resolverInternal: PopupRedirectResolverInternal = _getInstance(popupRedirectResolver);
207+
sinon.stub(resolverInternal, '_shouldInitProactively').value(true);
208+
sinon.spy(resolverInternal, '_initialize');
209+
await initAndWait(inMemoryPersistence, popupRedirectResolver);
210+
expect(resolverInternal._initialize).to.have.been.called;
211+
});
212+
194213
it('reloads non-redirect users', async () => {
195214
sinon
196215
.stub(_getInstance<PersistenceInternal>(inMemoryPersistence), '_get')

packages-exp/auth-exp/src/platform_browser/popup_redirect.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { browserSessionPersistence } from './persistence/session_storage';
3737
import { _open, AuthPopup } from './util/popup';
3838
import { _getRedirectResult } from './strategies/redirect';
3939
import { _getRedirectUrl } from '../core/util/handler';
40+
import { _isIOS, _isMobileBrowser, _isSafari } from '../core/util/browser';
4041

4142
/**
4243
* The special web storage event
@@ -162,6 +163,11 @@ class BrowserPopupRedirectResolver implements PopupRedirectResolverInternal {
162163
return this.originValidationPromises[key];
163164
}
164165

166+
get _shouldInitProactively(): boolean {
167+
// Mobile browsers and Safari need to optimistically initialize
168+
return _isMobileBrowser() || _isSafari() || _isIOS();
169+
}
170+
165171
_completeRedirectFn = _getRedirectResult;
166172
}
167173

packages-exp/auth-exp/src/platform_browser/strategies/redirect.test.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,10 @@ import { UserInternal } from '../../model/user';
4545
import { AuthEventManager } from '../../core/auth/auth_event_manager';
4646
import { AuthErrorCode } from '../../core/errors';
4747
import { PersistenceInternal } from '../../core/persistence';
48-
import { InMemoryPersistence } from '../../core/persistence/in_memory';
4948
import { OAuthProvider } from '../../core/providers/oauth';
5049
import * as link from '../../core/user/link_unlink';
5150
import { UserCredentialImpl } from '../../core/user/user_credential_impl';
52-
import { _getInstance } from '../../core/util/instantiator';
51+
import { _clearInstanceMap, _getInstance } from '../../core/util/instantiator';
5352
import * as idpTasks from '../../core/strategies/idp';
5453
import {
5554
getRedirectResult,
@@ -60,15 +59,14 @@ import {
6059
} from './redirect';
6160
import { FirebaseError } from '@firebase/util';
6261
import { _clearRedirectOutcomes } from '../../core/strategies/redirect';
62+
import { RedirectPersistence } from '../../../test/helpers/redirect_persistence';
6363

6464
use(sinonChai);
6565
use(chaiAsPromised);
6666

6767
const MATCHING_EVENT_ID = 'matching-event-id';
6868
const OTHER_EVENT_ID = 'wrong-id';
6969

70-
class RedirectPersistence extends InMemoryPersistence {}
71-
7270
describe('platform_browser/strategies/redirect', () => {
7371
let auth: TestAuth;
7472
let eventManager: AuthEventManager;
@@ -85,11 +83,13 @@ describe('platform_browser/strategies/redirect', () => {
8583
)._redirectPersistence = RedirectPersistence;
8684
auth = await testAuth(resolver);
8785
idpStubs = sinon.stub(idpTasks);
86+
_getInstance<RedirectPersistence>(RedirectPersistence).hasPendingRedirect = true;
8887
});
8988

9089
afterEach(() => {
9190
sinon.restore();
9291
_clearRedirectOutcomes();
92+
_clearInstanceMap();
9393
});
9494

9595
context('signInWithRedirect', () => {
@@ -299,16 +299,14 @@ describe('platform_browser/strategies/redirect', () => {
299299
}
300300

301301
async function reInitAuthWithRedirectUser(eventId: string): Promise<void> {
302-
const redirectPersistence: PersistenceInternal = _getInstance(
302+
const redirectPersistence: RedirectPersistence = _getInstance(
303303
RedirectPersistence
304304
);
305305
const mainPersistence = new MockPersistenceLayer();
306306
const oldAuth = await testAuth();
307307
const user = testUser(oldAuth, 'uid');
308308
user._redirectEventId = eventId;
309-
sinon
310-
.stub(redirectPersistence, '_get')
311-
.returns(Promise.resolve(user.toJSON()));
309+
redirectPersistence.redirectUser = user.toJSON();
312310
sinon
313311
.stub(mainPersistence, '_get')
314312
.returns(Promise.resolve(user.toJSON()));
@@ -427,7 +425,7 @@ describe('platform_browser/strategies/redirect', () => {
427425
type: AuthEventType.LINK_VIA_REDIRECT
428426
});
429427
expect(await promise).to.eq(cred);
430-
expect(redirectPersistence._remove).to.have.been.called;
428+
expect(redirectPersistence._remove).to.have.been.calledWith('firebase:redirectUser:test-api-key:test-app');
431429
expect(auth._currentUser?._redirectEventId).to.be.undefined;
432430
expect(auth.persistenceLayer.lastObjectSet?._redirectEventId).to.be
433431
.undefined;
@@ -451,7 +449,7 @@ describe('platform_browser/strategies/redirect', () => {
451449
type: AuthEventType.LINK_VIA_REDIRECT
452450
});
453451
expect(await promise).to.eq(cred);
454-
expect(redirectPersistence._remove).not.to.have.been.called;
452+
expect(redirectPersistence._remove).not.to.have.been.calledWith('firebase:redirectUser:test-api-key:test-app');
455453
expect(auth._currentUser?._redirectEventId).not.to.be.undefined;
456454
expect(auth.persistenceLayer.lastObjectSet?._redirectEventId).not.to.be
457455
.undefined;

packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { _generateEventId } from '../../core/util/event_id';
3232
import { AuthEventType } from '../../model/popup_redirect';
3333
import { UserInternal } from '../../model/user';
3434
import { _withDefaultResolver } from '../../core/util/resolver';
35-
import { RedirectAction } from '../../core/strategies/redirect';
35+
import { RedirectAction, _setPendingRedirectStatus } from '../../core/strategies/redirect';
3636

3737
/**
3838
* Authenticates a Firebase client using a full-page redirect flow.
@@ -93,7 +93,10 @@ export async function _signInWithRedirect(
9393
AuthErrorCode.ARGUMENT_ERROR
9494
);
9595

96-
return _withDefaultResolver(authInternal, resolver)._openRedirect(
96+
const resolverInternal = _withDefaultResolver(authInternal, resolver);
97+
await _setPendingRedirectStatus(resolverInternal, authInternal);
98+
99+
return resolverInternal._openRedirect(
97100
authInternal,
98101
provider,
99102
AuthEventType.SIGN_IN_VIA_REDIRECT
@@ -153,6 +156,7 @@ export async function _reauthenticateWithRedirect(
153156

154157
// Allow the resolver to error before persisting the redirect user
155158
const resolverInternal = _withDefaultResolver(userInternal.auth, resolver);
159+
await _setPendingRedirectStatus(resolverInternal, userInternal.auth);
156160

157161
const eventId = await prepareUserForRedirect(userInternal);
158162
return resolverInternal._openRedirect(
@@ -209,8 +213,9 @@ export async function _linkWithRedirect(
209213

210214
// Allow the resolver to error before persisting the redirect user
211215
const resolverInternal = _withDefaultResolver(userInternal.auth, resolver);
212-
213216
await _assertLinkedStatus(false, userInternal, provider.providerId);
217+
await _setPendingRedirectStatus(resolverInternal, userInternal.auth);
218+
214219
const eventId = await prepareUserForRedirect(userInternal);
215220
return resolverInternal._openRedirect(
216221
userInternal.auth,

packages-exp/auth-exp/src/platform_cordova/popup_redirect/popup_redirect.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const INITIAL_EVENT_TIMEOUT_MS = 500;
5252

5353
class CordovaPopupRedirectResolver implements PopupRedirectResolverInternal {
5454
readonly _redirectPersistence = browserSessionPersistence;
55+
readonly _shouldInitProactively = true; // This is lightweight for Cordova
5556
private readonly eventManagers = new Map<string, CordovaAuthEventManager>();
5657

5758
_completeRedirectFn = _getRedirectResult;

packages-exp/auth-exp/test/helpers/mock_popup_redirect_resolver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export function makeMockPopupRedirectResolver(
5656

5757
_redirectPersistence?: Persistence;
5858

59+
_shouldInitProactively = false;
60+
5961
async _completeRedirectFn(): Promise<void> {}
6062

6163
async _originValidation(): Promise<void> {}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { PersistenceValue } from '../../src/core/persistence';
2+
import { InMemoryPersistence } from '../../src/core/persistence/in_memory';
3+
4+
/** Helper class for handling redirect persistence */
5+
export class RedirectPersistence extends InMemoryPersistence {
6+
hasPendingRedirect = false;
7+
redirectUser: object|null = null;
8+
9+
async _get<T extends PersistenceValue>(key: string): Promise<T|null> {
10+
if (key.includes('pendingRedirect')) {
11+
return this.hasPendingRedirect.toString() as T;
12+
}
13+
14+
return this.redirectUser as T | null;
15+
}
16+
}

0 commit comments

Comments
 (0)