Skip to content

Commit bd554bf

Browse files
committed
Add auth event manager and popup_redirect browser implementation
1 parent eaf7d11 commit bd554bf

File tree

3 files changed

+393
-0
lines changed

3 files changed

+393
-0
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { AuthEvent, AuthEventConsumer, EventManager } from '../../model/popup_redirect';
19+
20+
export class AuthEventManager implements EventManager {
21+
private readonly consumers: Set<AuthEventConsumer> = new Set();
22+
23+
registerConsumer(authEventConsumer: AuthEventConsumer): void {
24+
this.consumers.add(authEventConsumer);
25+
}
26+
27+
unregisterConsumer(authEventConsumer: AuthEventConsumer): void {
28+
this.consumers.delete(authEventConsumer);
29+
}
30+
31+
onEvent(event: AuthEvent): void {
32+
this.consumers.forEach(consumer => {
33+
if (consumer.filter === event.type && consumer.isMatchingEvent(event.eventId)) {
34+
consumer.onAuthEvent(event);
35+
}
36+
});
37+
}
38+
}
39+
40+
// export abstract class Abstract
41+
42+
// export abstract class AbstractAuthEventManager {
43+
// // private readonly redirectOutcomeHandler = new RedirectManager();
44+
// private readonly popupOutcomeHandler = new PopupResultManager();
45+
46+
// constructor() {}
47+
48+
// abstract openPopup(
49+
// provider: AuthProvider,
50+
// authType: AuthEventType,
51+
// eventId?: string
52+
// ): Promise<AuthPopup>;
53+
54+
// abstract processRedirect(
55+
// auth: Auth,
56+
// provider: AuthProvider,
57+
// authType: AuthEventType
58+
// ): Promise<never>;
59+
60+
// abstract initializeAndWait(auth: Auth): Promise<void>;
61+
// abstract isInitialized(): boolean;
62+
63+
// async onEvent(event: AuthEvent): Promise<boolean> {
64+
// if (event.error && event.type !== AuthEventType.UNKNOWN) {
65+
// this.getOutcomeHandler(event.type).broadcastResult(null, event.error);
66+
// return true;
67+
// }
68+
69+
// const potentialUser = this.userForEvent(event.eventId);
70+
71+
// switch (event.type) {
72+
// case AuthEventType.SIGN_IN_VIA_POPUP:
73+
// if (!this.popupOutcomeHandler.isMatchingEvent(event.eventId)) {
74+
// break;
75+
// }
76+
// // Fallthrough
77+
// case AuthEventType.SIGN_IN_VIA_REDIRECT:
78+
// this.execIdpTask(event, idp.signIn);
79+
// break;
80+
// case AuthEventType.LINK_VIA_POPUP:
81+
// case AuthEventType.LINK_VIA_REDIRECT:
82+
// if (potentialUser) {
83+
// this.execIdpTask(event, idp.link, potentialUser);
84+
// }
85+
// break;
86+
// case AuthEventType.REAUTH_VIA_POPUP:
87+
// case AuthEventType.REAUTH_VIA_REDIRECT:
88+
// if (potentialUser) {
89+
// this.execIdpTask(event, idp.reauth, potentialUser);
90+
// }
91+
// break;
92+
// }
93+
94+
// // Always resolve with the iframe
95+
// return true;
96+
// }
97+
98+
// processPopup(
99+
// auth: Auth,
100+
// provider: AuthProvider,
101+
// authType: AuthEventType,
102+
// eventId?: string
103+
// ): Promise<UserCredential | null> {
104+
// // TODO: Fix the dirty hack
105+
// this.auth = auth;
106+
107+
// return this.popupOutcomeHandler.getNewPendingPromise(async () => {
108+
// if (!this.isInitialized()) {
109+
// await this.initializeAndWait(auth);
110+
// }
111+
112+
// const win = await this.openPopup(auth, provider, authType, eventId);
113+
// win.associatedEvent = eventId || null;
114+
// return win;
115+
// });
116+
// }
117+
118+
// getRedirectResult(auth: Auth): Promise<UserCredential | null> {
119+
// // TODO: Fix this dirty hack
120+
// this.auth = auth;
121+
// return this.redirectOutcomeHandler.getRedirectPromiseOrInit(() => {
122+
// if (!this.isInitialized()) {
123+
// this.initializeAndWait(auth);
124+
// }
125+
// });
126+
// }
127+
128+
// private userForEvent(id: string | null): User | undefined {
129+
// return this.auth
130+
// .getPotentialRedirectUsers_()
131+
// .find(u => u.redirectEventId_ === id);
132+
// }
133+
134+
// private getOutcomeHandler(
135+
// eventType: AuthEventType
136+
// ): PopupRedirectOutcomeHandler {
137+
// switch (eventType) {
138+
// case AuthEventType.SIGN_IN_VIA_POPUP:
139+
// case AuthEventType.LINK_VIA_POPUP:
140+
// case AuthEventType.REAUTH_VIA_POPUP:
141+
// return this.popupOutcomeHandler;
142+
// case AuthEventType.SIGN_IN_VIA_REDIRECT:
143+
// case AuthEventType.LINK_VIA_REDIRECT:
144+
// case AuthEventType.REAUTH_VIA_REDIRECT:
145+
// return this.redirectOutcomeHandler;
146+
// default:
147+
// throw AUTH_ERROR_FACTORY.create(AuthErrorCode.INTERNAL_ERROR, {
148+
// appName: 'TODO'
149+
// });
150+
// }
151+
// }
152+
153+
// private async execIdpTask(event: AuthEvent, task: idp.IdpTask, user?: User) {
154+
// const { urlResponse, sessionId, postBody, tenantId } = event;
155+
// const params: idp.IdpTaskParams = {
156+
// requestUri: urlResponse!,
157+
// sessionId: sessionId!,
158+
// auth: this.auth,
159+
// tenantId: tenantId || undefined,
160+
// postBody: postBody || undefined,
161+
// user
162+
// };
163+
164+
// const outcomeHandler = this.getOutcomeHandler(event.type);
165+
166+
// try {
167+
// const cred = await task(params);
168+
// outcomeHandler.broadcastResult(cred);
169+
// } catch (e) {
170+
// outcomeHandler.broadcastResult(null, e);
171+
// }
172+
// }
173+
// }
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import * as externs from '@firebase/auth-types-exp';
19+
20+
import { AuthPopup } from '../core/util/popup';
21+
import { Auth } from './auth';
22+
import { UserCredential } from './user';
23+
24+
export const enum EventFilter {
25+
POPUP,
26+
REDIRECT
27+
}
28+
29+
export const enum GapiOutcome {
30+
ACK = 'ACK',
31+
ERROR = 'ERROR'
32+
}
33+
34+
interface GapiAuthEvent extends gapi.iframes.Message {
35+
authEvent: AuthEvent;
36+
}
37+
38+
export const enum AuthEventType {
39+
LINK_VIA_POPUP = 'linkViaPopup',
40+
LINK_VIA_REDIRECT = 'linkViaRedirect',
41+
REAUTH_VIA_POPUP = 'reauthViaPopup',
42+
REAUTH_VIA_REDIRECT = 'reauthViaRedirect',
43+
SIGN_IN_VIA_POPUP = 'signInViaPopup',
44+
SIGN_IN_VIA_REDIRECT = 'signInViaRedirect',
45+
UNKNOWN = 'unknown',
46+
VERIFY_APP = 'verifyApp'
47+
}
48+
49+
// TODO: convert from these to FirebaseError
50+
export interface AuthEventError extends Error {
51+
code: string; // in the form of auth/${AuthErrorCode}
52+
message: string;
53+
}
54+
55+
export interface AuthEvent {
56+
type: AuthEventType;
57+
eventId: string | null;
58+
urlResponse: string | null;
59+
sessionId: string | null;
60+
postBody: string | null;
61+
tenantId: string | null;
62+
error: AuthEventError;
63+
}
64+
65+
export interface AuthEventConsumer {
66+
readonly filter: AuthEventType;
67+
isMatchingEvent(eventId: string | null): boolean;
68+
onAuthEvent(event: AuthEvent): unknown;
69+
}
70+
71+
export interface EventManager {
72+
registerConsumer(authEventConsumer: AuthEventConsumer): void;
73+
unregisterConsumer(authEventConsumer: AuthEventConsumer): void;
74+
}
75+
76+
export interface PopupRedirectResolver extends externs.PopupRedirectResolver {
77+
initialize(auth: Auth): Promise<EventManager>;
78+
// registerConsumer(authEventConsumer: AuthEventConsumer): void;
79+
// unregisterConsumer(authEventConsumer: AuthEventConsumer): void;
80+
openPopup(
81+
auth: Auth,
82+
provider: externs.AuthProvider,
83+
authType: AuthEventType,
84+
eventId?: string
85+
): Promise<AuthPopup>;
86+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { SDK_VERSION } from '@firebase/app-exp';
19+
import * as externs from '@firebase/auth-types-exp';
20+
import { isEmpty, querystring } from '@firebase/util';
21+
22+
import { AuthEventManager } from '../core/auth/auth_event_manager';
23+
import { AuthErrorCode } from '../core/errors';
24+
import { OAuthProvider } from '../core/providers/oauth';
25+
import { assert } from '../core/util/assert';
26+
import { _generateEventId } from '../core/util/event_id';
27+
import { _getCurrentUrl } from '../core/util/location';
28+
import { _open, AuthPopup } from '../core/util/popup';
29+
import { ApiKey, AppName, Auth } from '../model/auth';
30+
import {
31+
AuthEventType, EventManager, GapiAuthEvent, GapiOutcome, PopupRedirectResolver
32+
} from '../model/popup_redirect';
33+
import { _openIframe } from './iframe/iframe';
34+
35+
/**
36+
* URL for Authentication widget which will initiate the OAuth handshake
37+
*/
38+
const WIDGET_URL = '__/auth/handler';
39+
40+
export class BrowserPopupRedirectResolver implements PopupRedirectResolver {
41+
private eventManager: EventManager|null = null;
42+
43+
openPopup(
44+
auth: Auth,
45+
provider: externs.AuthProvider,
46+
authType: AuthEventType,
47+
eventId?: string
48+
): Promise<AuthPopup> {
49+
const url = getRedirectUrl(auth, provider, authType, eventId);
50+
return Promise.resolve(_open(auth.name, url, _generateEventId()));
51+
}
52+
53+
async initialize(auth: Auth): Promise<EventManager> {
54+
if (this.eventManager) {
55+
return this.eventManager;
56+
}
57+
58+
const iframe = await _openIframe(auth);
59+
const eventManager = new AuthEventManager();
60+
iframe.register<GapiAuthEvent>('authEvent',
61+
async (message: GapiAuthEvent) => {
62+
await eventManager.onEvent(message.authEvent);
63+
64+
// We always ACK with the iframe
65+
return { status: GapiOutcome.ACK };
66+
},
67+
gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER
68+
);
69+
70+
this.eventManager = eventManager;
71+
return eventManager;
72+
}
73+
}
74+
75+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
76+
type WidgetParams = {
77+
apiKey: ApiKey;
78+
appName: AppName;
79+
authType: AuthEventType;
80+
redirectUrl: string;
81+
v: string;
82+
providerId?: string;
83+
scopes?: string;
84+
customParameters?: string;
85+
eventId?: string;
86+
};
87+
88+
function getRedirectUrl(
89+
auth: Auth,
90+
provider: externs.AuthProvider,
91+
authType: AuthEventType,
92+
eventId?: string
93+
): string {
94+
assert(auth.config.authDomain, auth.name, AuthErrorCode.MISSING_AUTH_DOMAIN);
95+
assert(auth.config.apiKey, auth.name, AuthErrorCode.INVALID_API_KEY);
96+
97+
const params: WidgetParams = {
98+
apiKey: auth.config.apiKey,
99+
appName: auth.name,
100+
authType,
101+
redirectUrl: _getCurrentUrl(),
102+
v: SDK_VERSION,
103+
eventId
104+
};
105+
106+
if (provider instanceof OAuthProvider) {
107+
provider.setDefaultLanguage(auth.languageCode);
108+
params.providerId = provider.providerId || '';
109+
if (!isEmpty(provider.getCustomParameters())) {
110+
params.customParameters = JSON.stringify(provider.getCustomParameters());
111+
}
112+
const scopes = provider.getScopes();
113+
if (scopes.length > 0) {
114+
params.scopes = scopes.join(',');
115+
}
116+
// TODO set additionalParams?
117+
// let additionalParams = provider.getAdditionalParams();
118+
// for (let key in additionalParams) {
119+
// if (!params.hasOwnProperty(key)) {
120+
// params[key] = additionalParams[key]
121+
// }
122+
// }
123+
}
124+
125+
// TODO: maybe set tid as tenantId
126+
// TODO: maybe set eid as endipointId
127+
// TODO: maybe set fw as Frameworks.join(",")
128+
129+
const url = new URL(
130+
`https://${auth.config.authDomain}/${WIDGET_URL}?${querystring(params as Record<string, string|number>)}`
131+
);
132+
133+
return url.toString();
134+
}

0 commit comments

Comments
 (0)