Skip to content

Commit 2629b46

Browse files
committed
Auth heartbeat impl
1 parent c462daa commit 2629b46

File tree

10 files changed

+99
-19
lines changed

10 files changed

+99
-19
lines changed

packages/auth/src/api/authentication/token.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import { expect, use } from 'chai';
1919
import chaiAsPromised from 'chai-as-promised';
20+
import * as sinon from 'sinon';
2021

2122
import { FirebaseError, getUA, querystringDecode } from '@firebase/util';
2223

@@ -41,7 +42,10 @@ describe('requestStsToken', () => {
4142
fetch.setUp();
4243
});
4344

44-
afterEach(fetch.tearDown);
45+
afterEach(() => {
46+
fetch.tearDown();
47+
sinon.restore();
48+
});
4549

4650
it('should POST to the correct endpoint', async () => {
4751
const mock = fetch.mock(endpoint, {
@@ -68,6 +72,21 @@ describe('requestStsToken', () => {
6872
);
6973
});
7074

75+
it('should set (or not set) heartbeat correctly', async () => {
76+
const mock = fetch.mock(endpoint, {
77+
'access_token': 'new-access-token',
78+
'expires_in': '3600',
79+
'refresh_token': 'new-refresh-token'
80+
});
81+
await requestStsToken(auth, 'old-refresh-token');
82+
sinon.stub(auth, '_getHeartbeatHeader').returns(Promise.resolve('heartbeat'));
83+
await requestStsToken(auth, 'old-refresh-token');
84+
85+
// First call won't have the header, second call will.
86+
expect(mock.calls[0].headers!.has(HttpHeader.X_FIREBASE_CLIENT)).to.be.false;
87+
expect(mock.calls[1].headers!.get(HttpHeader.X_FIREBASE_CLIENT)).to.eq('heartbeat');
88+
});
89+
7190
it('should set the framework in clientVersion if logged', async () => {
7291
const mock = fetch.mock(endpoint, {
7392
'access_token': 'new-access-token',

packages/auth/src/api/authentication/token.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import { querystring } from '@firebase/util';
2222
import {
2323
_getFinalTarget,
2424
_performFetchWithErrorHandling,
25-
HttpMethod
25+
HttpMethod,
26+
HttpHeader
2627
} from '../index';
2728
import { FetchProvider } from '../../core/util/fetch_provider';
2829
import { Auth } from '../../model/public_types';
@@ -52,7 +53,7 @@ export async function requestStsToken(
5253
const response = await _performFetchWithErrorHandling<RequestStsTokenServerResponse>(
5354
auth,
5455
{},
55-
() => {
56+
async () => {
5657
const body = querystring({
5758
'grant_type': 'refresh_token',
5859
'refresh_token': refreshToken
@@ -65,12 +66,18 @@ export async function requestStsToken(
6566
`key=${apiKey}`
6667
);
6768

69+
const heartbeat = await (auth as AuthInternal)._getHeartbeatHeader();
70+
const headers: HeadersInit = {
71+
[HttpHeader.X_CLIENT_VERSION]: (auth as AuthInternal)._getSdkClientVersion(),
72+
[HttpHeader.CONTENT_TYPE]: 'application/x-www-form-urlencoded',
73+
};
74+
if (heartbeat) {
75+
headers[HttpHeader.X_FIREBASE_CLIENT] = heartbeat;
76+
}
77+
6878
return FetchProvider.fetch()(url, {
6979
method: HttpMethod.POST,
70-
headers: {
71-
'X-Client-Version': (auth as AuthInternal)._getSdkClientVersion(),
72-
'Content-Type': 'application/x-www-form-urlencoded'
73-
},
80+
headers,
7481
body
7582
});
7683
}

packages/auth/src/api/index.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ describe('api/_performApiRequest', () => {
5959
auth = await testAuth();
6060
});
6161

62+
afterEach(() => {
63+
sinon.restore();
64+
});
65+
6266
context('with regular requests', () => {
6367
beforeEach(mockFetch.setUp);
6468
afterEach(mockFetch.tearDown);
@@ -94,6 +98,27 @@ describe('api/_performApiRequest', () => {
9498
);
9599
});
96100

101+
it('should set the heartbeat header only if available', async () => {
102+
const mock = mockEndpoint(Endpoint.SIGN_UP, serverResponse);
103+
await _performApiRequest(auth, HttpMethod.POST, Endpoint.SIGN_UP, request);
104+
sinon.stub(auth, '_getHeartbeatHeader').returns(Promise.resolve('heartbeat'));
105+
await _performApiRequest(auth, HttpMethod.POST, Endpoint.SIGN_UP, request);
106+
107+
// First call won't have the header, second call will.
108+
expect(mock.calls[0].headers!.has(HttpHeader.X_FIREBASE_CLIENT)).to.be.false;
109+
expect(mock.calls[1].headers!.get(HttpHeader.X_FIREBASE_CLIENT)).to.eq('heartbeat');
110+
});
111+
112+
it('should not set the heartbeat header on config request', async () => {
113+
const mock = mockEndpoint(Endpoint.GET_PROJECT_CONFIG, serverResponse);
114+
await _performApiRequest(auth, HttpMethod.POST, Endpoint.GET_PROJECT_CONFIG, request);
115+
sinon.stub(auth, '_getHeartbeatHeader').returns(Promise.resolve('heartbeat'));
116+
await _performApiRequest(auth, HttpMethod.POST, Endpoint.GET_PROJECT_CONFIG, request);
117+
// First call won't have the header, second call will.
118+
expect(mock.calls[0].headers!.has(HttpHeader.X_FIREBASE_CLIENT)).to.be.false;
119+
expect(mock.calls[1].headers!.has(HttpHeader.X_FIREBASE_CLIENT)).to.be.false;
120+
});
121+
97122
it('should set the framework in clientVersion if logged', async () => {
98123
auth._logFramework('Mythical');
99124
const mock = mockEndpoint(Endpoint.SIGN_UP, serverResponse);

packages/auth/src/api/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export const enum HttpMethod {
3636
export const enum HttpHeader {
3737
CONTENT_TYPE = 'Content-Type',
3838
X_FIREBASE_LOCALE = 'X-Firebase-Locale',
39-
X_CLIENT_VERSION = 'X-Client-Version'
39+
X_CLIENT_VERSION = 'X-Client-Version',
40+
X_FIREBASE_CLIENT = 'X-Firebase-Client'
4041
}
4142

4243
export const enum Endpoint {
@@ -84,7 +85,8 @@ export async function _performApiRequest<T, V>(
8485
request?: T,
8586
customErrorMap: Partial<ServerErrorMap<ServerError>> = {}
8687
): Promise<V> {
87-
return _performFetchWithErrorHandling(auth, customErrorMap, () => {
88+
const authInternal = (auth as AuthInternal);
89+
return _performFetchWithErrorHandling(auth, customErrorMap, async () => {
8890
let body = {};
8991
let params = {};
9092
if (request) {
@@ -106,13 +108,20 @@ export async function _performApiRequest<T, V>(
106108
headers.set(HttpHeader.CONTENT_TYPE, 'application/json');
107109
headers.set(
108110
HttpHeader.X_CLIENT_VERSION,
109-
(auth as AuthInternal)._getSdkClientVersion()
111+
authInternal._getSdkClientVersion()
110112
);
111113

112114
if (auth.languageCode) {
113115
headers.set(HttpHeader.X_FIREBASE_LOCALE, auth.languageCode);
114116
}
115117

118+
if (!path.includes(Endpoint.GET_PROJECT_CONFIG)) {
119+
const heartbeat = await authInternal._getHeartbeatHeader();
120+
if (heartbeat) {
121+
headers.set(HttpHeader.X_FIREBASE_CLIENT, heartbeat);
122+
}
123+
}
124+
116125
return FetchProvider.fetch()(
117126
_getFinalTarget(auth, auth.config.apiHost, path, query),
118127
{

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,17 @@ const FAKE_APP: FirebaseApp = {
4747
automaticDataCollectionEnabled: false
4848
};
4949

50+
const FAKE_HEARTBEAT_CONTROLLER = {
51+
getHeartbeatsHeader: async () => '',
52+
};
53+
5054
describe('core/auth/auth_impl', () => {
5155
let auth: AuthInternal;
5256
let persistenceStub: sinon.SinonStubbedInstance<PersistenceInternal>;
5357

5458
beforeEach(async () => {
5559
persistenceStub = sinon.stub(_getInstance(inMemoryPersistence));
56-
const authImpl = new AuthImpl(FAKE_APP, {
60+
const authImpl = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER, {
5761
apiKey: FAKE_APP.options.apiKey!,
5862
apiHost: DefaultConfig.API_HOST,
5963
apiScheme: DefaultConfig.API_SCHEME,
@@ -431,7 +435,7 @@ describe('core/auth/auth_impl', () => {
431435
});
432436

433437
it('prevents initialization from completing', async () => {
434-
const authImpl = new AuthImpl(FAKE_APP, {
438+
const authImpl = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER, {
435439
apiKey: FAKE_APP.options.apiKey!,
436440
apiHost: DefaultConfig.API_HOST,
437441
apiScheme: DefaultConfig.API_SCHEME,

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ import { _getInstance } from '../util/instantiator';
5959
import { _getUserLanguage } from '../util/navigator';
6060
import { _getClientVersion } from '../util/version';
6161

62+
interface HeartbeatService {
63+
getHeartbeatsHeader(): Promise<string>;
64+
}
65+
6266
interface AsyncAction {
6367
(): Promise<void>;
6468
}
@@ -102,7 +106,8 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
102106

103107
constructor(
104108
public readonly app: FirebaseApp,
105-
public readonly config: ConfigInternal
109+
private readonly heartbeatController: HeartbeatService,
110+
public readonly config: ConfigInternal,
106111
) {
107112
this.name = app.name;
108113
this.clientVersion = config.sdkClientVersion;
@@ -577,6 +582,9 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
577582
_getSdkClientVersion(): string {
578583
return this.clientVersion;
579584
}
585+
_getHeartbeatHeader(): Promise<string> {
586+
return this.heartbeatController.getHeartbeatsHeader();
587+
}
580588
}
581589

582590
/**

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ export function registerAuth(clientPlatform: ClientPlatform): void {
6161
_ComponentName.AUTH,
6262
(container, { options: deps }: { options?: Dependencies }) => {
6363
const app = container.getProvider('app').getImmediate()!;
64+
const heartbeatController = container.getProvider('heartbeat').getImmediate()!;
6465
const { apiKey, authDomain } = app.options;
65-
return (app => {
66+
return ((app, heartbeatController) => {
6667
_assert(
6768
apiKey && !apiKey.includes(':'),
6869
AuthErrorCode.INVALID_API_KEY,
@@ -82,11 +83,11 @@ export function registerAuth(clientPlatform: ClientPlatform): void {
8283
sdkClientVersion: _getClientVersion(clientPlatform)
8384
};
8485

85-
const authInstance = new AuthImpl(app, config);
86+
const authInstance = new AuthImpl(app, heartbeatController, config);
8687
_initializeAuthInstance(authInstance, deps);
8788

8889
return authInstance;
89-
})(app);
90+
})(app, heartbeatController);
9091
},
9192
ComponentType.PUBLIC
9293
)

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
_getSdkClientVersion(): string;
85+
_getHeartbeatHeader(): Promise<string>;
8586

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

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,17 @@ const FAKE_APP: FirebaseApp = {
6060
automaticDataCollectionEnabled: false
6161
};
6262

63+
const FAKE_HEARTBEAT_CONTROLLER = {
64+
getHeartbeatsHeader: async () => '',
65+
};
66+
6367
describe('core/auth/auth_impl', () => {
6468
let auth: AuthInternal;
6569
let persistenceStub: sinon.SinonStubbedInstance<PersistenceInternal>;
6670

6771
beforeEach(async () => {
6872
persistenceStub = sinon.stub(_getInstance(inMemoryPersistence));
69-
const authImpl = new AuthImpl(FAKE_APP, {
73+
const authImpl = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER, {
7074
apiKey: FAKE_APP.options.apiKey!,
7175
apiHost: DefaultConfig.API_HOST,
7276
apiScheme: DefaultConfig.API_SCHEME,
@@ -132,7 +136,7 @@ describe('core/auth/initializeAuth', () => {
132136
popupRedirectResolver?: PopupRedirectResolver,
133137
authDomain = FAKE_APP.options.authDomain
134138
): Promise<Auth> {
135-
const auth = new AuthImpl(FAKE_APP, {
139+
const auth = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER, {
136140
apiKey: FAKE_APP.options.apiKey!,
137141
apiHost: DefaultConfig.API_HOST,
138142
apiScheme: DefaultConfig.API_SCHEME,
@@ -349,7 +353,7 @@ describe('core/auth/initializeAuth', () => {
349353

350354
// Manually initialize auth to make sure no error is thrown,
351355
// since the _initializeAuthInstance function floats
352-
const auth = new AuthImpl(FAKE_APP, {
356+
const auth = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER, {
353357
apiKey: FAKE_APP.options.apiKey!,
354358
apiHost: DefaultConfig.API_HOST,
355359
apiScheme: DefaultConfig.API_SCHEME,

packages/auth/test/helpers/mock_auth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ export async function testAuth(
6363
persistence = new MockPersistenceLayer()
6464
): Promise<TestAuth> {
6565
const auth: TestAuth = new AuthImpl(FAKE_APP, {
66+
getHeartbeatsHeader: () => Promise.resolve('')
67+
}, {
6668
apiKey: TEST_KEY,
6769
authDomain: TEST_AUTH_DOMAIN,
6870
apiHost: TEST_HOST,

0 commit comments

Comments
 (0)