Skip to content

Await on the auth initialization promise in signIn/link/reauthenticateWithRedirect #6914

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 3 commits into from
Jan 6, 2023
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
5 changes: 5 additions & 0 deletions .changeset/tricky-ravens-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/auth': patch
---

Fix to minimize a potential race condition between auth init and signInWithRedirect
21 changes: 21 additions & 0 deletions packages/auth/demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1812,6 +1812,27 @@ function initApp() {
},
onAuthError);

// Try sign in with redirect once upon page load, not on subsequent loads.
// This will demonstrate the behavior when signInWithRedirect is called before
// auth is fully initialized. This will fail on firebase/auth versions 0.21.0 and lower
// due to https://github.com/firebase/firebase-js-sdk/issues/6827
/*
if (sessionStorage.getItem('redirect-race-test') !== 'done') {
console.log('Starting redirect sign in upon page load.');
try {
sessionStorage.setItem('redirect-race-test', 'done');
signInWithRedirect(
auth,
new GoogleAuthProvider(),
browserPopupRedirectResolver
).catch(onAuthError);
} catch (error) {
console.log('Error while calling signInWithRedirect');
console.error(error);
}
}
*/

// Bootstrap tooltips.
$('[data-toggle="tooltip"]').tooltip();

Expand Down
98 changes: 98 additions & 0 deletions packages/auth/src/platform_browser/strategies/redirect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,37 @@ describe('platform_browser/strategies/redirect', () => {
'auth/argument-error'
);
});

it('awaits on the auth initialization promise before opening redirect', async () => {
// Obtain an auth instance which does not await on the initialization promise.
const authWithoutAwait: TestAuth = await testAuth(
resolver,
undefined,
true
);
// completeRedirectFn calls getRedirectResult under the hood.
const getRedirectResultSpy = sinon.spy(
_getInstance<PopupRedirectResolverInternal>(resolver),
'_completeRedirectFn'
);
const openRedirectSpy = sinon.spy(
_getInstance<PopupRedirectResolverInternal>(resolver),
'_openRedirect'
);
await signInWithRedirect(authWithoutAwait, provider);
expect(getRedirectResultSpy).to.have.been.called;
expect(getRedirectResultSpy).to.have.been.calledBefore(openRedirectSpy);
expect(getRedirectResultSpy).to.have.been.calledWith(
authWithoutAwait,
resolver,
true
);
expect(openRedirectSpy).to.have.been.calledWith(
authWithoutAwait,
provider,
AuthEventType.SIGN_IN_VIA_REDIRECT
);
});
});

context('linkWithRedirect', () => {
Expand Down Expand Up @@ -159,6 +190,39 @@ describe('platform_browser/strategies/redirect', () => {
);
});

it('awaits on the auth initialization promise before opening redirect', async () => {
// Obtain an auth instance which does not await on the initialization promise.
const authWithoutAwait: TestAuth = await testAuth(
resolver,
undefined,
true
);
user = testUser(authWithoutAwait, 'uid', 'email', true);
// completeRedirectFn calls getRedirectResult under the hood.
const getRedirectResultSpy = sinon.spy(
_getInstance<PopupRedirectResolverInternal>(resolver),
'_completeRedirectFn'
);
const openRedirectSpy = sinon.spy(
_getInstance<PopupRedirectResolverInternal>(resolver),
'_openRedirect'
);
await authWithoutAwait._updateCurrentUser(user);
await linkWithRedirect(user, provider, resolver);
expect(getRedirectResultSpy).to.have.been.called;
expect(getRedirectResultSpy).to.have.been.calledBefore(openRedirectSpy);
expect(getRedirectResultSpy).to.have.been.calledWith(
authWithoutAwait,
resolver,
true
);
expect(openRedirectSpy).to.have.been.calledWith(
authWithoutAwait,
provider,
AuthEventType.LINK_VIA_REDIRECT
);
});

it('errors if no resolver available', async () => {
auth._popupRedirectResolver = null;
await expect(linkWithRedirect(user, provider)).to.be.rejectedWith(
Expand Down Expand Up @@ -236,6 +300,40 @@ describe('platform_browser/strategies/redirect', () => {
);
});

it('awaits on the auth initialization promise before opening redirect', async () => {
// Obtain an auth instance which does not await on the initialization promise.
const authWithoutAwait: TestAuth = await testAuth(
resolver,
undefined,
true
);
user = testUser(authWithoutAwait, 'uid', 'email', true);
// completeRedirectFn calls getRedirectResult under the hood.
const getRedirectResultSpy = sinon.spy(
_getInstance<PopupRedirectResolverInternal>(resolver),
'_completeRedirectFn'
);
const openRedirectSpy = sinon.spy(
_getInstance<PopupRedirectResolverInternal>(resolver),
'_openRedirect'
);
await authWithoutAwait._updateCurrentUser(user);
await signInWithRedirect(authWithoutAwait, provider);
await reauthenticateWithRedirect(user, provider);
expect(getRedirectResultSpy).to.have.been.called;
expect(getRedirectResultSpy).to.have.been.calledBefore(openRedirectSpy);
expect(getRedirectResultSpy).to.have.been.calledWith(
authWithoutAwait,
resolver,
true
);
expect(openRedirectSpy).to.have.been.calledWith(
authWithoutAwait,
provider,
AuthEventType.REAUTH_VIA_REDIRECT
);
});

it('errors if no resolver available', async () => {
auth._popupRedirectResolver = null;
await expect(
Expand Down
12 changes: 12 additions & 0 deletions packages/auth/src/platform_browser/strategies/redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export async function _signInWithRedirect(
): Promise<void | never> {
const authInternal = _castAuth(auth);
_assertInstanceOf(auth, provider, FederatedAuthProvider);
// Wait for auth initialization to complete, this will process pending redirects and clear the
// PENDING_REDIRECT_KEY in persistence. This should be completed before starting a new
// redirect and creating a PENDING_REDIRECT_KEY entry.
await authInternal._initializationPromise;
const resolverInternal = _withDefaultResolver(authInternal, resolver);
await _setPendingRedirectStatus(resolverInternal, authInternal);

Expand Down Expand Up @@ -147,6 +151,10 @@ export async function _reauthenticateWithRedirect(
): Promise<void | never> {
const userInternal = getModularInstance(user) as UserInternal;
_assertInstanceOf(userInternal.auth, provider, FederatedAuthProvider);
// Wait for auth initialization to complete, this will process pending redirects and clear the
// PENDING_REDIRECT_KEY in persistence. This should be completed before starting a new
// redirect and creating a PENDING_REDIRECT_KEY entry.
await userInternal.auth._initializationPromise;
// Allow the resolver to error before persisting the redirect user
const resolverInternal = _withDefaultResolver(userInternal.auth, resolver);
await _setPendingRedirectStatus(resolverInternal, userInternal.auth);
Expand Down Expand Up @@ -199,6 +207,10 @@ export async function _linkWithRedirect(
): Promise<void | never> {
const userInternal = getModularInstance(user) as UserInternal;
_assertInstanceOf(userInternal.auth, provider, FederatedAuthProvider);
// Wait for auth initialization to complete, this will process pending redirects and clear the
// PENDING_REDIRECT_KEY in persistence. This should be completed before starting a new
// redirect and creating a PENDING_REDIRECT_KEY entry.
await userInternal.auth._initializationPromise;
// Allow the resolver to error before persisting the redirect user
const resolverInternal = _withDefaultResolver(userInternal.auth, resolver);
await _assertLinkedStatus(false, userInternal, provider.providerId);
Expand Down
11 changes: 9 additions & 2 deletions packages/auth/test/helpers/mock_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export class MockPersistenceLayer extends InMemoryPersistence {

export async function testAuth(
popupRedirectResolver?: PopupRedirectResolver,
persistence = new MockPersistenceLayer()
persistence = new MockPersistenceLayer(),
skipAwaitOnInit?: boolean
): Promise<TestAuth> {
const auth: TestAuth = new AuthImpl(
FAKE_APP,
Expand All @@ -88,7 +89,13 @@ export async function testAuth(
) as TestAuth;
auth._updateErrorMap(debugErrorMap);

await auth._initializeWithPersistence([persistence], popupRedirectResolver);
if (skipAwaitOnInit) {
// This is used to verify scenarios where auth flows (like signInWithRedirect) are invoked before auth is fully initialized.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
auth._initializeWithPersistence([persistence], popupRedirectResolver);
} else {
await auth._initializeWithPersistence([persistence], popupRedirectResolver);
}
auth.persistenceLayer = persistence;
auth.settings.appVerificationDisabledForTesting = true;
return auth;
Expand Down