Skip to content

[Auth] Fix redirect middleware #6165

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 4 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions packages/auth-compat/src/popup_redirect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ describe('popup_redirect/CompatPopupRedirectResolver', () => {

class FakeResolver implements exp.PopupRedirectResolverInternal {
_completeRedirectFn = async (): Promise<null> => null;
_overrideRedirectResult = (): void => {};
_redirectPersistence = exp.inMemoryPersistence;
_shouldInitProactively = true;

Expand Down
1 change: 1 addition & 0 deletions packages/auth-compat/src/popup_redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export class CompatPopupRedirectResolver
resolver: exp.PopupRedirectResolver,
bypassAuthState: boolean
) => Promise<exp.UserCredential | null> = exp._getRedirectResult;
_overrideRedirectResult = exp._overrideRedirectResult;

async _initialize(auth: exp.AuthImpl): Promise<exp.EventManager> {
await this.selectUnderlyingResolver();
Expand Down
1 change: 1 addition & 0 deletions packages/auth/internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export { TaggedWithTokenResponse } from '../src/model/id_token';
export { _fail, _assert } from '../src/core/util/assert';
export { AuthPopup } from '../src/platform_browser/util/popup';
export { _getRedirectResult } from '../src/platform_browser/strategies/redirect';
export { _overrideRedirectResult } from '../src/core/strategies/redirect';
export { cordovaPopupRedirectResolver } from '../src/platform_cordova/popup_redirect/popup_redirect';
export { FetchProvider } from '../src/core/util/fetch_provider';
export { SAMLAuthCredential } from '../src/core/credentials/saml';
Expand Down
49 changes: 29 additions & 20 deletions packages/auth/src/core/auth/auth_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,14 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
popupRedirectResolver?: PopupRedirectResolver
): Promise<void> {
// First check to see if we have a pending redirect event.
let storedUser =
const previouslyStoredUser =
(await this.assertedPersistence.getCurrentUser()) as UserInternal | null;
let futureCurrentUser = previouslyStoredUser;
let needsTocheckMiddleware = false;
if (popupRedirectResolver && this.config.authDomain) {
await this.getOrInitRedirectPersistenceManager();
const redirectUserEventId = this.redirectUser?._redirectEventId;
const storedUserEventId = storedUser?._redirectEventId;
const storedUserEventId = futureCurrentUser?._redirectEventId;
const result = await this.tryRedirectSignIn(popupRedirectResolver);

// If the stored user (i.e. the old "currentUser") has a redirectId that
Expand All @@ -208,44 +210,51 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
(!redirectUserEventId || redirectUserEventId === storedUserEventId) &&
result?.user
) {
storedUser = result.user as UserInternal;
futureCurrentUser = result.user as UserInternal;
needsTocheckMiddleware = true;
}
}

// If no user in persistence, there is no current user. Set to null.
if (!storedUser) {
if (!futureCurrentUser) {
return this.directlySetCurrentUser(null);
}

if (!storedUser._redirectEventId) {
// This isn't a redirect user, we can reload and bail
// This will also catch the redirected user, if available, as that method
// strips the _redirectEventId
return this.reloadAndSetCurrentUserOrClear(storedUser);
if (!futureCurrentUser._redirectEventId) {
// This isn't a redirect link operation, we can reload and bail.
// First though, ensure that we check the middleware is happy.
if (needsTocheckMiddleware) {
try {
await this._runBeforeStateCallbacks(futureCurrentUser);
} catch(e) {
futureCurrentUser = previouslyStoredUser;
// We know this is available since the bit is only set when the
// resolver is available
this._popupRedirectResolver!._overrideRedirectResult(this, () => Promise.reject(e));
}
}

if (futureCurrentUser) {
return this.reloadAndSetCurrentUserOrClear(futureCurrentUser);
} else {
return this.directlySetCurrentUser(null);
}
}

_assert(this._popupRedirectResolver, this, AuthErrorCode.ARGUMENT_ERROR);
await this.getOrInitRedirectPersistenceManager();

// At this point in the flow, this is a redirect user. Run blocking
// middleware callbacks before setting the user.
try {
await this._runBeforeStateCallbacks(storedUser);
} catch(e) {
return;
}

// If the redirect user's event ID matches the current user's event ID,
// DO NOT reload the current user, otherwise they'll be cleared from storage.
// This is important for the reauthenticateWithRedirect() flow.
if (
this.redirectUser &&
this.redirectUser._redirectEventId === storedUser._redirectEventId
this.redirectUser._redirectEventId === futureCurrentUser._redirectEventId
) {
return this.directlySetCurrentUser(storedUser);
return this.directlySetCurrentUser(futureCurrentUser);
}

return this.reloadAndSetCurrentUserOrClear(storedUser);
return this.reloadAndSetCurrentUserOrClear(futureCurrentUser);
}

private async tryRedirectSignIn(
Expand Down
2 changes: 2 additions & 0 deletions packages/auth/src/core/auth/initialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ describe('core/auth/initialize', () => {
): Promise<UserCredential | null> {
return null;
}
async _overrideRedirectResult(): Promise<void> {
}
}

const fakePopupRedirectResolver: PopupRedirectResolver =
Expand Down
4 changes: 4 additions & 0 deletions packages/auth/src/core/strategies/redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ export function _clearRedirectOutcomes(): void {
redirectOutcomeMap.clear();
}

export function _overrideRedirectResult(auth: AuthInternal, result: () => Promise<UserCredentialInternal | null>): void {
redirectOutcomeMap.set(auth._key(), result);
}

function resolverPersistence(
resolver: PopupRedirectResolverInternal
): PersistenceInternal {
Expand Down
4 changes: 3 additions & 1 deletion packages/auth/src/model/popup_redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { FirebaseError } from '@firebase/util';

import { AuthPopup } from '../platform_browser/util/popup';
import { AuthInternal } from './auth';
import { UserCredentialInternal } from './user';

export const enum EventFilter {
POPUP,
Expand Down Expand Up @@ -121,10 +122,11 @@ export interface PopupRedirectResolverInternal extends PopupRedirectResolver {
_redirectPersistence: Persistence;
_originValidation(auth: Auth): Promise<void>;

// This is needed so that auth does not have a hard dependency on redirect
// These are needed so that auth does not have a hard dependency on redirect
_completeRedirectFn: (
auth: Auth,
resolver: PopupRedirectResolver,
bypassAuthState: boolean
) => Promise<UserCredential | null>;
_overrideRedirectResult: (auth: AuthInternal, resultGetter: () => Promise<UserCredentialInternal|null>) => void;
}
88 changes: 87 additions & 1 deletion packages/auth/src/platform_browser/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ describe('core/auth/initializeAuth', () => {
async function initAndWait(
persistence: Persistence | Persistence[],
popupRedirectResolver?: PopupRedirectResolver,
authDomain = FAKE_APP.options.authDomain
authDomain = FAKE_APP.options.authDomain,
blockMiddleware = false,
): Promise<Auth> {
const auth = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, {
apiKey: FAKE_APP.options.apiKey!,
Expand All @@ -146,6 +147,12 @@ describe('core/auth/initializeAuth', () => {
persistence,
popupRedirectResolver
});

if (blockMiddleware) {
auth.beforeAuthStateChanged(() => {
throw new Error('blocked');
});
}
// Auth initializes async. We can make sure the initialization is
// flushed by awaiting a method on the queue.
await auth.setPersistence(inMemoryPersistence);
Expand Down Expand Up @@ -408,6 +415,85 @@ describe('core/auth/initializeAuth', () => {
expect(user).not.to.be.null;
expect(auth.currentUser).to.eq(user);
});

it('does not halt old user load if middleware throws', async () => {
const stub = sinon.stub(
_getInstance<PersistenceInternal>(inMemoryPersistence)
);
const oldUser = testUser(oldAuth, 'old-uid');
stub._get.returns(Promise.resolve(oldUser.toJSON()));
const overrideSpy = sinon.spy(_getInstance<PopupRedirectResolverInternal>(browserPopupRedirectResolver), '_overrideRedirectResult');
const auth = await initAndWait(
[inMemoryPersistence],
browserPopupRedirectResolver,
FAKE_APP.options.authDomain,
/* blockMiddleware */ true
);

expect(auth.currentUser!.uid).to.eq(oldUser.uid);
expect(reload._reloadWithoutSaving).to.have.been.called;
expect(overrideSpy).not.to.have.been.called;
});

it('Reloads and uses old user if middleware throws', async () => {
const stub = sinon.stub(
_getInstance<PersistenceInternal>(inMemoryPersistence)
);
const oldUser = testUser(oldAuth, 'old-uid');
stub._get.returns(Promise.resolve(oldUser.toJSON()));
const overrideSpy = sinon.spy(_getInstance<PopupRedirectResolverInternal>(browserPopupRedirectResolver), '_overrideRedirectResult');

let user: UserInternal | null = null;
completeRedirectFnStub.callsFake((auth: AuthInternal) => {
user = testUser(auth, 'uid', '[email protected]');
return Promise.resolve(
new UserCredentialImpl({
operationType: OperationType.SIGN_IN,
user,
providerId: null
})
);
});

const auth = await initAndWait(
[inMemoryPersistence],
browserPopupRedirectResolver,
FAKE_APP.options.authDomain,
/* blockMiddleware */ true
);
expect(user).not.to.be.null;
expect(auth.currentUser!.uid).to.eq(oldUser.uid);
expect(reload._reloadWithoutSaving).to.have.been.called;
expect(overrideSpy).to.have.been.called;
});

it('Nulls current user if redirect blocked by middleware', async () => {
const stub = sinon.stub(
_getInstance<PersistenceInternal>(inMemoryPersistence)
);
stub._get.returns(Promise.resolve(null));
let user: UserInternal | null = null;
completeRedirectFnStub.callsFake((auth: AuthInternal) => {
user = testUser(auth, 'uid', '[email protected]');
return Promise.resolve(
new UserCredentialImpl({
operationType: OperationType.SIGN_IN,
user,
providerId: null
})
);
});

const auth = await initAndWait(
[inMemoryPersistence],
browserPopupRedirectResolver,
FAKE_APP.options.authDomain,
/* blockMiddleware */ true
);
expect(user).not.to.be.null;
expect(auth.currentUser).to.be.null;
expect(reload._reloadWithoutSaving).not.to.have.been.called;
});
});
});
});
3 changes: 3 additions & 0 deletions packages/auth/src/platform_browser/popup_redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { _open, AuthPopup } from './util/popup';
import { _getRedirectResult } from './strategies/redirect';
import { _getRedirectUrl } from '../core/util/handler';
import { _isIOS, _isMobileBrowser, _isSafari } from '../core/util/browser';
import { _overrideRedirectResult } from '../core/strategies/redirect';

/**
* The special web storage event
Expand Down Expand Up @@ -176,6 +177,8 @@ class BrowserPopupRedirectResolver implements PopupRedirectResolverInternal {
}

_completeRedirectFn = _getRedirectResult;

_overrideRedirectResult = _overrideRedirectResult;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
} from './events';
import { AuthEventManager } from '../../core/auth/auth_event_manager';
import { _getRedirectResult } from '../../platform_browser/strategies/redirect';
import { _clearRedirectOutcomes } from '../../core/strategies/redirect';
import { _clearRedirectOutcomes, _overrideRedirectResult } from '../../core/strategies/redirect';
import { _cordovaWindow } from '../plugins';

/**
Expand All @@ -58,6 +58,7 @@ class CordovaPopupRedirectResolver implements PopupRedirectResolverInternal {
private readonly originValidationPromises: Record<string, Promise<void>> = {};

_completeRedirectFn = _getRedirectResult;
_overrideRedirectResult = _overrideRedirectResult;

async _initialize(auth: AuthInternal): Promise<CordovaAuthEventManager> {
const key = auth._key();
Expand Down
2 changes: 2 additions & 0 deletions packages/auth/test/helpers/mock_popup_redirect_resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export function makeMockPopupRedirectResolver(

async _completeRedirectFn(): Promise<void> {}

async _overrideRedirectResult(): Promise<void> {}

async _originValidation(): Promise<void> {}
};
}
10 changes: 7 additions & 3 deletions packages/auth/test/integration/webdriver/redirect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ import { START_FUNCTION } from './util/auth_driver';

use(chaiAsPromised);

declare const xit: typeof it;

browserDescribe('WebDriver redirect IdP test', driver => {
beforeEach(async () => {
await driver.pause(200); // Race condition on auth init
Expand Down Expand Up @@ -76,7 +74,12 @@ browserDescribe('WebDriver redirect IdP test', driver => {
});

// Redirect works with middleware for now
xit('is blocked by middleware', async () => {
it('is blocked by middleware', async () => {
if (driver.isCompatLayer()) {
console.warn('Skipping middleware tests in compat');
return;
}

await driver.callNoWait(RedirectFunction.IDP_REDIRECT);
const widget = new IdPPage(driver.webDriver);

Expand All @@ -92,6 +95,7 @@ browserDescribe('WebDriver redirect IdP test', driver => {
await driver.call(MiddlewareFunction.ATTACH_BLOCKING_MIDDLEWARE_ON_START);

await driver.reinitOnRedirect();
await expect(driver.call(RedirectFunction.REDIRECT_RESULT)).to.be.rejectedWith('auth/login-blocked');
expect(await driver.getUserSnapshot()).to.be.null;
});

Expand Down