Skip to content

Commit 5c01436

Browse files
authored
Store FirebaseToken to persistence - Required for BYO-CIAM (#9138)
* Maintain persistence for firebaseToken in AuthImpl object * Adding Unit Test for auth_impl firebaseToken
1 parent 15b2302 commit 5c01436

File tree

4 files changed

+203
-4
lines changed

4 files changed

+203
-4
lines changed

packages/auth/demo/src/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,7 +1526,7 @@ function onExchangeToken(event) {
15261526

15271527
exchangeCIAMToken(byoCiamInput.value)
15281528
.then(response => {
1529-
byoCiamResult.textContent = response.accessToken;
1529+
byoCiamResult.textContent = response;
15301530
console.log('Token:', response);
15311531
})
15321532
.catch(error => {
@@ -2086,7 +2086,7 @@ function initApp() {
20862086
const regionalApp = initializeApp(config, `${auth.name}-rgcip`);
20872087

20882088
regionalAuth = initializeAuth(regionalApp, {
2089-
persistence: inMemoryPersistence,
2089+
persistence: indexedDBLocalPersistence,
20902090
popupRedirectResolver: browserPopupRedirectResolver,
20912091
tenantConfig: tenantConfig
20922092
});

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

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ import { mockEndpointWithParams } from '../../../test/helpers/api/helper';
4646
import { Endpoint, RecaptchaClientType, RecaptchaVersion } from '../../api';
4747
import * as mockFetch from '../../../test/helpers/mock_fetch';
4848
import { AuthErrorCode } from '../errors';
49-
import { PasswordValidationStatus } from '../../model/public_types';
49+
import {
50+
FirebaseToken,
51+
PasswordValidationStatus
52+
} from '../../model/public_types';
5053
import { PasswordPolicyImpl } from './password_policy_impl';
54+
import { PersistenceUserManager } from '../persistence/persistence_user_manager';
5155

5256
use(sinonChai);
5357
use(chaiAsPromised);
@@ -150,6 +154,153 @@ describe('core/auth/auth_impl', () => {
150154
});
151155
});
152156

157+
describe('#updateFirebaseToken', () => {
158+
const token: FirebaseToken = {
159+
token: 'test-token',
160+
expirationTime: 123456789
161+
};
162+
163+
it('sets the field on the auth object', async () => {
164+
await auth._updateFirebaseToken(token);
165+
expect((auth as any).firebaseToken).to.eql(token);
166+
});
167+
168+
it('calls persistence._set with correct values', async () => {
169+
await auth._updateFirebaseToken(token);
170+
expect(persistenceStub._set).to.have.been.calledWith(
171+
'firebase:persistence-token:api-key:test-app', // key
172+
{
173+
token: token.token,
174+
expirationTime: token.expirationTime
175+
}
176+
);
177+
});
178+
179+
it('setting to null triggers persistence._remove', async () => {
180+
await auth._updateFirebaseToken(null);
181+
expect(persistenceStub._remove).to.have.been.calledWith(
182+
'firebase:persistence-token:api-key:test-app'
183+
);
184+
});
185+
186+
it('orders async updates correctly', async () => {
187+
const tokens: FirebaseToken[] = Array.from({ length: 5 }, (_, i) => ({
188+
token: `token-${i}`,
189+
expirationTime: Date.now() + i
190+
}));
191+
192+
persistenceStub._set.callsFake(() => {
193+
return new Promise(resolve => {
194+
setTimeout(() => resolve(), 1);
195+
});
196+
});
197+
198+
await Promise.all(tokens.map(t => auth._updateFirebaseToken(t)));
199+
200+
for (let i = 0; i < tokens.length; i++) {
201+
expect(persistenceStub._set.getCall(i)).to.have.been.calledWith(
202+
'firebase:persistence-token:api-key:test-app',
203+
{
204+
token: tokens[i].token,
205+
expirationTime: tokens[i].expirationTime
206+
}
207+
);
208+
}
209+
});
210+
211+
it('throws if persistence._set fails', async () => {
212+
persistenceStub._set.rejects(new Error('fail'));
213+
await expect(auth._updateFirebaseToken(token)).to.be.rejectedWith('fail');
214+
});
215+
216+
it('throws if persistence._remove fails', async () => {
217+
persistenceStub._remove.rejects(new Error('remove fail'));
218+
await expect(auth._updateFirebaseToken(null)).to.be.rejectedWith(
219+
'remove fail'
220+
);
221+
});
222+
});
223+
224+
describe('#_initializeWithPersistence', () => {
225+
let mockToken: FirebaseToken;
226+
let persistenceManager: any;
227+
let subscription: any;
228+
let authImpl: AuthImpl;
229+
230+
beforeEach(() => {
231+
mockToken = {
232+
token: 'test-token',
233+
expirationTime: 123456789
234+
};
235+
236+
persistenceManager = {
237+
getFirebaseToken: sinon.stub().resolves(mockToken),
238+
getCurrentUser: sinon.stub().resolves(null),
239+
setCurrentUser: sinon.stub().resolves(),
240+
removeCurrentUser: sinon.stub().resolves(),
241+
getPersistence: sinon.stub().returns('LOCAL')
242+
};
243+
244+
subscription = {
245+
next: sinon.spy()
246+
};
247+
248+
sinon.stub(PersistenceUserManager, 'create').resolves(persistenceManager);
249+
250+
authImpl = new AuthImpl(
251+
FAKE_APP,
252+
FAKE_HEARTBEAT_CONTROLLER_PROVIDER,
253+
FAKE_APP_CHECK_CONTROLLER_PROVIDER,
254+
{
255+
apiKey: FAKE_APP.options.apiKey!,
256+
apiHost: DefaultConfig.API_HOST,
257+
apiScheme: DefaultConfig.API_SCHEME,
258+
tokenApiHost: DefaultConfig.TOKEN_API_HOST,
259+
clientPlatform: ClientPlatform.BROWSER,
260+
sdkClientVersion: 'v'
261+
}
262+
);
263+
264+
(authImpl as any).firebaseTokenSubscription = subscription;
265+
});
266+
267+
afterEach(() => {
268+
sinon.restore();
269+
});
270+
271+
it('should load the firebaseToken from persistence and set it', async () => {
272+
await authImpl._initializeWithPersistence([
273+
persistenceStub as PersistenceInternal
274+
]);
275+
276+
expect(persistenceManager.getFirebaseToken).to.have.been.called;
277+
expect((authImpl as any).firebaseToken).to.eql(mockToken);
278+
expect(subscription.next).to.have.been.calledWith(mockToken);
279+
});
280+
281+
it('should set firebaseToken to null if getFirebaseToken returns undefined', async () => {
282+
persistenceManager.getFirebaseToken.resolves(undefined);
283+
284+
await authImpl._initializeWithPersistence([
285+
persistenceStub as PersistenceInternal
286+
]);
287+
288+
expect((authImpl as any).firebaseToken).to.be.null;
289+
expect(subscription.next).to.have.been.calledWith(null);
290+
});
291+
292+
it('should set firebaseToken to null if getFirebaseToken returns null', async () => {
293+
persistenceManager.getFirebaseToken.resolves(null);
294+
295+
await authImpl._initializeWithPersistence([
296+
persistenceStub as PersistenceInternal
297+
]);
298+
299+
expect((authImpl as any).firebaseToken).to.be.null;
300+
expect(subscription.next).to.have.been.calledWith(null);
301+
});
302+
});
303+
153304
describe('#signOut', () => {
154305
it('sets currentUser to null, calls remove', async () => {
155306
await auth._updateCurrentUser(testUser(auth, 'test'));

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
106106
private persistenceManager?: PersistenceUserManager;
107107
private redirectPersistenceManager?: PersistenceUserManager;
108108
private authStateSubscription = new Subscription<User>(this);
109+
private firebaseTokenSubscription = new Subscription<FirebaseToken>(this);
109110
private idTokenSubscription = new Subscription<User>(this);
110111
private readonly beforeStateQueue = new AuthMiddlewareQueue(this);
111112
private redirectUser: UserInternal | null = null;
@@ -195,6 +196,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
195196
}
196197

197198
await this.initializeCurrentUser(popupRedirectResolver);
199+
await this.initializeFirebaseToken();
198200

199201
this.lastNotifiedUid = this.currentUser?.uid || null;
200202

@@ -403,6 +405,12 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
403405
return this.directlySetCurrentUser(user);
404406
}
405407

408+
private async initializeFirebaseToken(): Promise<void> {
409+
this.firebaseToken =
410+
(await this.persistenceManager?.getFirebaseToken()) ?? null;
411+
this.firebaseTokenSubscription.next(this.firebaseToken);
412+
}
413+
406414
useDeviceLanguage(): void {
407415
this.languageCode = _getUserLanguage();
408416
}
@@ -461,6 +469,12 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
461469
firebaseToken: FirebaseToken | null
462470
): Promise<void> {
463471
this.firebaseToken = firebaseToken;
472+
this.firebaseTokenSubscription.next(firebaseToken);
473+
if (firebaseToken) {
474+
await this.assertedPersistence.setFirebaseToken(firebaseToken);
475+
} else {
476+
await this.assertedPersistence.removeFirebaseToken();
477+
}
464478
}
465479

466480
async signOut(): Promise<void> {

packages/auth/src/core/persistence/persistence_user_manager.ts

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

1818
import { getAccountInfo } from '../../api/account_management/account';
1919
import { ApiKey, AppName, AuthInternal } from '../../model/auth';
20+
import { FirebaseToken } from '../../model/public_types';
2021
import { UserInternal } from '../../model/user';
2122
import { PersistedBlob, PersistenceInternal } from '../persistence';
2223
import { UserImpl } from '../user/user_impl';
@@ -27,7 +28,8 @@ export const enum KeyName {
2728
AUTH_USER = 'authUser',
2829
AUTH_EVENT = 'authEvent',
2930
REDIRECT_USER = 'redirectUser',
30-
PERSISTENCE_USER = 'persistence'
31+
PERSISTENCE_USER = 'persistence',
32+
PERSISTENCE_TOKEN = 'persistence-token'
3133
}
3234
export const enum Namespace {
3335
PERSISTENCE = 'firebase'
@@ -44,6 +46,7 @@ export function _persistenceKeyName(
4446
export class PersistenceUserManager {
4547
private readonly fullUserKey: string;
4648
private readonly fullPersistenceKey: string;
49+
private readonly firebaseTokenPersistenceKey: string;
4750
private readonly boundEventHandler: () => void;
4851

4952
private constructor(
@@ -58,6 +61,11 @@ export class PersistenceUserManager {
5861
config.apiKey,
5962
name
6063
);
64+
this.firebaseTokenPersistenceKey = _persistenceKeyName(
65+
KeyName.PERSISTENCE_TOKEN,
66+
config.apiKey,
67+
name
68+
);
6169
this.boundEventHandler = auth._onStorageEvent.bind(auth);
6270
this.persistence._addListener(this.fullUserKey, this.boundEventHandler);
6371
}
@@ -66,6 +74,32 @@ export class PersistenceUserManager {
6674
return this.persistence._set(this.fullUserKey, user.toJSON());
6775
}
6876

77+
setFirebaseToken(firebaseToken: FirebaseToken): Promise<void> {
78+
return this.persistence._set(this.firebaseTokenPersistenceKey, {
79+
token: firebaseToken.token,
80+
expirationTime: firebaseToken.expirationTime
81+
});
82+
}
83+
84+
async getFirebaseToken(): Promise<FirebaseToken | null> {
85+
const blob = await this.persistence._get<PersistedBlob>(
86+
this.firebaseTokenPersistenceKey
87+
);
88+
if (!blob) {
89+
return null;
90+
}
91+
const token = blob.token as string;
92+
const expirationTime = blob.expirationTime as number;
93+
return {
94+
token,
95+
expirationTime
96+
};
97+
}
98+
99+
removeFirebaseToken(): Promise<void> {
100+
return this.persistence._remove(this.firebaseTokenPersistenceKey);
101+
}
102+
69103
async getCurrentUser(): Promise<UserInternal | null> {
70104
const blob = await this.persistence._get<PersistedBlob | string>(
71105
this.fullUserKey

0 commit comments

Comments
 (0)