Skip to content

Commit 1f92c09

Browse files
committed
Add App Check token to widget url fragment
1 parent ba68841 commit 1f92c09

File tree

5 files changed

+147
-14
lines changed

5 files changed

+147
-14
lines changed

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -649,20 +649,24 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
649649
}
650650

651651
// If the App Check service exists, add the App Check token in the headers
652-
const appCheckTokenResult = await this.appCheckServiceProvider
653-
.getImmediate({ optional: true })
654-
?.getToken();
652+
const appCheckToken = await this._getAppCheckToken();
655653
// TODO: What do we want to do if there is an error getting the token?
656654
// Context: appCheck.getToken() will never throw even if an error happened.
657655
// In the error case, a dummy token will be returned along with an error field describing
658656
// the error. In general, we shouldn't care about the error condition and just use
659657
// the token (actual or dummy) to send requests.
660-
if (appCheckTokenResult?.token) {
661-
headers[HttpHeader.X_FIREBASE_APP_CHECK] = appCheckTokenResult.token;
658+
if (appCheckToken) {
659+
headers[HttpHeader.X_FIREBASE_APP_CHECK] = appCheckToken;
662660
}
663661

664662
return headers;
665663
}
664+
async _getAppCheckToken(): Promise<string | undefined> {
665+
const appCheckTokenResult = await this.appCheckServiceProvider
666+
.getImmediate({ optional: true })
667+
?.getToken();
668+
return appCheckTokenResult?.token;
669+
}
666670
}
667671

668672
/**

packages/auth/src/core/util/handler.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ const WIDGET_PATH = '__/auth/handler';
4040
*/
4141
const EMULATOR_WIDGET_PATH = 'emulator/auth/handler';
4242

43+
/**
44+
* Fragment name for the App Check token that gets passed to the widget
45+
*
46+
* @internal
47+
*/
48+
const FIREBASE_APP_CHECK_FRAGMENT_ID = 'fac';
49+
4350
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
4451
type WidgetParams = {
4552
apiKey: ApiKey;
@@ -54,14 +61,14 @@ type WidgetParams = {
5461
tid?: string;
5562
} & { [key: string]: string | undefined };
5663

57-
export function _getRedirectUrl(
64+
export async function _getRedirectUrl(
5865
auth: AuthInternal,
5966
provider: AuthProvider,
6067
authType: AuthEventType,
6168
redirectUrl?: string,
6269
eventId?: string,
6370
additionalParams?: Record<string, string>
64-
): string {
71+
): Promise<string> {
6572
_assert(auth.config.authDomain, auth, AuthErrorCode.MISSING_AUTH_DOMAIN);
6673
_assert(auth.config.apiKey, auth, AuthErrorCode.INVALID_API_KEY);
6774

@@ -107,7 +114,18 @@ export function _getRedirectUrl(
107114
delete paramsDict[key];
108115
}
109116
}
110-
return `${getHandlerBase(auth)}?${querystring(paramsDict).slice(1)}`;
117+
118+
// Sets the App Check token to pass to the widget
119+
const appCheckToken = await auth._getAppCheckToken();
120+
const appCheckTokenFragment = appCheckToken
121+
? encodeURIComponent(FIREBASE_APP_CHECK_FRAGMENT_ID) +
122+
'=' +
123+
encodeURIComponent(appCheckToken)
124+
: '';
125+
126+
return `${getHandlerBase(auth)}?${querystring(paramsDict).slice(
127+
1
128+
)}#${appCheckTokenFragment}`;
111129
}
112130

113131
function getHandlerBase({ config }: AuthInternal): string {

packages/auth/src/model/auth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export interface AuthInternal extends Auth {
8282
_logFramework(framework: string): void;
8383
_getFrameworks(): readonly string[];
8484
_getAdditionalHeaders(): Promise<Record<string, string>>;
85+
_getAppCheckToken(): Promise<string | undefined>;
8586

8687
readonly name: AppName;
8788
readonly config: ConfigInternal;

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

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import {
3030
TEST_AUTH_DOMAIN,
3131
TEST_KEY,
3232
testAuth,
33-
TestAuth
33+
TestAuth,
34+
FAKE_APP_CHECK_CONTROLLER
3435
} from '../../test/helpers/mock_auth';
3536
import { AuthEventManager } from '../core/auth/auth_event_manager';
3637
import { OAuthProvider } from '../core/providers/oauth';
@@ -125,6 +126,48 @@ describe('platform_browser/popup_redirect', () => {
125126
);
126127
});
127128

129+
it('includes the App Check token in the url fragment if present', async () => {
130+
await resolver._initialize(auth);
131+
sinon
132+
.stub(FAKE_APP_CHECK_CONTROLLER, 'getToken')
133+
.returns(Promise.resolve({ token: 'fake-token' }));
134+
135+
await resolver._openPopup(auth, provider, event);
136+
137+
const matches = (popupUrl as string).match(/.*?#(.*)/);
138+
expect(matches).not.to.be.null;
139+
const fragment = matches![1];
140+
expect(fragment).to.include('fac=fake-token');
141+
});
142+
143+
it('does not add the App Check token in the url fragment if none returned', async () => {
144+
await resolver._initialize(auth);
145+
sinon
146+
.stub(FAKE_APP_CHECK_CONTROLLER, 'getToken')
147+
.returns(Promise.resolve({ token: '' }));
148+
149+
await resolver._openPopup(auth, provider, event);
150+
151+
const matches = (popupUrl as string).match(/.*?#(.*)/);
152+
expect(matches).not.to.be.null;
153+
const fragment = matches![1];
154+
expect(fragment).not.to.include('fac');
155+
});
156+
157+
it('does not add the App Check token in the url fragment if controller unavailable', async () => {
158+
await resolver._initialize(auth);
159+
sinon
160+
.stub(FAKE_APP_CHECK_CONTROLLER, 'getToken')
161+
.returns(undefined as any);
162+
163+
await resolver._openPopup(auth, provider, event);
164+
165+
const matches = (popupUrl as string).match(/.*?#(.*)/);
166+
expect(matches).not.to.be.null;
167+
const fragment = matches![1];
168+
expect(fragment).not.to.include('fac');
169+
});
170+
128171
it('throws an error if apiKey is unspecified', async () => {
129172
delete (auth.config as Partial<Config>).apiKey;
130173
await resolver._initialize(auth);
@@ -157,8 +200,10 @@ describe('platform_browser/popup_redirect', () => {
157200
// eslint-disable-next-line @typescript-eslint/no-floating-promises
158201
resolver._openRedirect(auth, provider, event);
159202

160-
// Delay one tick
161-
await Promise.resolve();
203+
// Wait a bit so the _openRedirect() call completes
204+
await new Promise((resolve): void => {
205+
setTimeout(resolve, 100);
206+
});
162207

163208
expect(newWindowLocation).to.include(
164209
`https://${TEST_AUTH_DOMAIN}/__/auth/handler`
@@ -177,6 +222,66 @@ describe('platform_browser/popup_redirect', () => {
177222
);
178223
});
179224

225+
it('includes the App Check token in the url fragment if present', async () => {
226+
sinon
227+
.stub(FAKE_APP_CHECK_CONTROLLER, 'getToken')
228+
.returns(Promise.resolve({ token: 'fake-token' }));
229+
230+
// This promise will never resolve on purpose
231+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
232+
resolver._openRedirect(auth, provider, event);
233+
234+
// Wait a bit so the _openRedirect() call completes
235+
await new Promise((resolve): void => {
236+
setTimeout(resolve, 100);
237+
});
238+
239+
const matches = newWindowLocation.match(/.*?#(.*)/);
240+
expect(matches).not.to.be.null;
241+
const fragment = matches![1];
242+
expect(fragment).to.include('fac=fake-token');
243+
});
244+
245+
it('does not add the App Check token in the url fragment if none returned', async () => {
246+
sinon
247+
.stub(FAKE_APP_CHECK_CONTROLLER, 'getToken')
248+
.returns(Promise.resolve({ token: '' }));
249+
250+
// This promise will never resolve on purpose
251+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
252+
resolver._openRedirect(auth, provider, event);
253+
254+
// Wait a bit so the _openRedirect() call completes
255+
await new Promise((resolve): void => {
256+
setTimeout(resolve, 100);
257+
});
258+
259+
const matches = newWindowLocation.match(/.*?#(.*)/);
260+
expect(matches).not.to.be.null;
261+
const fragment = matches![1];
262+
expect(fragment).not.to.include('fac');
263+
});
264+
265+
it('does not add the App Check token in the url fragment if controller unavailable', async () => {
266+
sinon
267+
.stub(FAKE_APP_CHECK_CONTROLLER, 'getToken')
268+
.returns(undefined as any);
269+
270+
// This promise will never resolve on purpose
271+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
272+
resolver._openRedirect(auth, provider, event);
273+
274+
// Wait a bit so the _openRedirect() call completes
275+
await new Promise((resolve): void => {
276+
setTimeout(resolve, 100);
277+
});
278+
279+
const matches = newWindowLocation.match(/.*?#(.*)/);
280+
expect(matches).not.to.be.null;
281+
const fragment = matches![1];
282+
expect(fragment).not.to.include('fac');
283+
});
284+
180285
it('throws an error if authDomain is unspecified', async () => {
181286
delete auth.config.authDomain;
182287

packages/auth/src/platform_browser/popup_redirect.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ class BrowserPopupRedirectResolver implements PopupRedirectResolverInternal {
7575
'_initialize() not called before _openPopup()'
7676
);
7777

78-
const url = _getRedirectUrl(
78+
const url = await _getRedirectUrl(
7979
auth,
8080
provider,
8181
authType,
@@ -92,9 +92,14 @@ class BrowserPopupRedirectResolver implements PopupRedirectResolverInternal {
9292
eventId?: string
9393
): Promise<never> {
9494
await this._originValidation(auth);
95-
_setWindowLocation(
96-
_getRedirectUrl(auth, provider, authType, _getCurrentUrl(), eventId)
95+
const url = await _getRedirectUrl(
96+
auth,
97+
provider,
98+
authType,
99+
_getCurrentUrl(),
100+
eventId
97101
);
102+
_setWindowLocation(url);
98103
return new Promise(() => {});
99104
}
100105

0 commit comments

Comments
 (0)