Skip to content

Add web storage support check to popup actions #3823

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 2 commits into from
Sep 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
4 changes: 4 additions & 0 deletions packages-exp/auth-exp/src/model/popup_redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,9 @@ export interface PopupRedirectResolver extends externs.PopupRedirectResolver {
authType: AuthEventType,
eventId?: string
): Promise<never>;
_isIframeWebStorageSupported(
auth: AuthCore,
cb: (support: boolean) => unknown
): void;
_redirectPersistence: externs.Persistence;
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ declare namespace gapi.iframes {
handler: MessageHandler<T>,
filter?: IframesFilter
): void;
send<T extends Message, U extends Message>(
type: string,
data: T,
callback?: MessageHandler<U>,
filter?: IframesFilter
): void;
ping(callback: SendCallback, data?: unknown): Promise<unknown[]>;
restyle(
style: Record<string, string | boolean>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('src/platform_browser/popup_redirect', () => {
let resolver: PopupRedirectResolver;
let auth: TestAuth;
let onIframeMessage: (event: GapiAuthEvent) => Promise<void>;
let iframeSendStub: sinon.SinonStub;

beforeEach(async () => {
auth = await testAuth();
Expand All @@ -59,6 +60,7 @@ describe('src/platform_browser/popup_redirect', () => {
>)();

sinon.stub(validateOrigin, '_validateOrigin').returns(Promise.resolve());
iframeSendStub = sinon.stub();

sinon.stub(gapiLoader, '_loadGapi').returns(
Promise.resolve(({
Expand All @@ -67,7 +69,8 @@ describe('src/platform_browser/popup_redirect', () => {
register: (
_message: string,
cb: (event: GapiAuthEvent) => Promise<void>
) => (onIframeMessage = cb)
) => (onIframeMessage = cb),
send: iframeSendStub
})
} as unknown) as gapi.iframes.Context)
);
Expand Down Expand Up @@ -283,4 +286,56 @@ describe('src/platform_browser/popup_redirect', () => {
});
});
});

context('#_isIframeWebStorageSupported', () => {
beforeEach(async () => {
await resolver._initialize(auth);
});

function setIframeResponse(value: unknown): void {
iframeSendStub.callsFake(
(
_message: string,
_event: unknown,
callback: (response: unknown) => void
) => {
callback(value);
}
);
}

it('calls the iframe send method with the correct parameters', () => {
resolver._isIframeWebStorageSupported(auth, () => {});
expect(iframeSendStub).to.have.been.calledOnce;
const args = iframeSendStub.getCalls()[0].args;
expect(args[0]).to.eq('webStorageSupport');
expect(args[1]).to.eql({
type: 'webStorageSupport'
});
expect(args[3]).to.eq(gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER);
});

it('passes through true value from the response to the callback', done => {
setIframeResponse([{ webStorageSupport: true }]);
resolver._isIframeWebStorageSupported(auth, supported => {
expect(supported).to.be.true;
done();
});
});

it('passes through false value from the response to callback', done => {
setIframeResponse([{ webStorageSupport: false }]);
resolver._isIframeWebStorageSupported(auth, supported => {
expect(supported).to.be.false;
done();
});
});

it('throws an error if the response is malformed', () => {
setIframeResponse({});
expect(() =>
resolver._isIframeWebStorageSupported(auth, () => {})
).to.throw(FirebaseError, 'auth/internal-error');
});
});
});
36 changes: 35 additions & 1 deletion packages-exp/auth-exp/src/platform_browser/popup_redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { isEmpty, querystring } from '@firebase/util';
import { AuthEventManager } from '../core/auth/auth_event_manager';
import { AuthErrorCode } from '../core/errors';
import { OAuthProvider } from '../core/providers/oauth';
import { assert, debugAssert } from '../core/util/assert';
import { assert, debugAssert, fail } from '../core/util/assert';
import { _emulatorUrl } from '../core/util/emulator';
import { _generateEventId } from '../core/util/event_id';
import { _getCurrentUrl } from '../core/util/location';
Expand Down Expand Up @@ -50,13 +50,23 @@ const WIDGET_PATH = '__/auth/handler';
*/
const EMULATOR_WIDGET_PATH = 'emulator/auth/handler';

/**
* The special web storage event
*/
const WEB_STORAGE_SUPPORT_KEY = 'webStorageSupport';

interface WebStorageSupportMessage extends gapi.iframes.Message {
[index: number]: Record<string, boolean>;
}

interface ManagerOrPromise {
manager?: EventManager;
promise?: Promise<EventManager>;
}

class BrowserPopupRedirectResolver implements PopupRedirectResolver {
private readonly eventManagers: Record<string, ManagerOrPromise> = {};
private readonly iframes: Record<string, gapi.iframes.Iframe> = {};
private readonly originValidationPromises: Record<string, Promise<void>> = {};

readonly _redirectPersistence = browserSessionPersistence;
Expand All @@ -73,6 +83,7 @@ class BrowserPopupRedirectResolver implements PopupRedirectResolver {
this.eventManagers[auth._key()]?.manager,
'_initialize() not called before _openPopup()'
);

await this.originValidation(auth);
const url = getRedirectUrl(auth, provider, authType, eventId);
return _open(auth.name, url, _generateEventId());
Expand Down Expand Up @@ -124,9 +135,32 @@ class BrowserPopupRedirectResolver implements PopupRedirectResolver {
);

this.eventManagers[auth._key()] = { manager };
this.iframes[auth._key()] = iframe;
return manager;
}

_isIframeWebStorageSupported(
auth: Auth,
cb: (supported: boolean) => unknown
): void {
const iframe = this.iframes[auth._key()];
iframe.send<gapi.iframes.Message, WebStorageSupportMessage>(
WEB_STORAGE_SUPPORT_KEY,
{ type: WEB_STORAGE_SUPPORT_KEY },
result => {
const isSupported = result?.[0]?.[WEB_STORAGE_SUPPORT_KEY];
if (isSupported !== undefined) {
cb(!!isSupported);
}

fail(AuthErrorCode.INTERNAL_ERROR, {
appName: auth.name
});
},
gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER
);
}

private originValidation(auth: Auth): Promise<void> {
const key = auth._key();
if (!this.originValidationPromises[key]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
reauthenticateWithPopup,
signInWithPopup
} from './popup';
import { _getInstance } from '../../../internal';

use(sinonChai);
use(chaiAsPromised);
Expand Down Expand Up @@ -197,6 +198,13 @@ describe('src/core/strategies/popup', () => {
);
});

it('errors if webstorage support comes back negative', async () => {
resolver = makeMockPopupRedirectResolver(eventManager, authPopup, false);
await expect(
signInWithPopup(auth, provider, resolver)
).to.be.rejectedWith(FirebaseError, 'auth/web-storage-unsupported');
});

it('passes any errors from idp task', async () => {
idpStubs._signIn.returns(
Promise.reject(
Expand Down Expand Up @@ -346,6 +354,14 @@ describe('src/core/strategies/popup', () => {
);
});

it('errors if webstorage support comes back negative', async () => {
resolver = makeMockPopupRedirectResolver(eventManager, authPopup, false);
await expect(linkWithPopup(user, provider, resolver)).to.be.rejectedWith(
FirebaseError,
'auth/web-storage-unsupported'
);
});

it('passes any errors from idp task', async () => {
idpStubs._link.returns(
Promise.reject(
Expand Down Expand Up @@ -473,6 +489,13 @@ describe('src/core/strategies/popup', () => {
expect(await promise).to.eq(cred);
});

it('errors if webstorage support comes back negative', async () => {
resolver = makeMockPopupRedirectResolver(eventManager, authPopup, false);
await expect(
reauthenticateWithPopup(user, provider, resolver)
).to.be.rejectedWith(FirebaseError, 'auth/web-storage-unsupported');
});

it('does error if the poll timeout and event timeout trip', async () => {
const cred = new UserCredentialImpl({
user,
Expand Down
17 changes: 17 additions & 0 deletions packages-exp/auth-exp/src/platform_browser/strategies/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,23 @@ class PopupOperation extends AbstractPopupRedirectOperation {
);
this.authWindow.associatedEvent = eventId;

// Check for web storage support _after_ the popup is loaded. Checking for
// web storage is slow (on the order of a second or so). Rather than
// waiting on that before opening the window, optimistically open the popup
// and check for storage support at the same time. If storage support is
// not available, this will cause the whole thing to reject properly. It
// will also close the popup, but since the promise has already rejected,
// the popup closed by user poll will reject into the void.
this.resolver._isIframeWebStorageSupported(this.auth, isSupported => {
if (!isSupported) {
this.reject(
AUTH_ERROR_FACTORY.create(AuthErrorCode.WEB_STORAGE_UNSUPPORTED, {
appName: this.auth.name
})
);
}
});

// Handle user closure. Notice this does *not* use await
this.pollUserCancellation(this.auth.name);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import { EventManager } from '../../src/model/popup_redirect';
*/
export function makeMockPopupRedirectResolver(
eventManager?: EventManager,
authPopup?: AuthPopup
authPopup?: AuthPopup,
webStorageSupported = true
): PopupRedirectResolver {
return class implements PopupRedirectResolver {
async _initialize(): Promise<EventManager> {
Expand All @@ -40,6 +41,13 @@ export function makeMockPopupRedirectResolver(

async _openRedirect(): Promise<void> {}

_isIframeWebStorageSupported(
_auth: unknown,
cb: (result: boolean) => void
): void {
cb(webStorageSupported);
}

_redirectPersistence?: Persistence;
};
}