Skip to content

Commit d32e926

Browse files
sam-gcavolkovi
authored andcommitted
Add auth isolation for oauth flows (#3420)
* Add auth isolation for oauth flows * Formatting * Fix bug * Formatting * PR feedback * Formatting
1 parent 0cc3fea commit d32e926

File tree

6 files changed

+60
-21
lines changed

6 files changed

+60
-21
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ export class AuthImpl implements Auth {
247247
}
248248
}
249249

250+
_key(): string {
251+
return `${this.config.authDomain}:${this.config.apiKey}:${this.name}`;
252+
}
253+
250254
private notifyAuthListeners(): void {
251255
if (!this._isInitialized) {
252256
return;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { UserCredentialImpl } from '../user/user_credential_impl';
4747
import { _getInstance } from '../util/instantiator';
4848
import * as idpTasks from './idp';
4949
import {
50+
_clearOutcomes,
5051
getRedirectResult,
5152
linkWithRedirect,
5253
reauthenticateWithRedirect,
@@ -81,6 +82,7 @@ describe('src/core/strategies/redirect', () => {
8182

8283
afterEach(() => {
8384
sinon.restore();
85+
_clearOutcomes();
8486
});
8587

8688
context('signInWithRedirect', () => {

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,10 @@ async function prepareUserForRedirect(auth: Auth, user: User): Promise<string> {
107107

108108
// We only get one redirect outcome for any one auth, so just store it
109109
// in here.
110-
const redirectOutcomeMap: WeakMap<
111-
Auth,
110+
const redirectOutcomeMap: Map<
111+
string,
112112
() => Promise<UserCredential | null>
113-
> = new WeakMap();
113+
> = new Map();
114114

115115
class RedirectAction extends AbstractPopupRedirectOperation {
116116
eventId = null;
@@ -133,7 +133,7 @@ class RedirectAction extends AbstractPopupRedirectOperation {
133133
* just return it.
134134
*/
135135
async execute(): Promise<UserCredential | null> {
136-
let readyOutcome = redirectOutcomeMap.get(this.auth);
136+
let readyOutcome = redirectOutcomeMap.get(this.auth._key());
137137
if (!readyOutcome) {
138138
try {
139139
const result = await super.execute();
@@ -142,7 +142,7 @@ class RedirectAction extends AbstractPopupRedirectOperation {
142142
readyOutcome = () => Promise.reject(e);
143143
}
144144

145-
redirectOutcomeMap.set(this.auth, readyOutcome);
145+
redirectOutcomeMap.set(this.auth._key(), readyOutcome);
146146
}
147147

148148
return readyOutcome();
@@ -172,3 +172,7 @@ class RedirectAction extends AbstractPopupRedirectOperation {
172172

173173
cleanUp(): void {}
174174
}
175+
176+
export function _clearOutcomes(): void {
177+
redirectOutcomeMap.clear();
178+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export interface Auth extends externs.Auth {
4949
): Promise<void>;
5050
_redirectUserForId(id: string): Promise<User | null>;
5151
_popupRedirectResolver: PopupRedirectResolver | null;
52+
_key(): string;
5253
}
5354

5455
export interface Dependencies {

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ describe('src/platform_browser/popup_redirect', () => {
8181
return {} as Window;
8282
});
8383
provider = new OAuthProvider(ProviderId.GOOGLE);
84-
await resolver._initialize(auth);
8584
});
8685

8786
it('builds the correct url', async () => {
87+
await resolver._initialize(auth);
8888
provider.addScope('some-scope-a');
8989
provider.addScope('some-scope-b');
9090
provider.setCustomParameters({ foo: 'bar' });
@@ -105,6 +105,7 @@ describe('src/platform_browser/popup_redirect', () => {
105105

106106
it('throws an error if authDomain is unspecified', async () => {
107107
delete auth.config.authDomain;
108+
await resolver._initialize(auth);
108109

109110
await expect(
110111
resolver._openPopup(auth, provider, event)
@@ -113,6 +114,7 @@ describe('src/platform_browser/popup_redirect', () => {
113114

114115
it('throws an error if apiKey is unspecified', async () => {
115116
delete auth.config.apiKey;
117+
await resolver._initialize(auth);
116118

117119
await expect(
118120
resolver._openPopup(auth, provider, event)
@@ -176,9 +178,26 @@ describe('src/platform_browser/popup_redirect', () => {
176178
});
177179

178180
context('#_initialize', () => {
179-
it('only registers once, returns same event manager', async () => {
181+
it('returns different manager for a different auth', async () => {
180182
const manager = await resolver._initialize(auth);
181183
expect(await resolver._initialize(auth)).to.eq(manager);
184+
185+
const secondAuth = await testAuth();
186+
secondAuth.config.authDomain = 'something-else';
187+
const secondManager = await resolver._initialize(secondAuth);
188+
expect(secondManager).not.to.eq(manager);
189+
expect(await resolver._initialize(secondAuth)).to.eq(secondManager);
190+
});
191+
192+
it('initialization promise is cached as well for diff auths', async () => {
193+
const promise = resolver._initialize(auth);
194+
expect(resolver._initialize(auth)).to.eq(promise);
195+
196+
const secondAuth = await testAuth();
197+
secondAuth.config.authDomain = 'something-else';
198+
const secondPromise = resolver._initialize(secondAuth);
199+
expect(secondPromise).not.to.eq(promise);
200+
expect(resolver._initialize(secondAuth)).to.eq(secondPromise);
182201
});
183202

184203
it('iframe event goes through to the manager', async () => {

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

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,13 @@ import { _openIframe } from './iframe/iframe';
4343
*/
4444
const WIDGET_URL = '__/auth/handler';
4545

46+
interface ManagerOrPromise {
47+
manager?: EventManager;
48+
promise?: Promise<EventManager>;
49+
}
50+
4651
class BrowserPopupRedirectResolver implements PopupRedirectResolver {
47-
private eventManager: EventManager | null = null;
48-
private initializationPromise: Promise<EventManager> | null = null;
52+
private readonly eventManagers: Record<string, ManagerOrPromise> = {};
4953

5054
readonly _redirectPersistence = browserSessionPersistence;
5155

@@ -58,7 +62,7 @@ class BrowserPopupRedirectResolver implements PopupRedirectResolver {
5862
eventId?: string
5963
): Promise<AuthPopup> {
6064
debugAssert(
61-
this.eventManager,
65+
this.eventManagers[auth._key()]?.manager,
6266
'_initialize() not called before _openPopup()'
6367
);
6468
const url = getRedirectUrl(auth, provider, authType, eventId);
@@ -76,32 +80,37 @@ class BrowserPopupRedirectResolver implements PopupRedirectResolver {
7680
}
7781

7882
_initialize(auth: Auth): Promise<EventManager> {
79-
if (this.eventManager) {
80-
return Promise.resolve(this.eventManager);
81-
}
82-
83-
if (!this.initializationPromise) {
84-
this.initializationPromise = this.initAndGetManager(auth);
83+
const key = auth._key();
84+
if (this.eventManagers[key]) {
85+
const { manager, promise } = this.eventManagers[key];
86+
if (manager) {
87+
return Promise.resolve(manager);
88+
} else {
89+
debugAssert(promise, 'If manager is not set, promise should be');
90+
return promise;
91+
}
8592
}
8693

87-
return this.initializationPromise;
94+
const promise = this.initAndGetManager(auth);
95+
this.eventManagers[key] = { promise };
96+
return promise;
8897
}
8998

9099
private async initAndGetManager(auth: Auth): Promise<EventManager> {
91100
const iframe = await _openIframe(auth);
92-
const eventManager = new AuthEventManager(auth.name);
101+
const manager = new AuthEventManager(auth.name);
93102
iframe.register<GapiAuthEvent>(
94103
'authEvent',
95104
({ authEvent }: GapiAuthEvent) => {
96105
// TODO: Consider splitting redirect and popup events earlier on
97-
const handled = eventManager.onEvent(authEvent);
106+
const handled = manager.onEvent(authEvent);
98107
return { status: handled ? GapiOutcome.ACK : GapiOutcome.ERROR };
99108
},
100109
gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER
101110
);
102111

103-
this.eventManager = eventManager;
104-
return eventManager;
112+
this.eventManagers[auth._key()] = { manager };
113+
return manager;
105114
}
106115
}
107116

0 commit comments

Comments
 (0)