Skip to content

Commit e07266d

Browse files
authored
Merge b9d36c9 into b3061f2
2 parents b3061f2 + b9d36c9 commit e07266d

File tree

5 files changed

+136
-6
lines changed

5 files changed

+136
-6
lines changed

common/api-review/auth.api.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export interface Auth {
8888
readonly config: Config;
8989
readonly currentUser: User | null;
9090
readonly emulatorConfig: EmulatorConfig | null;
91+
readonly firebaseToken: FirebaseToken | null;
9192
languageCode: string | null;
9293
readonly name: string;
9394
onAuthStateChanged(nextOrObserver: NextOrObserver<User | null>, error?: ErrorFn, completed?: CompleteFn): Unsubscribe;
@@ -364,7 +365,7 @@ export interface EmulatorConfig {
364365

365366
export { ErrorFn }
366367

367-
// @public (undocumented)
368+
// @public
368369
export function exchangeToken(auth: Auth, idpConfigId: string, customToken: string): Promise<string>;
369370

370371
// Warning: (ae-forgotten-export) The symbol "BaseOAuthProvider" needs to be exported by the entry point index.d.ts
@@ -388,6 +389,14 @@ export const FactorId: {
388389
// @public
389390
export function fetchSignInMethodsForEmail(auth: Auth, email: string): Promise<string[]>;
390391

392+
// @public (undocumented)
393+
export interface FirebaseToken {
394+
// (undocumented)
395+
readonly expirationTime: number;
396+
// (undocumented)
397+
readonly token: string;
398+
}
399+
391400
// @public
392401
export function getAdditionalUserInfo(userCredential: UserCredential): AdditionalUserInfo | null;
393402

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -460,9 +460,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
460460
async _updateFirebaseToken(
461461
firebaseToken: FirebaseToken | null
462462
): Promise<void> {
463-
if (firebaseToken) {
464-
this.firebaseToken = firebaseToken;
465-
}
463+
this.firebaseToken = firebaseToken;
466464
}
467465

468466
async signOut(): Promise<void> {

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

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@
1818
import { FirebaseError } from '@firebase/util';
1919
import { expect, use } from 'chai';
2020
import * as sinon from 'sinon';
21+
import sinonChai from 'sinon-chai';
2122
import chaiAsPromised from 'chai-as-promised';
2223

23-
import { testAuth, testUser } from '../../../test/helpers/mock_auth';
24+
import {
25+
regionalTestAuth,
26+
testAuth,
27+
testUser
28+
} from '../../../test/helpers/mock_auth';
2429
import { AuthInternal } from '../../model/auth';
2530
import { UserInternal } from '../../model/user';
2631
import { AuthInterop } from './firebase_internal';
2732

33+
use(sinonChai);
2834
use(chaiAsPromised);
2935

3036
describe('core/auth/firebase_internal', () => {
@@ -37,6 +43,9 @@ describe('core/auth/firebase_internal', () => {
3743

3844
afterEach(() => {
3945
sinon.restore();
46+
delete (auth as unknown as Record<string, unknown>)[
47+
'_initializationPromise'
48+
];
4049
});
4150

4251
context('getUid', () => {
@@ -215,3 +224,74 @@ describe('core/auth/firebase_internal', () => {
215224
});
216225
});
217226
});
227+
228+
describe('core/auth/firebase_internal - Regional Firebase Auth', () => {
229+
let regionalAuth: AuthInternal;
230+
let regionalAuthInternal: AuthInterop;
231+
let now: number;
232+
beforeEach(async () => {
233+
regionalAuth = await regionalTestAuth();
234+
regionalAuthInternal = new AuthInterop(regionalAuth);
235+
now = Date.now();
236+
sinon.stub(Date, 'now').returns(now);
237+
});
238+
239+
afterEach(() => {
240+
sinon.restore();
241+
});
242+
243+
context('getFirebaseToken', () => {
244+
it('returns null if firebase token is undefined', async () => {
245+
expect(await regionalAuthInternal.getToken()).to.be.null;
246+
});
247+
248+
it('returns the id token correctly', async () => {
249+
await regionalAuth._updateFirebaseToken({
250+
token: 'access-token',
251+
expirationTime: now + 300_000
252+
});
253+
expect(await regionalAuthInternal.getToken()).to.eql({
254+
accessToken: 'access-token'
255+
});
256+
});
257+
258+
it('logs out the the id token expires in next 30 seconds', async () => {
259+
expect(await regionalAuthInternal.getToken()).to.be.null;
260+
});
261+
262+
it('logs out if token has expired', async () => {
263+
await regionalAuth._updateFirebaseToken({
264+
token: 'access-token',
265+
expirationTime: now - 5_000
266+
});
267+
expect(await regionalAuthInternal.getToken()).to.null;
268+
expect(regionalAuth.firebaseToken).to.null;
269+
});
270+
271+
it('logs out if token is expiring in next 5 seconds', async () => {
272+
await regionalAuth._updateFirebaseToken({
273+
token: 'access-token',
274+
expirationTime: now + 5_000
275+
});
276+
expect(await regionalAuthInternal.getToken()).to.null;
277+
expect(regionalAuth.firebaseToken).to.null;
278+
});
279+
280+
it('logs warning if getToken is called with forceRefresh true', async () => {
281+
sinon.stub(console, 'warn');
282+
await regionalAuth._updateFirebaseToken({
283+
token: 'access-token',
284+
expirationTime: now + 300_000
285+
});
286+
expect(await regionalAuthInternal.getToken(true)).to.eql({
287+
accessToken: 'access-token'
288+
});
289+
expect(console.warn).to.have.been.calledWith(
290+
sinon.match.string,
291+
sinon.match(
292+
/Refresh token is not a valid operation for Regional Auth instance initialized\./
293+
)
294+
);
295+
});
296+
});
297+
});

packages/auth/src/core/auth/firebase_internal.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ import { AuthInternal } from '../../model/auth';
2222
import { UserInternal } from '../../model/user';
2323
import { _assert } from '../util/assert';
2424
import { AuthErrorCode } from '../errors';
25+
import { _logWarn } from '../util/log';
2526

2627
interface TokenListener {
2728
(tok: string | null): unknown;
2829
}
2930

3031
export class AuthInterop implements FirebaseAuthInternal {
32+
private readonly TOKEN_EXPIRATION_BUFFER = 30_000;
3133
private readonly internalListeners: Map<TokenListener, Unsubscribe> =
3234
new Map();
3335

@@ -43,6 +45,14 @@ export class AuthInterop implements FirebaseAuthInternal {
4345
): Promise<{ accessToken: string } | null> {
4446
this.assertAuthConfigured();
4547
await this.auth._initializationPromise;
48+
if (this.auth.tenantConfig) {
49+
if (forceRefresh) {
50+
_logWarn(
51+
'Refresh token is not a valid operation for Regional Auth instance initialized.'
52+
);
53+
}
54+
return this.getTokenForRegionalAuth();
55+
}
4656
if (!this.auth.currentUser) {
4757
return null;
4858
}
@@ -92,4 +102,24 @@ export class AuthInterop implements FirebaseAuthInternal {
92102
this.auth._stopProactiveRefresh();
93103
}
94104
}
105+
106+
private async getTokenForRegionalAuth(): Promise<{
107+
accessToken: string;
108+
} | null> {
109+
if (!this.auth.firebaseToken) {
110+
return null;
111+
}
112+
113+
if (
114+
!this.auth.firebaseToken.expirationTime ||
115+
Date.now() >
116+
this.auth.firebaseToken.expirationTime - this.TOKEN_EXPIRATION_BUFFER
117+
) {
118+
await this.auth._updateFirebaseToken(null);
119+
return null;
120+
}
121+
122+
const accessToken = await this.auth.firebaseToken.token;
123+
return { accessToken };
124+
}
95125
}

packages/auth/test/helpers/mock_auth.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,11 @@ export async function testAuth(
118118
return auth;
119119
}
120120

121-
export async function regionalTestAuth(): Promise<TestAuth> {
121+
export async function regionalTestAuth(
122+
popupRedirectResolver?: PopupRedirectResolver,
123+
persistence = new MockPersistenceLayer(),
124+
skipAwaitOnInit?: boolean
125+
): Promise<TestAuth> {
122126
const tenantConfig = { 'location': 'us', 'tenantId': 'tenant-1' };
123127
const auth: TestAuth = new AuthImpl(
124128
FAKE_APP,
@@ -135,6 +139,15 @@ export async function regionalTestAuth(): Promise<TestAuth> {
135139
},
136140
tenantConfig
137141
) as TestAuth;
142+
if (skipAwaitOnInit) {
143+
// This is used to verify scenarios where auth flows (like signInWithRedirect) are invoked before auth is fully initialized.
144+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
145+
auth._initializeWithPersistence([persistence], popupRedirectResolver);
146+
} else {
147+
await auth._initializeWithPersistence([persistence], popupRedirectResolver);
148+
}
149+
auth.persistenceLayer = persistence;
150+
auth.settings.appVerificationDisabledForTesting = true;
138151
return auth;
139152
}
140153

0 commit comments

Comments
 (0)