From 28c985de7aea2884237258b3ec22e5cd9e49af47 Mon Sep 17 00:00:00 2001 From: mansisampat Date: Mon, 26 May 2025 11:45:54 +0530 Subject: [PATCH 01/11] Write Firebase Token to Auth instance --- .../auth/src/api/authentication/exchange_token.ts | 4 +++- packages/auth/src/core/auth/auth_impl.ts | 10 +++++++++- .../src/core/strategies/exchange_token.test.ts | 10 +++++++++- .../auth/src/core/strategies/exhange_token.ts | 7 ++++++- packages/auth/src/model/auth.ts | 2 ++ packages/auth/src/model/public_types.ts | 15 +++++++++++++++ 6 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/auth/src/api/authentication/exchange_token.ts b/packages/auth/src/api/authentication/exchange_token.ts index 751e51c4c2c..9a073c8f61d 100644 --- a/packages/auth/src/api/authentication/exchange_token.ts +++ b/packages/auth/src/api/authentication/exchange_token.ts @@ -27,8 +27,10 @@ export interface ExchangeTokenRequest { } export interface ExchangeTokenResponse { + // The firebase access token (JWT signed by Firebase Auth). accessToken: string; - expiresIn?: string; + // The time when the access token expires. + expiresIn: number; } export async function exchangeToken( diff --git a/packages/auth/src/core/auth/auth_impl.ts b/packages/auth/src/core/auth/auth_impl.ts index 7e18b7d44e0..16569cc86fb 100644 --- a/packages/auth/src/core/auth/auth_impl.ts +++ b/packages/auth/src/core/auth/auth_impl.ts @@ -37,7 +37,8 @@ import { NextFn, Unsubscribe, PasswordValidationStatus, - TenantConfig + TenantConfig, + FirebaseToken } from '../../model/public_types'; import { createSubscribe, @@ -100,6 +101,7 @@ export const enum DefaultConfig { export class AuthImpl implements AuthInternal, _FirebaseService { currentUser: User | null = null; emulatorConfig: EmulatorConfig | null = null; + firebaseToken: FirebaseToken | null = null; private operations = Promise.resolve(); private persistenceManager?: PersistenceUserManager; private redirectPersistenceManager?: PersistenceUserManager; @@ -455,6 +457,12 @@ export class AuthImpl implements AuthInternal, _FirebaseService { }); } + async _updateFirebaseToken(firebaseToken: FirebaseToken | null): Promise { + if (firebaseToken) { + this.firebaseToken = firebaseToken; + } + } + async signOut(): Promise { if (_isFirebaseServerApp(this.app)) { return Promise.reject( diff --git a/packages/auth/src/core/strategies/exchange_token.test.ts b/packages/auth/src/core/strategies/exchange_token.test.ts index 60166034b64..075ba380f8f 100644 --- a/packages/auth/src/core/strategies/exchange_token.test.ts +++ b/packages/auth/src/core/strategies/exchange_token.test.ts @@ -24,6 +24,7 @@ import { testAuth, TestAuth } from '../../../test/helpers/mock_auth'; +import * as sinon from 'sinon'; import * as mockFetch from '../../../test/helpers/mock_fetch'; import { HttpHeader, RegionalEndpoint } from '../../api'; import { exchangeToken } from './exhange_token'; @@ -35,19 +36,23 @@ use(chaiAsPromised); describe('core/strategies/exchangeToken', () => { let auth: TestAuth; let regionalAuth: TestAuth; + let now: number; beforeEach(async () => { auth = await testAuth(); regionalAuth = await regionalTestAuth(); mockFetch.setUp(); + now = Date.now(); + sinon.stub(Date, 'now').returns(now); }); afterEach(mockFetch.tearDown); + afterEach(() => sinon.restore()); it('should return a valid access token for Regional Auth', async () => { const mock = mockRegionalEndpointWithParent( RegionalEndpoint.EXCHANGE_TOKEN, 'projects/test-project-id/locations/us/tenants/tenant-1/idpConfigs/idp-config', - { accessToken: 'outbound-token', expiresIn: '1000' } + { accessToken: 'outbound-token', expiresIn: 10000 } ); const accessToken = await exchangeToken( @@ -65,6 +70,8 @@ describe('core/strategies/exchangeToken', () => { expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( 'application/json' ); + expect(regionalAuth.firebaseToken?.token).to.equal('outbound-token'); + expect(regionalAuth.firebaseToken?.expirationTime).to.equal(now + 10_000); }); it('throws exception for default Auth', async () => { @@ -106,5 +113,6 @@ describe('core/strategies/exchangeToken', () => { expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq( 'application/json' ); + expect(regionalAuth.firebaseToken).is.null; }); }); diff --git a/packages/auth/src/core/strategies/exhange_token.ts b/packages/auth/src/core/strategies/exhange_token.ts index 6b4c11b8017..8787c3e96d9 100644 --- a/packages/auth/src/core/strategies/exhange_token.ts +++ b/packages/auth/src/core/strategies/exhange_token.ts @@ -54,7 +54,12 @@ export async function exchangeToken( parent: buildParent(auth, idpConfigId), token: customToken }); - // TODO(sammansi): Write token to the Auth object passed. + if (token) { + await authInternal._updateFirebaseToken({ + token: token.accessToken, + expirationTime: Date.now() + token.expiresIn * 1000 + }); + } return token.accessToken; } diff --git a/packages/auth/src/model/auth.ts b/packages/auth/src/model/auth.ts index 60faedef4e6..ea9f4b203d2 100644 --- a/packages/auth/src/model/auth.ts +++ b/packages/auth/src/model/auth.ts @@ -20,6 +20,7 @@ import { AuthSettings, Config, EmulatorConfig, + FirebaseToken, PasswordPolicy, PasswordValidationStatus, PopupRedirectResolver, @@ -75,6 +76,7 @@ export interface AuthInternal extends Auth { _initializationPromise: Promise | null; _persistenceManagerAvailable: Promise; _updateCurrentUser(user: UserInternal | null): Promise; + _updateFirebaseToken(firebaseToken: FirebaseToken | null): Promise; _onStorageEvent(): void; diff --git a/packages/auth/src/model/public_types.ts b/packages/auth/src/model/public_types.ts index 353064af467..5c6503e15fb 100644 --- a/packages/auth/src/model/public_types.ts +++ b/packages/auth/src/model/public_types.ts @@ -334,6 +334,14 @@ export interface Auth { * {@link @firebase/app#FirebaseServerApp}. */ signOut(): Promise; + /** + * The token response initialized via {@link exchangeToken} endpoint. + * + * @remarks + * This field is only supported for {@link Auth} instance that have defined + * {@link TenantConfig}. + */ + readonly firebaseToken: FirebaseToken | null; } /** @@ -966,6 +974,13 @@ export interface ReactNativeAsyncStorage { removeItem(key: string): Promise; } +export interface FirebaseToken { + // The firebase access token (JWT signed by Firebase Auth). + readonly token: string; + // The time when the access token expires. + readonly expirationTime: number; +} + /** * A user account. * From df70b658c1e74ad5fd7d6ff4422b37a866485d67 Mon Sep 17 00:00:00 2001 From: mansisampat Date: Mon, 26 May 2025 11:57:16 +0530 Subject: [PATCH 02/11] yarn run format --- packages/auth/src/core/auth/auth_impl.ts | 6 ++++-- packages/auth/src/model/public_types.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/auth/src/core/auth/auth_impl.ts b/packages/auth/src/core/auth/auth_impl.ts index 16569cc86fb..1c377b37332 100644 --- a/packages/auth/src/core/auth/auth_impl.ts +++ b/packages/auth/src/core/auth/auth_impl.ts @@ -457,8 +457,10 @@ export class AuthImpl implements AuthInternal, _FirebaseService { }); } - async _updateFirebaseToken(firebaseToken: FirebaseToken | null): Promise { - if (firebaseToken) { + async _updateFirebaseToken( + firebaseToken: FirebaseToken | null + ): Promise { + if (firebaseToken) { this.firebaseToken = firebaseToken; } } diff --git a/packages/auth/src/model/public_types.ts b/packages/auth/src/model/public_types.ts index 5c6503e15fb..4a8cf348c1b 100644 --- a/packages/auth/src/model/public_types.ts +++ b/packages/auth/src/model/public_types.ts @@ -978,7 +978,7 @@ export interface FirebaseToken { // The firebase access token (JWT signed by Firebase Auth). readonly token: string; // The time when the access token expires. - readonly expirationTime: number; + readonly expirationTime: number; } /** From f1509dad846b61f1c983800e346c86c46aab023a Mon Sep 17 00:00:00 2001 From: mansisampat Date: Mon, 26 May 2025 12:22:29 +0530 Subject: [PATCH 03/11] Fix unit test and run yarn docgen:all --- docs-devsite/_toc.yaml | 2 + docs-devsite/auth.auth.md | 13 ++++++ docs-devsite/auth.firebasetoken.md | 40 +++++++++++++++++++ docs-devsite/auth.md | 1 + .../core/strategies/exchange_token.test.ts | 2 +- 5 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 docs-devsite/auth.firebasetoken.md diff --git a/docs-devsite/_toc.yaml b/docs-devsite/_toc.yaml index 088facef70f..afd95cee717 100644 --- a/docs-devsite/_toc.yaml +++ b/docs-devsite/_toc.yaml @@ -246,6 +246,8 @@ toc: path: /docs/reference/js/auth.emulatorconfig.md - title: FacebookAuthProvider path: /docs/reference/js/auth.facebookauthprovider.md + - title: FirebaseToken + path: /docs/reference/js/auth.firebasetoken.md - title: GithubAuthProvider path: /docs/reference/js/auth.githubauthprovider.md - title: GoogleAuthProvider diff --git a/docs-devsite/auth.auth.md b/docs-devsite/auth.auth.md index 170447bc43c..6a73129640a 100644 --- a/docs-devsite/auth.auth.md +++ b/docs-devsite/auth.auth.md @@ -28,6 +28,7 @@ export interface Auth | [config](./auth.auth.md#authconfig) | [Config](./auth.config.md#config_interface) | The [Config](./auth.config.md#config_interface) used to initialize this instance. | | [currentUser](./auth.auth.md#authcurrentuser) | [User](./auth.user.md#user_interface) \| null | The currently signed-in user (or null). | | [emulatorConfig](./auth.auth.md#authemulatorconfig) | [EmulatorConfig](./auth.emulatorconfig.md#emulatorconfig_interface) \| null | The current emulator configuration (or null). | +| [firebaseToken](./auth.auth.md#authfirebasetoken) | [FirebaseToken](./auth.firebasetoken.md#firebasetoken_interface) \| null | The token response initialized via [exchangeToken()](./auth.md#exchangetoken_b6b1871) endpoint. | | [languageCode](./auth.auth.md#authlanguagecode) | string \| null | The [Auth](./auth.auth.md#auth_interface) instance's language code. | | [name](./auth.auth.md#authname) | string | The name of the app associated with the Auth service instance. | | [settings](./auth.auth.md#authsettings) | [AuthSettings](./auth.authsettings.md#authsettings_interface) | The [Auth](./auth.auth.md#auth_interface) instance's settings. | @@ -87,6 +88,18 @@ The current emulator configuration (or null). readonly emulatorConfig: EmulatorConfig | null; ``` +## Auth.firebaseToken + +The token response initialized via [exchangeToken()](./auth.md#exchangetoken_b6b1871) endpoint. + +This field is only supported for [Auth](./auth.auth.md#auth_interface) instance that have defined [TenantConfig](./auth.tenantconfig.md#tenantconfig_interface). + +Signature: + +```typescript +readonly firebaseToken: FirebaseToken | null; +``` + ## Auth.languageCode The [Auth](./auth.auth.md#auth_interface) instance's language code. diff --git a/docs-devsite/auth.firebasetoken.md b/docs-devsite/auth.firebasetoken.md new file mode 100644 index 00000000000..7a6a8d1223b --- /dev/null +++ b/docs-devsite/auth.firebasetoken.md @@ -0,0 +1,40 @@ +Project: /docs/reference/js/_project.yaml +Book: /docs/reference/_book.yaml +page_type: reference + +{% comment %} +DO NOT EDIT THIS FILE! +This is generated by the JS SDK team, and any local changes will be +overwritten. Changes should be made in the source code at +https://github.com/firebase/firebase-js-sdk +{% endcomment %} + +# FirebaseToken interface +Signature: + +```typescript +export interface FirebaseToken +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [expirationTime](./auth.firebasetoken.md#firebasetokenexpirationtime) | number | | +| [token](./auth.firebasetoken.md#firebasetokentoken) | string | | + +## FirebaseToken.expirationTime + +Signature: + +```typescript +readonly expirationTime: number; +``` + +## FirebaseToken.token + +Signature: + +```typescript +readonly token: string; +``` diff --git a/docs-devsite/auth.md b/docs-devsite/auth.md index 9b8a294ede4..8f9a1745dc9 100644 --- a/docs-devsite/auth.md +++ b/docs-devsite/auth.md @@ -118,6 +118,7 @@ Firebase Authentication | [ConfirmationResult](./auth.confirmationresult.md#confirmationresult_interface) | A result from a phone number sign-in, link, or reauthenticate call. | | [Dependencies](./auth.dependencies.md#dependencies_interface) | The dependencies that can be used to initialize an [Auth](./auth.auth.md#auth_interface) instance. | | [EmulatorConfig](./auth.emulatorconfig.md#emulatorconfig_interface) | Configuration of Firebase Authentication Emulator. | +| [FirebaseToken](./auth.firebasetoken.md#firebasetoken_interface) | | | [IdTokenResult](./auth.idtokenresult.md#idtokenresult_interface) | Interface representing ID token result obtained from [User.getIdTokenResult()](./auth.user.md#usergetidtokenresult). | | [MultiFactorAssertion](./auth.multifactorassertion.md#multifactorassertion_interface) | The base class for asserting ownership of a second factor. | | [MultiFactorError](./auth.multifactorerror.md#multifactorerror_interface) | The error thrown when the user needs to provide a second factor to sign in successfully. | diff --git a/packages/auth/src/core/strategies/exchange_token.test.ts b/packages/auth/src/core/strategies/exchange_token.test.ts index 075ba380f8f..25c13572e39 100644 --- a/packages/auth/src/core/strategies/exchange_token.test.ts +++ b/packages/auth/src/core/strategies/exchange_token.test.ts @@ -52,7 +52,7 @@ describe('core/strategies/exchangeToken', () => { const mock = mockRegionalEndpointWithParent( RegionalEndpoint.EXCHANGE_TOKEN, 'projects/test-project-id/locations/us/tenants/tenant-1/idpConfigs/idp-config', - { accessToken: 'outbound-token', expiresIn: 10000 } + { accessToken: 'outbound-token', expiresIn: 10 } ); const accessToken = await exchangeToken( From 817f8beaf83e9b5a9d12a84ae5d7fe7a6f6a279f Mon Sep 17 00:00:00 2001 From: mansisampat Date: Mon, 26 May 2025 12:36:50 +0530 Subject: [PATCH 04/11] getFirebaseToken Auth Interop method for BYO-CIAM --- packages/auth-interop-types/index.d.ts | 1 + .../auth/src/core/auth/firebase_internal.ts | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/auth-interop-types/index.d.ts b/packages/auth-interop-types/index.d.ts index 6c9eb3d2bf0..04ea05815c9 100644 --- a/packages/auth-interop-types/index.d.ts +++ b/packages/auth-interop-types/index.d.ts @@ -21,6 +21,7 @@ export interface FirebaseAuthTokenData { export interface FirebaseAuthInternal { getToken(refreshToken?: boolean): Promise; + getFirebaseToken(): Promise; getUid(): string | null; addAuthTokenListener(fn: (token: string | null) => void): void; removeAuthTokenListener(fn: (token: string | null) => void): void; diff --git a/packages/auth/src/core/auth/firebase_internal.ts b/packages/auth/src/core/auth/firebase_internal.ts index 4fad0754375..1efd2c4293d 100644 --- a/packages/auth/src/core/auth/firebase_internal.ts +++ b/packages/auth/src/core/auth/firebase_internal.ts @@ -28,6 +28,7 @@ interface TokenListener { } export class AuthInterop implements FirebaseAuthInternal { + private readonly TOKEN_EXPIRATION_BUFFER = 30_000; private readonly internalListeners: Map = new Map(); @@ -51,6 +52,26 @@ export class AuthInterop implements FirebaseAuthInternal { return { accessToken }; } + async getFirebaseToken(): Promise<{ accessToken: string } | null> { + this.assertAuthConfigured(); + this.assertRegionalAuthConfigured(); + if (!this.auth.firebaseToken) { + return null; + } + + if ( + !this.auth.firebaseToken.expirationTime || + Date.now() > + this.auth.firebaseToken.expirationTime - this.TOKEN_EXPIRATION_BUFFER + ) { + await this.auth._updateFirebaseToken(null); + return null; + } + + const accessToken = await this.auth.firebaseToken.token; + return { accessToken }; + } + addAuthTokenListener(listener: TokenListener): void { this.assertAuthConfigured(); if (this.internalListeners.has(listener)) { @@ -85,6 +106,10 @@ export class AuthInterop implements FirebaseAuthInternal { ); } + private assertRegionalAuthConfigured(): void { + _assert(this.auth.tenantConfig, AuthErrorCode.OPERATION_NOT_ALLOWED); + } + private updateProactiveRefresh(): void { if (this.internalListeners.size > 0) { this.auth._startProactiveRefresh(); From ea7b6050589eb907e0ce205cbf15f0a0035a4974 Mon Sep 17 00:00:00 2001 From: mansisampat Date: Mon, 26 May 2025 16:55:56 +0530 Subject: [PATCH 05/11] Add unit test --- .../src/core/auth/firebase_internal.test.ts | 24 ++++++++++++++++++- .../auth/src/core/auth/firebase_internal.ts | 1 + packages/auth/test/helpers/mock_auth.ts | 14 ++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/auth/src/core/auth/firebase_internal.test.ts b/packages/auth/src/core/auth/firebase_internal.test.ts index ff5b94b3f4f..fa0c434c283 100644 --- a/packages/auth/src/core/auth/firebase_internal.test.ts +++ b/packages/auth/src/core/auth/firebase_internal.test.ts @@ -20,7 +20,7 @@ import { expect, use } from 'chai'; import * as sinon from 'sinon'; import chaiAsPromised from 'chai-as-promised'; -import { testAuth, testUser } from '../../../test/helpers/mock_auth'; +import { regionalTestAuth, testAuth, testUser } from '../../../test/helpers/mock_auth'; import { AuthInternal } from '../../model/auth'; import { UserInternal } from '../../model/user'; import { AuthInterop } from './firebase_internal'; @@ -37,6 +37,9 @@ describe('core/auth/firebase_internal', () => { afterEach(() => { sinon.restore(); + delete (auth as unknown as Record)[ + '_initializationPromise' + ]; }); context('getUid', () => { @@ -215,3 +218,22 @@ describe('core/auth/firebase_internal', () => { }); }); }); + +describe('core/auth/firebase_internal - Regional Firebase Auth', () => { + let regionalAuth: AuthInternal; + let regionalAuthInternal: AuthInterop; + beforeEach(async () => { + regionalAuth = await regionalTestAuth(); + regionalAuthInternal = new AuthInterop(regionalAuth); + }); + + afterEach(() => { + sinon.restore(); + }); + + context('getFirebaseToken', () => { + it('returns null if firebase token is undefined', async () => { + expect(await regionalAuthInternal.getFirebaseToken()).to.be.null; + }); + }); +}); diff --git a/packages/auth/src/core/auth/firebase_internal.ts b/packages/auth/src/core/auth/firebase_internal.ts index 1efd2c4293d..0ec940f26f3 100644 --- a/packages/auth/src/core/auth/firebase_internal.ts +++ b/packages/auth/src/core/auth/firebase_internal.ts @@ -54,6 +54,7 @@ export class AuthInterop implements FirebaseAuthInternal { async getFirebaseToken(): Promise<{ accessToken: string } | null> { this.assertAuthConfigured(); + await this.auth._initializationPromise; this.assertRegionalAuthConfigured(); if (!this.auth.firebaseToken) { return null; diff --git a/packages/auth/test/helpers/mock_auth.ts b/packages/auth/test/helpers/mock_auth.ts index 68e155a88f4..3412da9565b 100644 --- a/packages/auth/test/helpers/mock_auth.ts +++ b/packages/auth/test/helpers/mock_auth.ts @@ -118,7 +118,10 @@ export async function testAuth( return auth; } -export async function regionalTestAuth(): Promise { +export async function regionalTestAuth( + popupRedirectResolver?: PopupRedirectResolver, + persistence = new MockPersistenceLayer(), + skipAwaitOnInit?: boolean): Promise { const tenantConfig = { 'location': 'us', 'tenantId': 'tenant-1' }; const auth: TestAuth = new AuthImpl( FAKE_APP, @@ -135,6 +138,15 @@ export async function regionalTestAuth(): Promise { }, tenantConfig ) as TestAuth; + if (skipAwaitOnInit) { + // This is used to verify scenarios where auth flows (like signInWithRedirect) are invoked before auth is fully initialized. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + auth._initializeWithPersistence([persistence], popupRedirectResolver); + } else { + await auth._initializeWithPersistence([persistence], popupRedirectResolver); + } + auth.persistenceLayer = persistence; + auth.settings.appVerificationDisabledForTesting = true; return auth; } From 0f7032ff39fc5572f2bea54158a357f9172a3844 Mon Sep 17 00:00:00 2001 From: mansisampat Date: Mon, 26 May 2025 17:42:35 +0530 Subject: [PATCH 06/11] Adding Unit test --- packages/auth/src/core/auth/auth_impl.ts | 2 - .../src/core/auth/firebase_internal.test.ts | 45 +++++++++++++++++-- packages/auth/test/helpers/mock_auth.ts | 3 +- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/auth/src/core/auth/auth_impl.ts b/packages/auth/src/core/auth/auth_impl.ts index 1c377b37332..2059eaee085 100644 --- a/packages/auth/src/core/auth/auth_impl.ts +++ b/packages/auth/src/core/auth/auth_impl.ts @@ -460,9 +460,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService { async _updateFirebaseToken( firebaseToken: FirebaseToken | null ): Promise { - if (firebaseToken) { this.firebaseToken = firebaseToken; - } } async signOut(): Promise { diff --git a/packages/auth/src/core/auth/firebase_internal.test.ts b/packages/auth/src/core/auth/firebase_internal.test.ts index fa0c434c283..8195e814a8f 100644 --- a/packages/auth/src/core/auth/firebase_internal.test.ts +++ b/packages/auth/src/core/auth/firebase_internal.test.ts @@ -20,7 +20,11 @@ import { expect, use } from 'chai'; import * as sinon from 'sinon'; import chaiAsPromised from 'chai-as-promised'; -import { regionalTestAuth, testAuth, testUser } from '../../../test/helpers/mock_auth'; +import { + regionalTestAuth, + testAuth, + testUser +} from '../../../test/helpers/mock_auth'; import { AuthInternal } from '../../model/auth'; import { UserInternal } from '../../model/user'; import { AuthInterop } from './firebase_internal'; @@ -38,8 +42,8 @@ describe('core/auth/firebase_internal', () => { afterEach(() => { sinon.restore(); delete (auth as unknown as Record)[ - '_initializationPromise' - ]; + '_initializationPromise' + ]; }); context('getUid', () => { @@ -222,9 +226,12 @@ describe('core/auth/firebase_internal', () => { describe('core/auth/firebase_internal - Regional Firebase Auth', () => { let regionalAuth: AuthInternal; let regionalAuthInternal: AuthInterop; + let now: number; beforeEach(async () => { regionalAuth = await regionalTestAuth(); regionalAuthInternal = new AuthInterop(regionalAuth); + now = Date.now(); + sinon.stub(Date, 'now').returns(now); }); afterEach(() => { @@ -235,5 +242,37 @@ describe('core/auth/firebase_internal - Regional Firebase Auth', () => { it('returns null if firebase token is undefined', async () => { expect(await regionalAuthInternal.getFirebaseToken()).to.be.null; }); + + it('returns the id token correctly', async () => { + await regionalAuth._updateFirebaseToken({ + token: 'access-token', + expirationTime: now + 300_000 + }); + expect(await regionalAuthInternal.getFirebaseToken()).to.eql({ + accessToken: 'access-token' + }); + }); + + it('logs out the the id token expires in next 30 seconds', async () => { + expect(await regionalAuthInternal.getFirebaseToken()).to.be.null; + }); + + it('logs out if token has expired', async () => { + await regionalAuth._updateFirebaseToken({ + token: 'access-token', + expirationTime: now - 5_000 + }); + expect(await regionalAuthInternal.getFirebaseToken()).to.null; + expect(regionalAuth.firebaseToken).to.null; + }); + + it('logs out if token is expiring in next 5 seconds', async () => { + await regionalAuth._updateFirebaseToken({ + token: 'access-token', + expirationTime: now + 5_000 + }); + expect(await regionalAuthInternal.getFirebaseToken()).to.null; + expect(regionalAuth.firebaseToken).to.null; + }); }); }); diff --git a/packages/auth/test/helpers/mock_auth.ts b/packages/auth/test/helpers/mock_auth.ts index 3412da9565b..dee18047fde 100644 --- a/packages/auth/test/helpers/mock_auth.ts +++ b/packages/auth/test/helpers/mock_auth.ts @@ -121,7 +121,8 @@ export async function testAuth( export async function regionalTestAuth( popupRedirectResolver?: PopupRedirectResolver, persistence = new MockPersistenceLayer(), - skipAwaitOnInit?: boolean): Promise { + skipAwaitOnInit?: boolean +): Promise { const tenantConfig = { 'location': 'us', 'tenantId': 'tenant-1' }; const auth: TestAuth = new AuthImpl( FAKE_APP, From a5a79f103e22758f2c1beb181ba74211b11a2353 Mon Sep 17 00:00:00 2001 From: mansisampat Date: Mon, 26 May 2025 19:10:17 +0530 Subject: [PATCH 07/11] Running yarn docgen:all --- packages/auth/src/core/auth/auth_impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/auth/src/core/auth/auth_impl.ts b/packages/auth/src/core/auth/auth_impl.ts index 2059eaee085..2651fe43af9 100644 --- a/packages/auth/src/core/auth/auth_impl.ts +++ b/packages/auth/src/core/auth/auth_impl.ts @@ -460,7 +460,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService { async _updateFirebaseToken( firebaseToken: FirebaseToken | null ): Promise { - this.firebaseToken = firebaseToken; + this.firebaseToken = firebaseToken; } async signOut(): Promise { From c8d9cb5d4366d91bdec2ed86e1a5b25794ecca40 Mon Sep 17 00:00:00 2001 From: mansisampat Date: Tue, 27 May 2025 10:37:47 +0530 Subject: [PATCH 08/11] Use getToken instead of getFirebaseToken --- common/api-review/auth.api.md | 11 ++++- packages/auth-interop-types/index.d.ts | 1 - .../auth/src/core/auth/firebase_internal.ts | 46 ++++++++++--------- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/common/api-review/auth.api.md b/common/api-review/auth.api.md index ef28f329f56..4c1a00d154b 100644 --- a/common/api-review/auth.api.md +++ b/common/api-review/auth.api.md @@ -88,6 +88,7 @@ export interface Auth { readonly config: Config; readonly currentUser: User | null; readonly emulatorConfig: EmulatorConfig | null; + readonly firebaseToken: FirebaseToken | null; languageCode: string | null; readonly name: string; onAuthStateChanged(nextOrObserver: NextOrObserver, error?: ErrorFn, completed?: CompleteFn): Unsubscribe; @@ -364,7 +365,7 @@ export interface EmulatorConfig { export { ErrorFn } -// @public (undocumented) +// @public export function exchangeToken(auth: Auth, idpConfigId: string, customToken: string): Promise; // 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: { // @public export function fetchSignInMethodsForEmail(auth: Auth, email: string): Promise; +// @public (undocumented) +export interface FirebaseToken { + // (undocumented) + readonly expirationTime: number; + // (undocumented) + readonly token: string; +} + // @public export function getAdditionalUserInfo(userCredential: UserCredential): AdditionalUserInfo | null; diff --git a/packages/auth-interop-types/index.d.ts b/packages/auth-interop-types/index.d.ts index 04ea05815c9..6c9eb3d2bf0 100644 --- a/packages/auth-interop-types/index.d.ts +++ b/packages/auth-interop-types/index.d.ts @@ -21,7 +21,6 @@ export interface FirebaseAuthTokenData { export interface FirebaseAuthInternal { getToken(refreshToken?: boolean): Promise; - getFirebaseToken(): Promise; getUid(): string | null; addAuthTokenListener(fn: (token: string | null) => void): void; removeAuthTokenListener(fn: (token: string | null) => void): void; diff --git a/packages/auth/src/core/auth/firebase_internal.ts b/packages/auth/src/core/auth/firebase_internal.ts index 0ec940f26f3..832ecfef766 100644 --- a/packages/auth/src/core/auth/firebase_internal.ts +++ b/packages/auth/src/core/auth/firebase_internal.ts @@ -22,6 +22,7 @@ import { AuthInternal } from '../../model/auth'; import { UserInternal } from '../../model/user'; import { _assert } from '../util/assert'; import { AuthErrorCode } from '../errors'; +import { _logWarn } from '../util/log'; interface TokenListener { (tok: string | null): unknown; @@ -44,6 +45,12 @@ export class AuthInterop implements FirebaseAuthInternal { ): Promise<{ accessToken: string } | null> { this.assertAuthConfigured(); await this.auth._initializationPromise; + if (this.auth.tenantConfig) { + if (forceRefresh) { + _logWarn("Refresh token is not a valid operation for Regional Auth instance initialized."); + } + return this.getTokenForRegionalAuth(); + } if (!this.auth.currentUser) { return null; } @@ -52,27 +59,6 @@ export class AuthInterop implements FirebaseAuthInternal { return { accessToken }; } - async getFirebaseToken(): Promise<{ accessToken: string } | null> { - this.assertAuthConfigured(); - await this.auth._initializationPromise; - this.assertRegionalAuthConfigured(); - if (!this.auth.firebaseToken) { - return null; - } - - if ( - !this.auth.firebaseToken.expirationTime || - Date.now() > - this.auth.firebaseToken.expirationTime - this.TOKEN_EXPIRATION_BUFFER - ) { - await this.auth._updateFirebaseToken(null); - return null; - } - - const accessToken = await this.auth.firebaseToken.token; - return { accessToken }; - } - addAuthTokenListener(listener: TokenListener): void { this.assertAuthConfigured(); if (this.internalListeners.has(listener)) { @@ -118,4 +104,22 @@ export class AuthInterop implements FirebaseAuthInternal { this.auth._stopProactiveRefresh(); } } + + private async getTokenForRegionalAuth(): Promise<{ accessToken: string } | null> { + if (!this.auth.firebaseToken) { + return null; + } + + if ( + !this.auth.firebaseToken.expirationTime || + Date.now() > + this.auth.firebaseToken.expirationTime - this.TOKEN_EXPIRATION_BUFFER + ) { + await this.auth._updateFirebaseToken(null); + return null; + } + + const accessToken = await this.auth.firebaseToken.token; + return { accessToken }; + } } From 205916f76653abd04ef8404fc45c5c8d68d77f81 Mon Sep 17 00:00:00 2001 From: mansisampat Date: Tue, 27 May 2025 11:25:59 +0530 Subject: [PATCH 09/11] Adding Unit test --- .../src/core/auth/firebase_internal.test.ts | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/auth/src/core/auth/firebase_internal.test.ts b/packages/auth/src/core/auth/firebase_internal.test.ts index 8195e814a8f..60d5ea83a92 100644 --- a/packages/auth/src/core/auth/firebase_internal.test.ts +++ b/packages/auth/src/core/auth/firebase_internal.test.ts @@ -18,6 +18,7 @@ import { FirebaseError } from '@firebase/util'; import { expect, use } from 'chai'; import * as sinon from 'sinon'; +import sinonChai from 'sinon-chai'; import chaiAsPromised from 'chai-as-promised'; import { @@ -29,6 +30,7 @@ import { AuthInternal } from '../../model/auth'; import { UserInternal } from '../../model/user'; import { AuthInterop } from './firebase_internal'; +use(sinonChai); use(chaiAsPromised); describe('core/auth/firebase_internal', () => { @@ -240,7 +242,7 @@ describe('core/auth/firebase_internal - Regional Firebase Auth', () => { context('getFirebaseToken', () => { it('returns null if firebase token is undefined', async () => { - expect(await regionalAuthInternal.getFirebaseToken()).to.be.null; + expect(await regionalAuthInternal.getToken()).to.be.null; }); it('returns the id token correctly', async () => { @@ -248,13 +250,13 @@ describe('core/auth/firebase_internal - Regional Firebase Auth', () => { token: 'access-token', expirationTime: now + 300_000 }); - expect(await regionalAuthInternal.getFirebaseToken()).to.eql({ + expect(await regionalAuthInternal.getToken()).to.eql({ accessToken: 'access-token' }); }); it('logs out the the id token expires in next 30 seconds', async () => { - expect(await regionalAuthInternal.getFirebaseToken()).to.be.null; + expect(await regionalAuthInternal.getToken()).to.be.null; }); it('logs out if token has expired', async () => { @@ -262,7 +264,7 @@ describe('core/auth/firebase_internal - Regional Firebase Auth', () => { token: 'access-token', expirationTime: now - 5_000 }); - expect(await regionalAuthInternal.getFirebaseToken()).to.null; + expect(await regionalAuthInternal.getToken()).to.null; expect(regionalAuth.firebaseToken).to.null; }); @@ -271,8 +273,23 @@ describe('core/auth/firebase_internal - Regional Firebase Auth', () => { token: 'access-token', expirationTime: now + 5_000 }); - expect(await regionalAuthInternal.getFirebaseToken()).to.null; + expect(await regionalAuthInternal.getToken()).to.null; expect(regionalAuth.firebaseToken).to.null; }); + + it('logs warning if getToken is called with forceRefresh true', async () => { + sinon.stub(console, 'warn'); + await regionalAuth._updateFirebaseToken({ + token: 'access-token', + expirationTime: now + 300_000 + }); + expect(await regionalAuthInternal.getToken(true)).to.eql({ + accessToken: 'access-token' + }); + expect(console.warn).to.have.been.calledWith( + sinon.match.string, + sinon.match(/Refresh token is not a valid operation for Regional Auth instance initialized\./) + ); + }); }); }); From 94d19132e846377d9cc7734fb04b4946313e9a1a Mon Sep 17 00:00:00 2001 From: mansisampat Date: Tue, 27 May 2025 11:27:28 +0530 Subject: [PATCH 10/11] yarn run format --- packages/auth/src/core/auth/firebase_internal.test.ts | 4 +++- packages/auth/src/core/auth/firebase_internal.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/auth/src/core/auth/firebase_internal.test.ts b/packages/auth/src/core/auth/firebase_internal.test.ts index 60d5ea83a92..4213d720385 100644 --- a/packages/auth/src/core/auth/firebase_internal.test.ts +++ b/packages/auth/src/core/auth/firebase_internal.test.ts @@ -288,7 +288,9 @@ describe('core/auth/firebase_internal - Regional Firebase Auth', () => { }); expect(console.warn).to.have.been.calledWith( sinon.match.string, - sinon.match(/Refresh token is not a valid operation for Regional Auth instance initialized\./) + sinon.match( + /Refresh token is not a valid operation for Regional Auth instance initialized\./ + ) ); }); }); diff --git a/packages/auth/src/core/auth/firebase_internal.ts b/packages/auth/src/core/auth/firebase_internal.ts index 832ecfef766..d7d5b8e3c66 100644 --- a/packages/auth/src/core/auth/firebase_internal.ts +++ b/packages/auth/src/core/auth/firebase_internal.ts @@ -47,7 +47,9 @@ export class AuthInterop implements FirebaseAuthInternal { await this.auth._initializationPromise; if (this.auth.tenantConfig) { if (forceRefresh) { - _logWarn("Refresh token is not a valid operation for Regional Auth instance initialized."); + _logWarn( + 'Refresh token is not a valid operation for Regional Auth instance initialized.' + ); } return this.getTokenForRegionalAuth(); } @@ -105,7 +107,9 @@ export class AuthInterop implements FirebaseAuthInternal { } } - private async getTokenForRegionalAuth(): Promise<{ accessToken: string } | null> { + private async getTokenForRegionalAuth(): Promise<{ + accessToken: string; + } | null> { if (!this.auth.firebaseToken) { return null; } From b6e9a9e74586a826b392330d2fc2a0132ff25a51 Mon Sep 17 00:00:00 2001 From: mansisampat Date: Tue, 27 May 2025 11:37:19 +0530 Subject: [PATCH 11/11] Remove unused method --- packages/auth/src/core/auth/firebase_internal.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/auth/src/core/auth/firebase_internal.ts b/packages/auth/src/core/auth/firebase_internal.ts index d7d5b8e3c66..caf06c49f6a 100644 --- a/packages/auth/src/core/auth/firebase_internal.ts +++ b/packages/auth/src/core/auth/firebase_internal.ts @@ -95,10 +95,6 @@ export class AuthInterop implements FirebaseAuthInternal { ); } - private assertRegionalAuthConfigured(): void { - _assert(this.auth.tenantConfig, AuthErrorCode.OPERATION_NOT_ALLOWED); - } - private updateProactiveRefresh(): void { if (this.internalListeners.size > 0) { this.auth._startProactiveRefresh();