Skip to content

Commit ba68841

Browse files
committed
Add App Check token to headers of Auth requests
1 parent f8ddadb commit ba68841

File tree

10 files changed

+141
-42
lines changed

10 files changed

+141
-42
lines changed

.changeset/brave-ducks-relax.md

Lines changed: 6 additions & 0 deletions
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+
App Check <> Auth integration

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

Lines changed: 5 additions & 1 deletion
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

Lines changed: 12 additions & 4 deletions
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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 32 additions & 0 deletions
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

Lines changed: 16 additions & 0 deletions
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,
@@ -108,6 +109,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
108109
constructor(
109110
public readonly app: FirebaseApp,
110111
private readonly heartbeatServiceProvider: Provider<'heartbeat'>,
112+
private readonly appCheckServiceProvider: Provider<AppCheckInternalComponentName>,
111113
public readonly config: ConfigInternal
112114
) {
113115
this.name = app.name;
@@ -645,6 +647,20 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
645647
if (heartbeatsHeader) {
646648
headers[HttpHeader.X_FIREBASE_CLIENT] = heartbeatsHeader;
647649
}
650+
651+
// 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();
655+
// TODO: What do we want to do if there is an error getting the token?
656+
// Context: appCheck.getToken() will never throw even if an error happened.
657+
// In the error case, a dummy token will be returned along with an error field describing
658+
// the error. In general, we shouldn't care about the error condition and just use
659+
// the token (actual or dummy) to send requests.
660+
if (appCheckTokenResult?.token) {
661+
headers[HttpHeader.X_FIREBASE_APP_CHECK] = appCheckTokenResult.token;
662+
}
663+
648664
return headers;
649665
}
650666
}

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

Lines changed: 29 additions & 27 deletions
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/platform_browser/auth.test.ts

Lines changed: 17 additions & 9 deletions
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,

packages/auth/test/helpers/mock_auth.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import { FirebaseApp } from '@firebase/app';
1919
import { Provider } from '@firebase/component';
20+
import { AppCheckTokenResult } from '@firebase/app-check-interop-types';
2021
import { PopupRedirectResolver } from '../../src/model/public_types';
2122
import { debugErrorMap } from '../../src';
2223

@@ -55,6 +56,19 @@ export const FAKE_HEARTBEAT_CONTROLLER_PROVIDER: Provider<'heartbeat'> = {
5556
}
5657
} as unknown as Provider<'heartbeat'>;
5758

59+
export const FAKE_APP_CHECK_CONTROLLER = {
60+
getToken: async () => {
61+
return { token: '' } as AppCheckTokenResult;
62+
}
63+
};
64+
65+
export const FAKE_APP_CHECK_CONTROLLER_PROVIDER: Provider<'app-check-internal'> =
66+
{
67+
getImmediate(): typeof FAKE_APP_CHECK_CONTROLLER {
68+
return FAKE_APP_CHECK_CONTROLLER;
69+
}
70+
} as unknown as Provider<'app-check-internal'>;
71+
5872
export class MockPersistenceLayer extends InMemoryPersistence {
5973
lastObjectSet: PersistedBlob | null = null;
6074

@@ -77,6 +91,7 @@ export async function testAuth(
7791
const auth: TestAuth = new AuthImpl(
7892
FAKE_APP,
7993
FAKE_HEARTBEAT_CONTROLLER_PROVIDER,
94+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
8095
{
8196
apiKey: TEST_KEY,
8297
authDomain: TEST_AUTH_DOMAIN,

0 commit comments

Comments
 (0)