Skip to content

Commit b04f040

Browse files
authored
Added Firebase App Check support to Firebase Auth. (#7191)
1 parent fb6df83 commit b04f040

File tree

15 files changed

+296
-51
lines changed

15 files changed

+296
-51
lines changed

.changeset/brave-ducks-relax.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/auth': minor
3+
'@firebase/auth-compat': minor
4+
---
5+
6+
[feature] Added Firebase App Check support to Firebase Auth.

packages/auth-compat/src/auth.test.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import sinonChai from 'sinon-chai';
2424
import { Auth } from './auth';
2525
import { CompatPopupRedirectResolver } from './popup_redirect';
2626
import * as platform from './platform';
27-
import { FAKE_HEARTBEAT_CONTROLLER_PROVIDER } from '../test/helpers/helpers';
27+
import {
28+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
29+
FAKE_HEARTBEAT_CONTROLLER_PROVIDER
30+
} from '../test/helpers/helpers';
2831

2932
use(sinonChai);
3033

@@ -45,6 +48,7 @@ describe('auth compat', () => {
4548
underlyingAuth = new exp.AuthImpl(
4649
app,
4750
FAKE_HEARTBEAT_CONTROLLER_PROVIDER,
51+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
4852
{
4953
apiKey: 'api-key'
5054
} as exp.ConfigInternal

packages/auth-compat/src/popup_redirect.test.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import * as exp from '@firebase/auth/internal';
2222
import * as platform from './platform';
2323
import { CompatPopupRedirectResolver } from './popup_redirect';
2424
import { FirebaseApp } from '@firebase/app-compat';
25-
import { FAKE_HEARTBEAT_CONTROLLER_PROVIDER } from '../test/helpers/helpers';
25+
import {
26+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
27+
FAKE_HEARTBEAT_CONTROLLER_PROVIDER
28+
} from '../test/helpers/helpers';
2629

2730
use(sinonChai);
2831

@@ -42,9 +45,14 @@ describe('popup_redirect/CompatPopupRedirectResolver', () => {
4245
beforeEach(() => {
4346
compatResolver = new CompatPopupRedirectResolver();
4447
const app = { options: { apiKey: 'api-key' } } as FirebaseApp;
45-
auth = new exp.AuthImpl(app, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, {
46-
apiKey: 'api-key'
47-
} as exp.ConfigInternal);
48+
auth = new exp.AuthImpl(
49+
app,
50+
FAKE_HEARTBEAT_CONTROLLER_PROVIDER,
51+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
52+
{
53+
apiKey: 'api-key'
54+
} as exp.ConfigInternal
55+
);
4856
});
4957

5058
afterEach(() => {

packages/auth-compat/test/helpers/helpers.ts

+7
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ export const FAKE_HEARTBEAT_CONTROLLER_PROVIDER = {
3434
}
3535
} as unknown as Provider<'heartbeat'>;
3636

37+
// App Check is fully tested in core auth impl
38+
export const FAKE_APP_CHECK_CONTROLLER_PROVIDER = {
39+
getImmediate(): undefined {
40+
return undefined;
41+
}
42+
} as unknown as Provider<'app-check-internal'>;
43+
3744
export function initializeTestInstance(): void {
3845
firebase.initializeApp(getAppConfig());
3946
const stub = stubConsoleToSilenceEmulatorWarnings();

packages/auth/src/api/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export const enum HttpHeader {
4242
X_FIREBASE_LOCALE = 'X-Firebase-Locale',
4343
X_CLIENT_VERSION = 'X-Client-Version',
4444
X_FIREBASE_GMPID = 'X-Firebase-gmpid',
45-
X_FIREBASE_CLIENT = 'X-Firebase-Client'
45+
X_FIREBASE_CLIENT = 'X-Firebase-Client',
46+
X_FIREBASE_APP_CHECK = 'X-Firebase-AppCheck'
4647
}
4748

4849
export const enum Endpoint {

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

+32
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import { FirebaseApp } from '@firebase/app';
2424
import { FirebaseError } from '@firebase/util';
2525

2626
import {
27+
FAKE_APP_CHECK_CONTROLLER,
28+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
2729
FAKE_HEARTBEAT_CONTROLLER,
2830
FAKE_HEARTBEAT_CONTROLLER_PROVIDER,
2931
testAuth,
@@ -62,6 +64,7 @@ describe('core/auth/auth_impl', () => {
6264
const authImpl = new AuthImpl(
6365
FAKE_APP,
6466
FAKE_HEARTBEAT_CONTROLLER_PROVIDER,
67+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
6568
{
6669
apiKey: FAKE_APP.options.apiKey!,
6770
apiHost: DefaultConfig.API_HOST,
@@ -582,6 +585,7 @@ describe('core/auth/auth_impl', () => {
582585
const authImpl = new AuthImpl(
583586
FAKE_APP,
584587
FAKE_HEARTBEAT_CONTROLLER_PROVIDER,
588+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
585589
{
586590
apiKey: FAKE_APP.options.apiKey!,
587591
apiHost: DefaultConfig.API_HOST,
@@ -656,5 +660,33 @@ describe('core/auth/auth_impl', () => {
656660
'X-Client-Version': 'v'
657661
});
658662
});
663+
664+
it('adds the App Check token if available', async () => {
665+
sinon
666+
.stub(FAKE_APP_CHECK_CONTROLLER, 'getToken')
667+
.returns(Promise.resolve({ token: 'fake-token' }));
668+
expect(await auth._getAdditionalHeaders()).to.eql({
669+
'X-Client-Version': 'v',
670+
'X-Firebase-AppCheck': 'fake-token'
671+
});
672+
});
673+
674+
it('does not add the App Check token if none returned', async () => {
675+
sinon
676+
.stub(FAKE_APP_CHECK_CONTROLLER, 'getToken')
677+
.returns(Promise.resolve({ token: '' }));
678+
expect(await auth._getAdditionalHeaders()).to.eql({
679+
'X-Client-Version': 'v'
680+
});
681+
});
682+
683+
it('does not add the App Check token if controller unavailable', async () => {
684+
sinon
685+
.stub(FAKE_APP_CHECK_CONTROLLER, 'getToken')
686+
.returns(undefined as any);
687+
expect(await auth._getAdditionalHeaders()).to.eql({
688+
'X-Client-Version': 'v'
689+
});
690+
});
659691
});
660692
});

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

+26
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import { _FirebaseService, FirebaseApp } from '@firebase/app';
1919
import { Provider } from '@firebase/component';
20+
import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types';
2021
import {
2122
Auth,
2223
AuthErrorMap,
@@ -62,6 +63,7 @@ import { _getUserLanguage } from '../util/navigator';
6263
import { _getClientVersion } from '../util/version';
6364
import { HttpHeader } from '../../api';
6465
import { AuthMiddlewareQueue } from './middleware';
66+
import { _logWarn } from '../util/log';
6567

6668
interface AsyncAction {
6769
(): Promise<void>;
@@ -108,6 +110,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
108110
constructor(
109111
public readonly app: FirebaseApp,
110112
private readonly heartbeatServiceProvider: Provider<'heartbeat'>,
113+
private readonly appCheckServiceProvider: Provider<AppCheckInternalComponentName>,
111114
public readonly config: ConfigInternal
112115
) {
113116
this.name = app.name;
@@ -645,8 +648,31 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
645648
if (heartbeatsHeader) {
646649
headers[HttpHeader.X_FIREBASE_CLIENT] = heartbeatsHeader;
647650
}
651+
652+
// If the App Check service exists, add the App Check token in the headers
653+
const appCheckToken = await this._getAppCheckToken();
654+
if (appCheckToken) {
655+
headers[HttpHeader.X_FIREBASE_APP_CHECK] = appCheckToken;
656+
}
657+
648658
return headers;
649659
}
660+
661+
async _getAppCheckToken(): Promise<string | undefined> {
662+
const appCheckTokenResult = await this.appCheckServiceProvider
663+
.getImmediate({ optional: true })
664+
?.getToken();
665+
if (appCheckTokenResult?.error) {
666+
// Context: appCheck.getToken() will never throw even if an error happened.
667+
// In the error case, a dummy token will be returned along with an error field describing
668+
// the error. In general, we shouldn't care about the error condition and just use
669+
// the token (actual or dummy) to send requests.
670+
_logWarn(
671+
`Error while retrieving App Check token: ${appCheckTokenResult.error}`
672+
);
673+
}
674+
return appCheckTokenResult?.token;
675+
}
650676
}
651677

652678
/**

packages/auth/src/core/auth/register.ts

+29-27
Original file line numberDiff line numberDiff line change
@@ -63,36 +63,38 @@ export function registerAuth(clientPlatform: ClientPlatform): void {
6363
const app = container.getProvider('app').getImmediate()!;
6464
const heartbeatServiceProvider =
6565
container.getProvider<'heartbeat'>('heartbeat');
66+
const appCheckServiceProvider =
67+
container.getProvider<'app-check-internal'>('app-check-internal');
6668
const { apiKey, authDomain } = app.options;
67-
return ((app, heartbeatServiceProvider) => {
68-
_assert(
69-
apiKey && !apiKey.includes(':'),
70-
AuthErrorCode.INVALID_API_KEY,
71-
{ appName: app.name }
72-
);
73-
// Auth domain is optional if IdP sign in isn't being used
74-
_assert(!authDomain?.includes(':'), AuthErrorCode.ARGUMENT_ERROR, {
75-
appName: app.name
76-
});
77-
const config: ConfigInternal = {
78-
apiKey,
79-
authDomain,
80-
clientPlatform,
81-
apiHost: DefaultConfig.API_HOST,
82-
tokenApiHost: DefaultConfig.TOKEN_API_HOST,
83-
apiScheme: DefaultConfig.API_SCHEME,
84-
sdkClientVersion: _getClientVersion(clientPlatform)
85-
};
8669

87-
const authInstance = new AuthImpl(
88-
app,
89-
heartbeatServiceProvider,
90-
config
91-
);
92-
_initializeAuthInstance(authInstance, deps);
70+
_assert(
71+
apiKey && !apiKey.includes(':'),
72+
AuthErrorCode.INVALID_API_KEY,
73+
{ appName: app.name }
74+
);
75+
// Auth domain is optional if IdP sign in isn't being used
76+
_assert(!authDomain?.includes(':'), AuthErrorCode.ARGUMENT_ERROR, {
77+
appName: app.name
78+
});
79+
const config: ConfigInternal = {
80+
apiKey,
81+
authDomain,
82+
clientPlatform,
83+
apiHost: DefaultConfig.API_HOST,
84+
tokenApiHost: DefaultConfig.TOKEN_API_HOST,
85+
apiScheme: DefaultConfig.API_SCHEME,
86+
sdkClientVersion: _getClientVersion(clientPlatform)
87+
};
88+
89+
const authInstance = new AuthImpl(
90+
app,
91+
heartbeatServiceProvider,
92+
appCheckServiceProvider,
93+
config
94+
);
95+
_initializeAuthInstance(authInstance, deps);
9396

94-
return authInstance;
95-
})(app, heartbeatServiceProvider);
97+
return authInstance;
9698
},
9799
ComponentType.PUBLIC
98100
)

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

+20-3
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 = encodeURIComponent('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,17 @@ 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+
? `#${FIREBASE_APP_CHECK_FRAGMENT_ID}=${encodeURIComponent(appCheckToken)}`
122+
: '';
123+
124+
// Start at index 1 to skip the leading '&' in the query string
125+
return `${getHandlerBase(auth)}?${querystring(paramsDict).slice(
126+
1
127+
)}${appCheckTokenFragment}`;
111128
}
112129

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

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

+6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export function _logDebug(msg: string, ...args: string[]): void {
3737
}
3838
}
3939

40+
export function _logWarn(msg: string, ...args: string[]): void {
41+
if (logClient.logLevel <= LogLevel.WARN) {
42+
logClient.warn(`Auth (${SDK_VERSION}): ${msg}`, ...args);
43+
}
44+
}
45+
4046
export function _logError(msg: string, ...args: string[]): void {
4147
if (logClient.logLevel <= LogLevel.ERROR) {
4248
logClient.error(`Auth (${SDK_VERSION}): ${msg}`, ...args);

packages/auth/src/model/auth.ts

+1
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/auth.test.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
import { OperationType } from '../model/enums';
3030

3131
import {
32+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
3233
FAKE_HEARTBEAT_CONTROLLER_PROVIDER,
3334
testAuth,
3435
testUser
@@ -73,6 +74,7 @@ describe('core/auth/auth_impl', () => {
7374
const authImpl = new AuthImpl(
7475
FAKE_APP,
7576
FAKE_HEARTBEAT_CONTROLLER_PROVIDER,
77+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
7678
{
7779
apiKey: FAKE_APP.options.apiKey!,
7880
apiHost: DefaultConfig.API_HOST,
@@ -141,15 +143,20 @@ describe('core/auth/initializeAuth', () => {
141143
authDomain = FAKE_APP.options.authDomain,
142144
blockMiddleware = false
143145
): Promise<Auth> {
144-
const auth = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, {
145-
apiKey: FAKE_APP.options.apiKey!,
146-
apiHost: DefaultConfig.API_HOST,
147-
apiScheme: DefaultConfig.API_SCHEME,
148-
tokenApiHost: DefaultConfig.TOKEN_API_HOST,
149-
authDomain,
150-
clientPlatform: ClientPlatform.BROWSER,
151-
sdkClientVersion: _getClientVersion(ClientPlatform.BROWSER)
152-
});
146+
const auth = new AuthImpl(
147+
FAKE_APP,
148+
FAKE_HEARTBEAT_CONTROLLER_PROVIDER,
149+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
150+
{
151+
apiKey: FAKE_APP.options.apiKey!,
152+
apiHost: DefaultConfig.API_HOST,
153+
apiScheme: DefaultConfig.API_SCHEME,
154+
tokenApiHost: DefaultConfig.TOKEN_API_HOST,
155+
authDomain,
156+
clientPlatform: ClientPlatform.BROWSER,
157+
sdkClientVersion: _getClientVersion(ClientPlatform.BROWSER)
158+
}
159+
);
153160

154161
_initializeAuthInstance(auth, {
155162
persistence,
@@ -378,6 +385,7 @@ describe('core/auth/initializeAuth', () => {
378385
const auth = new AuthImpl(
379386
FAKE_APP,
380387
FAKE_HEARTBEAT_CONTROLLER_PROVIDER,
388+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
381389
{
382390
apiKey: FAKE_APP.options.apiKey!,
383391
apiHost: DefaultConfig.API_HOST,

0 commit comments

Comments
 (0)