Skip to content

Commit 7e2cf04

Browse files
authored
Add web storage support check to popup actions (#3823)
* Add web storage check to popup actions * Formatting
1 parent deb2f7b commit 7e2cf04

File tree

7 files changed

+150
-3
lines changed

7 files changed

+150
-3
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,9 @@ export interface PopupRedirectResolver extends externs.PopupRedirectResolver {
8787
authType: AuthEventType,
8888
eventId?: string
8989
): Promise<never>;
90+
_isIframeWebStorageSupported(
91+
auth: AuthCore,
92+
cb: (support: boolean) => unknown
93+
): void;
9094
_redirectPersistence: externs.Persistence;
9195
}

packages-exp/auth-exp/src/platform_browser/iframe/gapi.iframes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ declare namespace gapi.iframes {
5454
handler: MessageHandler<T>,
5555
filter?: IframesFilter
5656
): void;
57+
send<T extends Message, U extends Message>(
58+
type: string,
59+
data: T,
60+
callback?: MessageHandler<U>,
61+
filter?: IframesFilter
62+
): void;
5763
ping(callback: SendCallback, data?: unknown): Promise<unknown[]>;
5864
restyle(
5965
style: Record<string, string | boolean>,

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe('src/platform_browser/popup_redirect', () => {
5151
let resolver: PopupRedirectResolver;
5252
let auth: TestAuth;
5353
let onIframeMessage: (event: GapiAuthEvent) => Promise<void>;
54+
let iframeSendStub: sinon.SinonStub;
5455

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

6162
sinon.stub(validateOrigin, '_validateOrigin').returns(Promise.resolve());
63+
iframeSendStub = sinon.stub();
6264

6365
sinon.stub(gapiLoader, '_loadGapi').returns(
6466
Promise.resolve(({
@@ -67,7 +69,8 @@ describe('src/platform_browser/popup_redirect', () => {
6769
register: (
6870
_message: string,
6971
cb: (event: GapiAuthEvent) => Promise<void>
70-
) => (onIframeMessage = cb)
72+
) => (onIframeMessage = cb),
73+
send: iframeSendStub
7174
})
7275
} as unknown) as gapi.iframes.Context)
7376
);
@@ -283,4 +286,56 @@ describe('src/platform_browser/popup_redirect', () => {
283286
});
284287
});
285288
});
289+
290+
context('#_isIframeWebStorageSupported', () => {
291+
beforeEach(async () => {
292+
await resolver._initialize(auth);
293+
});
294+
295+
function setIframeResponse(value: unknown): void {
296+
iframeSendStub.callsFake(
297+
(
298+
_message: string,
299+
_event: unknown,
300+
callback: (response: unknown) => void
301+
) => {
302+
callback(value);
303+
}
304+
);
305+
}
306+
307+
it('calls the iframe send method with the correct parameters', () => {
308+
resolver._isIframeWebStorageSupported(auth, () => {});
309+
expect(iframeSendStub).to.have.been.calledOnce;
310+
const args = iframeSendStub.getCalls()[0].args;
311+
expect(args[0]).to.eq('webStorageSupport');
312+
expect(args[1]).to.eql({
313+
type: 'webStorageSupport'
314+
});
315+
expect(args[3]).to.eq(gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER);
316+
});
317+
318+
it('passes through true value from the response to the callback', done => {
319+
setIframeResponse([{ webStorageSupport: true }]);
320+
resolver._isIframeWebStorageSupported(auth, supported => {
321+
expect(supported).to.be.true;
322+
done();
323+
});
324+
});
325+
326+
it('passes through false value from the response to callback', done => {
327+
setIframeResponse([{ webStorageSupport: false }]);
328+
resolver._isIframeWebStorageSupported(auth, supported => {
329+
expect(supported).to.be.false;
330+
done();
331+
});
332+
});
333+
334+
it('throws an error if the response is malformed', () => {
335+
setIframeResponse({});
336+
expect(() =>
337+
resolver._isIframeWebStorageSupported(auth, () => {})
338+
).to.throw(FirebaseError, 'auth/internal-error');
339+
});
340+
});
286341
});

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { isEmpty, querystring } from '@firebase/util';
2222
import { AuthEventManager } from '../core/auth/auth_event_manager';
2323
import { AuthErrorCode } from '../core/errors';
2424
import { OAuthProvider } from '../core/providers/oauth';
25-
import { assert, debugAssert } from '../core/util/assert';
25+
import { assert, debugAssert, fail } from '../core/util/assert';
2626
import { _emulatorUrl } from '../core/util/emulator';
2727
import { _generateEventId } from '../core/util/event_id';
2828
import { _getCurrentUrl } from '../core/util/location';
@@ -50,13 +50,23 @@ const WIDGET_PATH = '__/auth/handler';
5050
*/
5151
const EMULATOR_WIDGET_PATH = 'emulator/auth/handler';
5252

53+
/**
54+
* The special web storage event
55+
*/
56+
const WEB_STORAGE_SUPPORT_KEY = 'webStorageSupport';
57+
58+
interface WebStorageSupportMessage extends gapi.iframes.Message {
59+
[index: number]: Record<string, boolean>;
60+
}
61+
5362
interface ManagerOrPromise {
5463
manager?: EventManager;
5564
promise?: Promise<EventManager>;
5665
}
5766

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

6272
readonly _redirectPersistence = browserSessionPersistence;
@@ -73,6 +83,7 @@ class BrowserPopupRedirectResolver implements PopupRedirectResolver {
7383
this.eventManagers[auth._key()]?.manager,
7484
'_initialize() not called before _openPopup()'
7585
);
86+
7687
await this.originValidation(auth);
7788
const url = getRedirectUrl(auth, provider, authType, eventId);
7889
return _open(auth.name, url, _generateEventId());
@@ -124,9 +135,32 @@ class BrowserPopupRedirectResolver implements PopupRedirectResolver {
124135
);
125136

126137
this.eventManagers[auth._key()] = { manager };
138+
this.iframes[auth._key()] = iframe;
127139
return manager;
128140
}
129141

142+
_isIframeWebStorageSupported(
143+
auth: Auth,
144+
cb: (supported: boolean) => unknown
145+
): void {
146+
const iframe = this.iframes[auth._key()];
147+
iframe.send<gapi.iframes.Message, WebStorageSupportMessage>(
148+
WEB_STORAGE_SUPPORT_KEY,
149+
{ type: WEB_STORAGE_SUPPORT_KEY },
150+
result => {
151+
const isSupported = result?.[0]?.[WEB_STORAGE_SUPPORT_KEY];
152+
if (isSupported !== undefined) {
153+
cb(!!isSupported);
154+
}
155+
156+
fail(AuthErrorCode.INTERNAL_ERROR, {
157+
appName: auth.name
158+
});
159+
},
160+
gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER
161+
);
162+
}
163+
130164
private originValidation(auth: Auth): Promise<void> {
131165
const key = auth._key();
132166
if (!this.originValidationPromises[key]) {

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
reauthenticateWithPopup,
4949
signInWithPopup
5050
} from './popup';
51+
import { _getInstance } from '../../../internal';
5152

5253
use(sinonChai);
5354
use(chaiAsPromised);
@@ -197,6 +198,13 @@ describe('src/core/strategies/popup', () => {
197198
);
198199
});
199200

201+
it('errors if webstorage support comes back negative', async () => {
202+
resolver = makeMockPopupRedirectResolver(eventManager, authPopup, false);
203+
await expect(
204+
signInWithPopup(auth, provider, resolver)
205+
).to.be.rejectedWith(FirebaseError, 'auth/web-storage-unsupported');
206+
});
207+
200208
it('passes any errors from idp task', async () => {
201209
idpStubs._signIn.returns(
202210
Promise.reject(
@@ -346,6 +354,14 @@ describe('src/core/strategies/popup', () => {
346354
);
347355
});
348356

357+
it('errors if webstorage support comes back negative', async () => {
358+
resolver = makeMockPopupRedirectResolver(eventManager, authPopup, false);
359+
await expect(linkWithPopup(user, provider, resolver)).to.be.rejectedWith(
360+
FirebaseError,
361+
'auth/web-storage-unsupported'
362+
);
363+
});
364+
349365
it('passes any errors from idp task', async () => {
350366
idpStubs._link.returns(
351367
Promise.reject(
@@ -473,6 +489,13 @@ describe('src/core/strategies/popup', () => {
473489
expect(await promise).to.eq(cred);
474490
});
475491

492+
it('errors if webstorage support comes back negative', async () => {
493+
resolver = makeMockPopupRedirectResolver(eventManager, authPopup, false);
494+
await expect(
495+
reauthenticateWithPopup(user, provider, resolver)
496+
).to.be.rejectedWith(FirebaseError, 'auth/web-storage-unsupported');
497+
});
498+
476499
it('does error if the poll timeout and event timeout trip', async () => {
477500
const cred = new UserCredentialImpl({
478501
user,

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,23 @@ class PopupOperation extends AbstractPopupRedirectOperation {
146146
);
147147
this.authWindow.associatedEvent = eventId;
148148

149+
// Check for web storage support _after_ the popup is loaded. Checking for
150+
// web storage is slow (on the order of a second or so). Rather than
151+
// waiting on that before opening the window, optimistically open the popup
152+
// and check for storage support at the same time. If storage support is
153+
// not available, this will cause the whole thing to reject properly. It
154+
// will also close the popup, but since the promise has already rejected,
155+
// the popup closed by user poll will reject into the void.
156+
this.resolver._isIframeWebStorageSupported(this.auth, isSupported => {
157+
if (!isSupported) {
158+
this.reject(
159+
AUTH_ERROR_FACTORY.create(AuthErrorCode.WEB_STORAGE_UNSUPPORTED, {
160+
appName: this.auth.name
161+
})
162+
);
163+
}
164+
});
165+
149166
// Handle user closure. Notice this does *not* use await
150167
this.pollUserCancellation(this.auth.name);
151168
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ import { EventManager } from '../../src/model/popup_redirect';
2727
*/
2828
export function makeMockPopupRedirectResolver(
2929
eventManager?: EventManager,
30-
authPopup?: AuthPopup
30+
authPopup?: AuthPopup,
31+
webStorageSupported = true
3132
): PopupRedirectResolver {
3233
return class implements PopupRedirectResolver {
3334
async _initialize(): Promise<EventManager> {
@@ -40,6 +41,13 @@ export function makeMockPopupRedirectResolver(
4041

4142
async _openRedirect(): Promise<void> {}
4243

44+
_isIframeWebStorageSupported(
45+
_auth: unknown,
46+
cb: (result: boolean) => void
47+
): void {
48+
cb(webStorageSupported);
49+
}
50+
4351
_redirectPersistence?: Persistence;
4452
};
4553
}

0 commit comments

Comments
 (0)