Skip to content

Commit 4160e32

Browse files
sam-gcavolkovi
authored andcommitted
Add browser popup resolver class, some utils (#3336)
* Add auth event manager and popup_redirect browser implementation * Add browser popup implementation, some utils * Formatting * PR feedback * Formatting * Fix failing tests * formatting
1 parent 0c70559 commit 4160e32

File tree

6 files changed

+585
-1
lines changed

6 files changed

+585
-1
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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 { expect, use } from 'chai';
19+
import * as sinon from 'sinon';
20+
import * as sinonChai from 'sinon-chai';
21+
22+
import {
23+
AuthEvent,
24+
AuthEventConsumer,
25+
AuthEventError,
26+
AuthEventType
27+
} from '../../model/popup_redirect';
28+
import { AuthErrorCode } from '../errors';
29+
import { AuthEventManager } from './auth_event_manager';
30+
31+
use(sinonChai);
32+
33+
describe('src/core/auth/auth_event_manager', () => {
34+
let manager: AuthEventManager;
35+
36+
function makeConsumer(
37+
filter: AuthEventType
38+
): sinon.SinonStubbedInstance<AuthEventConsumer> {
39+
const stub = sinon.stub({
40+
filter,
41+
isMatchingEvent: () => true,
42+
onAuthEvent: () => {},
43+
onError: () => {}
44+
});
45+
46+
// Make isMatchingEvent call through by default
47+
stub.isMatchingEvent.returns(true);
48+
49+
return stub;
50+
}
51+
52+
function makeEvent(type: AuthEventType, eventId = 'event'): AuthEvent {
53+
return {
54+
type,
55+
eventId
56+
} as AuthEvent;
57+
}
58+
59+
beforeEach(() => {
60+
manager = new AuthEventManager('app-name');
61+
});
62+
63+
it('multiple consumers may be registered for one event type', () => {
64+
const a = makeConsumer(AuthEventType.LINK_VIA_POPUP);
65+
const b = makeConsumer(AuthEventType.LINK_VIA_POPUP);
66+
67+
const evt = makeEvent(AuthEventType.LINK_VIA_POPUP);
68+
manager.registerConsumer(a);
69+
manager.registerConsumer(b);
70+
manager.onEvent(evt);
71+
72+
expect(a.onAuthEvent).to.have.been.calledWith(evt);
73+
expect(b.onAuthEvent).to.have.been.calledWith(evt);
74+
});
75+
76+
it('can unregister listeners', () => {
77+
const a = makeConsumer(AuthEventType.LINK_VIA_POPUP);
78+
const b = makeConsumer(AuthEventType.LINK_VIA_POPUP);
79+
80+
const evt = makeEvent(AuthEventType.LINK_VIA_POPUP);
81+
manager.registerConsumer(a);
82+
manager.registerConsumer(b);
83+
manager.unregisterConsumer(a);
84+
manager.onEvent(evt);
85+
86+
expect(a.onAuthEvent).not.to.have.been.calledWith(evt);
87+
expect(b.onAuthEvent).to.have.been.calledWith(evt);
88+
});
89+
90+
it('does not call the consumer if filter does not match', () => {
91+
const consumer = makeConsumer(AuthEventType.REAUTH_VIA_POPUP);
92+
manager.registerConsumer(consumer);
93+
manager.onEvent(makeEvent(AuthEventType.REAUTH_VIA_REDIRECT));
94+
expect(consumer.onAuthEvent).not.to.have.been.called;
95+
});
96+
97+
it('calls isMatchingEvent with the event id', () => {
98+
const consumer = makeConsumer(AuthEventType.REAUTH_VIA_POPUP);
99+
manager.registerConsumer(consumer);
100+
manager.onEvent(makeEvent(AuthEventType.REAUTH_VIA_POPUP, 'event-id'));
101+
expect(consumer.isMatchingEvent).to.have.been.calledWith('event-id');
102+
});
103+
104+
it('does not call through if isMatchingEvent is false', () => {
105+
const consumer = makeConsumer(AuthEventType.REAUTH_VIA_POPUP);
106+
manager.registerConsumer(consumer);
107+
consumer.isMatchingEvent.returns(false);
108+
109+
manager.onEvent(makeEvent(AuthEventType.REAUTH_VIA_POPUP));
110+
expect(consumer.onAuthEvent).not.to.have.been.called;
111+
expect(consumer.isMatchingEvent).to.have.been.called;
112+
});
113+
114+
it('converts errors into FirebaseError if the type matches', () => {
115+
const consumer = makeConsumer(AuthEventType.REAUTH_VIA_POPUP);
116+
manager.registerConsumer(consumer);
117+
const event = makeEvent(AuthEventType.REAUTH_VIA_POPUP);
118+
event.error = {
119+
code: `auth/${AuthErrorCode.INVALID_APP_CREDENTIAL}`,
120+
message: 'foo',
121+
name: 'name'
122+
};
123+
124+
manager.onEvent(event);
125+
const error = consumer.onError.getCall(0).args[0];
126+
expect(error.code).to.eq(`auth/${AuthErrorCode.INVALID_APP_CREDENTIAL}`);
127+
});
128+
129+
it('converts random errors into FirebaseError with internal error', () => {
130+
const consumer = makeConsumer(AuthEventType.REAUTH_VIA_POPUP);
131+
manager.registerConsumer(consumer);
132+
const event = makeEvent(AuthEventType.REAUTH_VIA_POPUP);
133+
event.error = {
134+
message: 'foo',
135+
name: 'name'
136+
} as AuthEventError;
137+
138+
manager.onEvent(event);
139+
const error = consumer.onError.getCall(0).args[0];
140+
expect(error.code).to.eq(`auth/${AuthErrorCode.INTERNAL_ERROR}`);
141+
});
142+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 {
19+
AuthEvent,
20+
AuthEventConsumer,
21+
EventManager
22+
} from '../../model/popup_redirect';
23+
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
24+
25+
export class AuthEventManager implements EventManager {
26+
private readonly consumers: Set<AuthEventConsumer> = new Set();
27+
28+
constructor(private readonly appName: string) {}
29+
30+
registerConsumer(authEventConsumer: AuthEventConsumer): void {
31+
this.consumers.add(authEventConsumer);
32+
}
33+
34+
unregisterConsumer(authEventConsumer: AuthEventConsumer): void {
35+
this.consumers.delete(authEventConsumer);
36+
}
37+
38+
onEvent(event: AuthEvent): void {
39+
this.consumers.forEach(consumer => {
40+
if (
41+
consumer.filter === event.type &&
42+
consumer.isMatchingEvent(event.eventId)
43+
) {
44+
if (event.error) {
45+
console.error('ERROR');
46+
const code =
47+
(event.error.code?.split('auth/')[1] as AuthErrorCode) ||
48+
AuthErrorCode.INTERNAL_ERROR;
49+
consumer.onError(
50+
AUTH_ERROR_FACTORY.create(code, {
51+
appName: this.appName
52+
})
53+
);
54+
} else {
55+
consumer.onAuthEvent(event);
56+
}
57+
}
58+
});
59+
}
60+
}

packages-exp/auth-exp/src/core/providers/oauth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class OAuthProvider implements externs.AuthProvider {
3232
defaultLanguageCode: string | null = null;
3333
private scopes: string[] = [];
3434
private customParameters: CustomParameters = {};
35-
private constructor(readonly providerId: externs.ProviderId) {}
35+
constructor(readonly providerId: externs.ProviderId) {}
3636
static credentialFromResult(
3737
_userCredential: externs.UserCredential
3838
): externs.OAuthCredential | null {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
import { FirebaseError } from '@firebase/util';
20+
21+
import { AuthPopup } from '../core/util/popup';
22+
import { Auth } from './auth';
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+
export interface AuthEventError extends Error {
50+
code: string; // in the form of auth/${AuthErrorCode}
51+
message: string;
52+
}
53+
54+
export interface AuthEvent {
55+
type: AuthEventType;
56+
eventId: string | null;
57+
urlResponse: string | null;
58+
sessionId: string | null;
59+
postBody: string | null;
60+
tenantId: string | null;
61+
error: AuthEventError;
62+
}
63+
64+
export interface AuthEventConsumer {
65+
readonly filter: AuthEventType;
66+
isMatchingEvent(eventId: string | null): boolean;
67+
onAuthEvent(event: AuthEvent): unknown;
68+
onError(error: FirebaseError): 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+
_openPopup(
79+
auth: Auth,
80+
provider: externs.AuthProvider,
81+
authType: AuthEventType,
82+
eventId?: string
83+
): Promise<AuthPopup>;
84+
}

0 commit comments

Comments
 (0)