From b45e3ebc66fbf8e67bdce3a28193186420c34e2a Mon Sep 17 00:00:00 2001 From: Gyula Soos Date: Thu, 27 Nov 2025 13:04:52 +0000 Subject: [PATCH 1/6] feat(auth): cache password policy per-app in validatePassword Cache the fetched password policy per Firebase app to avoid redundant API calls to the identity toolkit endpoint. --- .../auth/__tests__/validatePassword.test.js | 118 ++++++++++++++++++ packages/auth/lib/modular/index.js | 12 +- 2 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 packages/auth/__tests__/validatePassword.test.js diff --git a/packages/auth/__tests__/validatePassword.test.js b/packages/auth/__tests__/validatePassword.test.js new file mode 100644 index 0000000000..5f5e205a17 --- /dev/null +++ b/packages/auth/__tests__/validatePassword.test.js @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { jest, describe, it, expect, beforeEach } from '@jest/globals'; + +const mockPasswordPolicy = { + schemaVersion: 1, + customStrengthOptions: { + minPasswordLength: 8, + maxPasswordLength: 100, + containsLowercaseCharacter: true, + containsUppercaseCharacter: true, + containsNumericCharacter: true, + containsNonAlphanumericCharacter: true, + }, + allowedNonAlphanumericCharacters: ['!', '@', '#', '$', '%'], + enforcementState: 'ENFORCE', +}; + +const mockFetchPasswordPolicy = jest.fn().mockResolvedValue(mockPasswordPolicy); + +jest.unstable_mockModule('../lib/password-policy/passwordPolicyApi', () => ({ + fetchPasswordPolicy: mockFetchPasswordPolicy, +})); + +describe('validatePassword', () => { + let validatePassword; + let mockAuthDefault; + let mockAuthSecondary; + + beforeEach(async () => { + jest.resetModules(); + + mockFetchPasswordPolicy.mockClear(); + mockFetchPasswordPolicy.mockResolvedValue(mockPasswordPolicy); + + jest.unstable_mockModule('../lib/password-policy/passwordPolicyApi', () => ({ + fetchPasswordPolicy: mockFetchPasswordPolicy, + })); + + const modular = await import('../lib/modular/index.js'); + validatePassword = modular.validatePassword; + + mockAuthDefault = { + app: { + name: '[DEFAULT]', + options: { apiKey: 'test-api-key-default' }, + }, + }; + + mockAuthSecondary = { + app: { + name: 'secondaryApp', + options: { apiKey: 'test-api-key-secondary' }, + }, + }; + }); + + describe('password policy caching', () => { + it('should fetch password policy on first call', async () => { + await validatePassword(mockAuthDefault, 'Password123$'); + + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); + expect(mockFetchPasswordPolicy).toHaveBeenCalledWith(mockAuthDefault); + }); + + it('should use cached policy on subsequent calls for same app', async () => { + await validatePassword(mockAuthDefault, 'Password123$'); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); + + await validatePassword(mockAuthDefault, 'AnotherPassword1!'); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); + + await validatePassword(mockAuthDefault, 'YetAnother1@'); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); + }); + + it('should maintain separate cache per app', async () => { + await validatePassword(mockAuthDefault, 'Password123$'); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); + expect(mockFetchPasswordPolicy).toHaveBeenCalledWith(mockAuthDefault); + + await validatePassword(mockAuthSecondary, 'Password123$'); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); + expect(mockFetchPasswordPolicy).toHaveBeenCalledWith(mockAuthSecondary); + + await validatePassword(mockAuthDefault, 'AnotherPassword1!'); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); + + await validatePassword(mockAuthSecondary, 'AnotherPassword1!'); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); + }); + + it('should return correct validation status using cached policy', async () => { + const status1 = await validatePassword(mockAuthDefault, 'Password123$'); + expect(status1.isValid).toBe(true); + + const status2 = await validatePassword(mockAuthDefault, 'weak'); + expect(status2.isValid).toBe(false); + + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/auth/lib/modular/index.js b/packages/auth/lib/modular/index.js index b9385d9c46..5711966a6f 100644 --- a/packages/auth/lib/modular/index.js +++ b/packages/auth/lib/modular/index.js @@ -630,6 +630,8 @@ export function getCustomAuthDomain(auth) { return auth.getCustomAuthDomain.call(auth, MODULAR_DEPRECATION_ARG); } +const cachedPasswordPolicies = {}; + /** * Returns a password validation status * @param {Auth} auth - The Auth instance. @@ -642,10 +644,14 @@ export async function validatePassword(auth, password) { "firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.", ); } - let passwordPolicy = await fetchPasswordPolicy(auth); - const passwordPolicyImpl = await new PasswordPolicyImpl(passwordPolicy); - let status = passwordPolicyImpl.validatePassword(password); + const appName = auth.app.name; + if (!cachedPasswordPolicies[appName]) { + cachedPasswordPolicies[appName] = await fetchPasswordPolicy(auth); + } + + const passwordPolicyImpl = new PasswordPolicyImpl(cachedPasswordPolicies[appName]); + const status = passwordPolicyImpl.validatePassword(password); return status; } From 58b68b6d9a0ce025552a7bd22146538795d8b149 Mon Sep 17 00:00:00 2001 From: Gyula Soos Date: Mon, 1 Dec 2025 12:46:21 +0000 Subject: [PATCH 2/6] feat(auth): match Firebase JS SDK password policy caching behavior Refactor validatePassword to use instance-level caching with tenant support and reactive cache invalidation, matching the Firebase JS SDK implementation. Changes: - Move cache from module-level to Auth instance (_projectPasswordPolicy, _tenantPasswordPolicies) for proper multi-app and multi-tenant support - Add reactive cache invalidation on PASSWORD_DOES_NOT_MEET_REQUIREMENTS errors in createUserWithEmailAndPassword, signInWithEmailAndPassword, and confirmPasswordReset - Add schema version validation (rejects unsupported versions) - Delegate modular API validatePassword to auth instance This ensures the password policy is: - Fetched once per tenant/project (lazy loading) - Validated locally without server calls - Automatically refreshed when the server rejects a password Matches: https://github.com/firebase/firebase-js-sdk/blob/main/packages/auth/src/core/auth/auth_impl.ts --- .../auth/__tests__/validatePassword.test.js | 199 +++++++++++++++--- packages/auth/lib/index.js | 85 +++++++- packages/auth/lib/modular/index.js | 14 +- .../lib/password-policy/passwordPolicyApi.js | 11 +- 4 files changed, 259 insertions(+), 50 deletions(-) diff --git a/packages/auth/__tests__/validatePassword.test.js b/packages/auth/__tests__/validatePassword.test.js index 5f5e205a17..231312b6a5 100644 --- a/packages/auth/__tests__/validatePassword.test.js +++ b/packages/auth/__tests__/validatePassword.test.js @@ -39,8 +39,7 @@ jest.unstable_mockModule('../lib/password-policy/passwordPolicyApi', () => ({ describe('validatePassword', () => { let validatePassword; - let mockAuthDefault; - let mockAuthSecondary; + let mockAuth; beforeEach(async () => { jest.resetModules(); @@ -55,64 +54,216 @@ describe('validatePassword', () => { const modular = await import('../lib/modular/index.js'); validatePassword = modular.validatePassword; - mockAuthDefault = { + // Create a mock auth instance that mimics FirebaseAuthModule + mockAuth = { app: { name: '[DEFAULT]', options: { apiKey: 'test-api-key-default' }, }, + _tenantId: null, + _projectPasswordPolicy: null, + _tenantPasswordPolicies: {}, }; - mockAuthSecondary = { - app: { - name: 'secondaryApp', - options: { apiKey: 'test-api-key-secondary' }, - }, + const { PasswordPolicyImpl } = await import('../lib/password-policy/PasswordPolicyImpl.js'); + + mockAuth._getPasswordPolicyInternal = function () { + if (this._tenantId === null) { + return this._projectPasswordPolicy; + } + return this._tenantPasswordPolicies[this._tenantId]; + }; + + mockAuth._updatePasswordPolicy = async function () { + const response = await mockFetchPasswordPolicy(this); + const passwordPolicy = new PasswordPolicyImpl(response); + if (this._tenantId === null) { + this._projectPasswordPolicy = passwordPolicy; + } else { + this._tenantPasswordPolicies[this._tenantId] = passwordPolicy; + } + }; + + mockAuth._recachePasswordPolicy = async function () { + if (this._getPasswordPolicyInternal()) { + await this._updatePasswordPolicy(); + } + }; + + mockAuth.validatePassword = async function (password) { + if (!this._getPasswordPolicyInternal()) { + await this._updatePasswordPolicy(); + } + const passwordPolicy = this._getPasswordPolicyInternal(); + + if (passwordPolicy.schemaVersion !== 1) { + throw new Error( + 'auth/unsupported-password-policy-schema-version: The password policy received from the backend uses a schema version that is not supported by this version of the SDK.', + ); + } + + return passwordPolicy.validatePassword(password); }; }); - describe('password policy caching', () => { + describe('caching behavior', () => { it('should fetch password policy on first call', async () => { - await validatePassword(mockAuthDefault, 'Password123$'); + await validatePassword(mockAuth, 'Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); - expect(mockFetchPasswordPolicy).toHaveBeenCalledWith(mockAuthDefault); + expect(mockFetchPasswordPolicy).toHaveBeenCalledWith(mockAuth); }); - it('should use cached policy on subsequent calls for same app', async () => { - await validatePassword(mockAuthDefault, 'Password123$'); + it('should use cached policy on subsequent calls for same auth instance', async () => { + await validatePassword(mockAuth, 'Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); - await validatePassword(mockAuthDefault, 'AnotherPassword1!'); + await validatePassword(mockAuth, 'AnotherPassword1!'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); - await validatePassword(mockAuthDefault, 'YetAnother1@'); + await validatePassword(mockAuth, 'YetAnother1@'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); }); - it('should maintain separate cache per app', async () => { - await validatePassword(mockAuthDefault, 'Password123$'); + it('should cache at project level when tenantId is null', async () => { + mockAuth._tenantId = null; + + await validatePassword(mockAuth, 'Password123$'); + + expect(mockAuth._projectPasswordPolicy).not.toBeNull(); + expect(Object.keys(mockAuth._tenantPasswordPolicies).length).toBe(0); + }); + + it('should cache separately per tenant', async () => { + // First tenant + mockAuth._tenantId = 'tenant-1'; + await validatePassword(mockAuth, 'Password123$'); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); + + // Same tenant should use cache + await validatePassword(mockAuth, 'AnotherPassword1!'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); - expect(mockFetchPasswordPolicy).toHaveBeenCalledWith(mockAuthDefault); - await validatePassword(mockAuthSecondary, 'Password123$'); + // Different tenant should fetch again + mockAuth._tenantId = 'tenant-2'; + await validatePassword(mockAuth, 'Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); - expect(mockFetchPasswordPolicy).toHaveBeenCalledWith(mockAuthSecondary); - await validatePassword(mockAuthDefault, 'AnotherPassword1!'); + // Back to first tenant should use its cache + mockAuth._tenantId = 'tenant-1'; + await validatePassword(mockAuth, 'Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); - await validatePassword(mockAuthSecondary, 'AnotherPassword1!'); + // Verify both tenant policies are cached + expect(mockAuth._tenantPasswordPolicies['tenant-1']).toBeDefined(); + expect(mockAuth._tenantPasswordPolicies['tenant-2']).toBeDefined(); + }); + + it('should keep project and tenant caches separate', async () => { + // Project level (no tenant) + mockAuth._tenantId = null; + await validatePassword(mockAuth, 'Password123$'); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); + + // Tenant level + mockAuth._tenantId = 'tenant-1'; + await validatePassword(mockAuth, 'Password123$'); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); + + // Back to project level should use project cache + mockAuth._tenantId = null; + await validatePassword(mockAuth, 'Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); + + // Verify both caches exist + expect(mockAuth._projectPasswordPolicy).not.toBeNull(); + expect(mockAuth._tenantPasswordPolicies['tenant-1']).toBeDefined(); }); it('should return correct validation status using cached policy', async () => { - const status1 = await validatePassword(mockAuthDefault, 'Password123$'); + const status1 = await validatePassword(mockAuth, 'Password123$'); expect(status1.isValid).toBe(true); - const status2 = await validatePassword(mockAuthDefault, 'weak'); + const status2 = await validatePassword(mockAuth, 'weak'); expect(status2.isValid).toBe(false); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); }); }); + + describe('schema validation', () => { + it('should throw error on unsupported schema version', async () => { + const unsupportedPolicy = { + ...mockPasswordPolicy, + schemaVersion: 2, + }; + mockFetchPasswordPolicy.mockResolvedValueOnce(unsupportedPolicy); + + await expect(validatePassword(mockAuth, 'Password123$')).rejects.toThrow( + 'auth/unsupported-password-policy-schema-version', + ); + }); + + it('should accept schema version 1', async () => { + const validPolicy = { + ...mockPasswordPolicy, + schemaVersion: 1, + }; + mockFetchPasswordPolicy.mockResolvedValueOnce(validPolicy); + + const status = await validatePassword(mockAuth, 'Password123$'); + expect(status.isValid).toBe(true); + }); + }); + + describe('cache invalidation', () => { + it('should refresh cache when _recachePasswordPolicy is called with existing cache', async () => { + // First call caches the policy + await validatePassword(mockAuth, 'Password123$'); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); + + // Simulate cache invalidation + await mockAuth._recachePasswordPolicy(); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); + }); + + it('should not fetch when _recachePasswordPolicy is called without existing cache', async () => { + // No prior validation, so no cache exists + await mockAuth._recachePasswordPolicy(); + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(0); + }); + + it('should refresh correct tenant cache on invalidation', async () => { + // Cache for tenant-1 + mockAuth._tenantId = 'tenant-1'; + await validatePassword(mockAuth, 'Password123$'); + + // Cache for tenant-2 + mockAuth._tenantId = 'tenant-2'; + await validatePassword(mockAuth, 'Password123$'); + + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); + + // Invalidate tenant-1 cache + mockAuth._tenantId = 'tenant-1'; + await mockAuth._recachePasswordPolicy(); + + // Should have fetched again for tenant-1 + expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(3); + }); + }); + + describe('input validation', () => { + it('should throw error for null password', async () => { + await expect(validatePassword(mockAuth, null)).rejects.toThrow( + "firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.", + ); + }); + + it('should throw error for undefined password', async () => { + await expect(validatePassword(mockAuth, undefined)).rejects.toThrow( + "firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.", + ); + }); + }); }); diff --git a/packages/auth/lib/index.js b/packages/auth/lib/index.js index 21d3e8db1d..8a8a87e627 100644 --- a/packages/auth/lib/index.js +++ b/packages/auth/lib/index.js @@ -51,6 +51,10 @@ import TwitterAuthProvider from './providers/TwitterAuthProvider'; import { TotpSecret } from './TotpSecret'; import version from './version'; import fallBackModule from './web/RNFBAuthModule'; +import { fetchPasswordPolicy } from './password-policy/passwordPolicyApi'; +import { PasswordPolicyImpl } from './password-policy/PasswordPolicyImpl'; + +const EXPECTED_PASSWORD_POLICY_SCHEMA_VERSION = 1; const PhoneAuthState = { CODE_SENT: 'sent', @@ -103,6 +107,8 @@ class FirebaseAuthModule extends FirebaseModule { this._authResult = false; this._languageCode = this.native.APP_LANGUAGE[this.app._name]; this._tenantId = null; + this._projectPasswordPolicy = null; + this._tenantPasswordPolicies = {}; if (!this.languageCode) { this._languageCode = this.native.APP_LANGUAGE['[DEFAULT]']; @@ -338,13 +344,37 @@ class FirebaseAuthModule extends FirebaseModule { createUserWithEmailAndPassword(email, password) { return this.native .createUserWithEmailAndPassword(email, password) - .then(userCredential => this._setUserCredential(userCredential)); + .then(userCredential => this._setUserCredential(userCredential)) + .catch(error => { + if (error.code === 'auth/password-does-not-meet-requirements') { + return this._recachePasswordPolicy() + .catch(() => { + // Silently ignore recache failures - the original error matters more + }) + .then(() => { + throw error; + }); + } + throw error; + }); } signInWithEmailAndPassword(email, password) { return this.native .signInWithEmailAndPassword(email, password) - .then(userCredential => this._setUserCredential(userCredential)); + .then(userCredential => this._setUserCredential(userCredential)) + .catch(error => { + if (error.code === 'auth/password-does-not-meet-requirements') { + return this._recachePasswordPolicy() + .catch(() => { + // Silently ignore recache failures - the original error matters more + }) + .then(() => { + throw error; + }); + } + throw error; + }); } signInWithCustomToken(customToken) { @@ -382,7 +412,18 @@ class FirebaseAuthModule extends FirebaseModule { } confirmPasswordReset(code, newPassword) { - return this.native.confirmPasswordReset(code, newPassword); + return this.native.confirmPasswordReset(code, newPassword).catch(error => { + if (error.code === 'auth/password-does-not-meet-requirements') { + return this._recachePasswordPolicy() + .catch(() => { + // Silently ignore recache failures - the original error matters more + }) + .then(() => { + throw error; + }); + } + throw error; + }); } applyActionCode(code) { @@ -491,6 +532,44 @@ class FirebaseAuthModule extends FirebaseModule { getCustomAuthDomain() { return this.native.getCustomAuthDomain(); } + + _getPasswordPolicyInternal() { + if (this._tenantId === null) { + return this._projectPasswordPolicy; + } + return this._tenantPasswordPolicies[this._tenantId]; + } + + async _updatePasswordPolicy() { + const response = await fetchPasswordPolicy(this); + const passwordPolicy = new PasswordPolicyImpl(response); + if (this._tenantId === null) { + this._projectPasswordPolicy = passwordPolicy; + } else { + this._tenantPasswordPolicies[this._tenantId] = passwordPolicy; + } + } + + async _recachePasswordPolicy() { + if (this._getPasswordPolicyInternal()) { + await this._updatePasswordPolicy(); + } + } + + async validatePassword(password) { + if (!this._getPasswordPolicyInternal()) { + await this._updatePasswordPolicy(); + } + const passwordPolicy = this._getPasswordPolicyInternal(); + + if (passwordPolicy.schemaVersion !== EXPECTED_PASSWORD_POLICY_SCHEMA_VERSION) { + throw new Error( + 'auth/unsupported-password-policy-schema-version: The password policy received from the backend uses a schema version that is not supported by this version of the SDK.', + ); + } + + return passwordPolicy.validatePassword(password); + } } // import { SDK_VERSION } from '@react-native-firebase/auth'; diff --git a/packages/auth/lib/modular/index.js b/packages/auth/lib/modular/index.js index 5711966a6f..42bbf5d11d 100644 --- a/packages/auth/lib/modular/index.js +++ b/packages/auth/lib/modular/index.js @@ -16,8 +16,6 @@ */ import { getApp } from '@react-native-firebase/app'; -import { fetchPasswordPolicy } from '../password-policy/passwordPolicyApi'; -import { PasswordPolicyImpl } from '../password-policy/PasswordPolicyImpl'; import { MultiFactorUser } from '../multiFactor'; import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/lib/common'; @@ -630,8 +628,6 @@ export function getCustomAuthDomain(auth) { return auth.getCustomAuthDomain.call(auth, MODULAR_DEPRECATION_ARG); } -const cachedPasswordPolicies = {}; - /** * Returns a password validation status * @param {Auth} auth - The Auth instance. @@ -645,13 +641,5 @@ export async function validatePassword(auth, password) { ); } - const appName = auth.app.name; - if (!cachedPasswordPolicies[appName]) { - cachedPasswordPolicies[appName] = await fetchPasswordPolicy(auth); - } - - const passwordPolicyImpl = new PasswordPolicyImpl(cachedPasswordPolicies[appName]); - const status = passwordPolicyImpl.validatePassword(password); - - return status; + return auth.validatePassword(password); } diff --git a/packages/auth/lib/password-policy/passwordPolicyApi.js b/packages/auth/lib/password-policy/passwordPolicyApi.js index 8f68cf9796..a890309c5f 100644 --- a/packages/auth/lib/password-policy/passwordPolicyApi.js +++ b/packages/auth/lib/password-policy/passwordPolicyApi.js @@ -23,8 +23,6 @@ * @throws {Error} Throws an error if the request fails or encounters an issue. */ export async function fetchPasswordPolicy(auth) { - let schemaVersion = 1; - try { // Identity toolkit API endpoint for password policy. Ensure this is enabled on Google cloud. const baseURL = 'https://identitytoolkit.googleapis.com/v2/passwordPolicy?key='; @@ -37,14 +35,7 @@ export async function fetchPasswordPolicy(auth) { `firebase.auth().validatePassword(*) failed to fetch password policy from Firebase Console: ${response.statusText}. Details: ${errorDetails}`, ); } - const passwordPolicy = await response.json(); - - if (passwordPolicy.schemaVersion !== schemaVersion) { - throw new Error( - `Password policy schema version mismatch. Expected: ${schemaVersion}, received: ${passwordPolicy.schemaVersion}`, - ); - } - return passwordPolicy; + return await response.json(); } catch (error) { throw new Error( `firebase.auth().validatePassword(*) Failed to fetch password policy: ${error.message}`, From 24e3ca1b0e24c2cdda0d15ac152a4df498718628 Mon Sep 17 00:00:00 2001 From: Gyula Soos Date: Tue, 2 Dec 2025 09:59:42 +0000 Subject: [PATCH 3/6] feat(auth): implement password policy mixin for caching and validation --- .../auth/__tests__/validatePassword.test.js | 195 +++++++++++------- packages/auth/lib/index.js | 141 +++++-------- .../password-policy/PasswordPolicyMixin.js | 66 ++++++ 3 files changed, 239 insertions(+), 163 deletions(-) create mode 100644 packages/auth/lib/password-policy/PasswordPolicyMixin.js diff --git a/packages/auth/__tests__/validatePassword.test.js b/packages/auth/__tests__/validatePassword.test.js index 231312b6a5..3cc0aae4ae 100644 --- a/packages/auth/__tests__/validatePassword.test.js +++ b/packages/auth/__tests__/validatePassword.test.js @@ -15,7 +15,9 @@ * */ -import { jest, describe, it, expect, beforeEach } from '@jest/globals'; +import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals'; +import { PasswordPolicyImpl } from '../lib/password-policy/PasswordPolicyImpl.js'; +import { PasswordPolicyMixin } from '../lib/password-policy/PasswordPolicyMixin.js'; const mockPasswordPolicy = { schemaVersion: 1, @@ -31,30 +33,65 @@ const mockPasswordPolicy = { enforcementState: 'ENFORCE', }; -const mockFetchPasswordPolicy = jest.fn().mockResolvedValue(mockPasswordPolicy); +describe('PasswordPolicyMixin', () => { + describe('_getPasswordPolicyInternal', () => { + it('should return project policy when tenantId is null', () => { + const projectPolicy = new PasswordPolicyImpl(mockPasswordPolicy); + const auth = { + _tenantId: null, + _projectPasswordPolicy: projectPolicy, + _tenantPasswordPolicies: {}, + }; + Object.assign(auth, { + _getPasswordPolicyInternal: PasswordPolicyMixin._getPasswordPolicyInternal, + }); -jest.unstable_mockModule('../lib/password-policy/passwordPolicyApi', () => ({ - fetchPasswordPolicy: mockFetchPasswordPolicy, -})); + const result = auth._getPasswordPolicyInternal(); -describe('validatePassword', () => { - let validatePassword; - let mockAuth; + expect(result).toBe(projectPolicy); + }); - beforeEach(async () => { - jest.resetModules(); + it('should return tenant policy when tenantId is set', () => { + const tenantPolicy = new PasswordPolicyImpl(mockPasswordPolicy); + const auth = { + _tenantId: 'tenant-1', + _projectPasswordPolicy: null, + _tenantPasswordPolicies: { 'tenant-1': tenantPolicy }, + }; + Object.assign(auth, { + _getPasswordPolicyInternal: PasswordPolicyMixin._getPasswordPolicyInternal, + }); + + const result = auth._getPasswordPolicyInternal(); - mockFetchPasswordPolicy.mockClear(); - mockFetchPasswordPolicy.mockResolvedValue(mockPasswordPolicy); + expect(result).toBe(tenantPolicy); + }); - jest.unstable_mockModule('../lib/password-policy/passwordPolicyApi', () => ({ - fetchPasswordPolicy: mockFetchPasswordPolicy, - })); + it('should return undefined when no policy is cached', () => { + const auth = { + _tenantId: null, + _projectPasswordPolicy: null, + _tenantPasswordPolicies: {}, + }; + Object.assign(auth, { + _getPasswordPolicyInternal: PasswordPolicyMixin._getPasswordPolicyInternal, + }); - const modular = await import('../lib/modular/index.js'); - validatePassword = modular.validatePassword; + const result = auth._getPasswordPolicyInternal(); + + expect(result).toBeNull(); + }); + }); +}); + +describe('validatePassword (integration)', () => { + let mockAuth; + let mockFetchPasswordPolicy; + + beforeEach(() => { + mockFetchPasswordPolicy = jest.fn().mockResolvedValue(mockPasswordPolicy); - // Create a mock auth instance that mimics FirebaseAuthModule + // Create mock auth with the mixin, but override _updatePasswordPolicy to use our mock mockAuth = { app: { name: '[DEFAULT]', @@ -65,15 +102,14 @@ describe('validatePassword', () => { _tenantPasswordPolicies: {}, }; - const { PasswordPolicyImpl } = await import('../lib/password-policy/PasswordPolicyImpl.js'); - - mockAuth._getPasswordPolicyInternal = function () { - if (this._tenantId === null) { - return this._projectPasswordPolicy; - } - return this._tenantPasswordPolicies[this._tenantId]; - }; + // Apply the real mixin methods + Object.assign(mockAuth, { + _getPasswordPolicyInternal: PasswordPolicyMixin._getPasswordPolicyInternal, + _recachePasswordPolicy: PasswordPolicyMixin._recachePasswordPolicy, + validatePassword: PasswordPolicyMixin.validatePassword, + }); + // Override _updatePasswordPolicy to use our mock fetch mockAuth._updatePasswordPolicy = async function () { const response = await mockFetchPasswordPolicy(this); const passwordPolicy = new PasswordPolicyImpl(response); @@ -83,52 +119,33 @@ describe('validatePassword', () => { this._tenantPasswordPolicies[this._tenantId] = passwordPolicy; } }; + }); - mockAuth._recachePasswordPolicy = async function () { - if (this._getPasswordPolicyInternal()) { - await this._updatePasswordPolicy(); - } - }; - - mockAuth.validatePassword = async function (password) { - if (!this._getPasswordPolicyInternal()) { - await this._updatePasswordPolicy(); - } - const passwordPolicy = this._getPasswordPolicyInternal(); - - if (passwordPolicy.schemaVersion !== 1) { - throw new Error( - 'auth/unsupported-password-policy-schema-version: The password policy received from the backend uses a schema version that is not supported by this version of the SDK.', - ); - } - - return passwordPolicy.validatePassword(password); - }; + afterEach(() => { + jest.clearAllMocks(); }); describe('caching behavior', () => { it('should fetch password policy on first call', async () => { - await validatePassword(mockAuth, 'Password123$'); + await mockAuth.validatePassword('Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); expect(mockFetchPasswordPolicy).toHaveBeenCalledWith(mockAuth); }); it('should use cached policy on subsequent calls for same auth instance', async () => { - await validatePassword(mockAuth, 'Password123$'); + await mockAuth.validatePassword('Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); - await validatePassword(mockAuth, 'AnotherPassword1!'); + await mockAuth.validatePassword('AnotherPassword1!'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); - await validatePassword(mockAuth, 'YetAnother1@'); + await mockAuth.validatePassword('YetAnother1@'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); }); it('should cache at project level when tenantId is null', async () => { - mockAuth._tenantId = null; - - await validatePassword(mockAuth, 'Password123$'); + await mockAuth.validatePassword('Password123$'); expect(mockAuth._projectPasswordPolicy).not.toBeNull(); expect(Object.keys(mockAuth._tenantPasswordPolicies).length).toBe(0); @@ -137,21 +154,21 @@ describe('validatePassword', () => { it('should cache separately per tenant', async () => { // First tenant mockAuth._tenantId = 'tenant-1'; - await validatePassword(mockAuth, 'Password123$'); + await mockAuth.validatePassword('Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); // Same tenant should use cache - await validatePassword(mockAuth, 'AnotherPassword1!'); + await mockAuth.validatePassword('AnotherPassword1!'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); // Different tenant should fetch again mockAuth._tenantId = 'tenant-2'; - await validatePassword(mockAuth, 'Password123$'); + await mockAuth.validatePassword('Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); // Back to first tenant should use its cache mockAuth._tenantId = 'tenant-1'; - await validatePassword(mockAuth, 'Password123$'); + await mockAuth.validatePassword('Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); // Verify both tenant policies are cached @@ -161,18 +178,17 @@ describe('validatePassword', () => { it('should keep project and tenant caches separate', async () => { // Project level (no tenant) - mockAuth._tenantId = null; - await validatePassword(mockAuth, 'Password123$'); + await mockAuth.validatePassword('Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); // Tenant level mockAuth._tenantId = 'tenant-1'; - await validatePassword(mockAuth, 'Password123$'); + await mockAuth.validatePassword('Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); // Back to project level should use project cache mockAuth._tenantId = null; - await validatePassword(mockAuth, 'Password123$'); + await mockAuth.validatePassword('Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); // Verify both caches exist @@ -181,10 +197,10 @@ describe('validatePassword', () => { }); it('should return correct validation status using cached policy', async () => { - const status1 = await validatePassword(mockAuth, 'Password123$'); + const status1 = await mockAuth.validatePassword('Password123$'); expect(status1.isValid).toBe(true); - const status2 = await validatePassword(mockAuth, 'weak'); + const status2 = await mockAuth.validatePassword('weak'); expect(status2.isValid).toBe(false); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); @@ -199,7 +215,7 @@ describe('validatePassword', () => { }; mockFetchPasswordPolicy.mockResolvedValueOnce(unsupportedPolicy); - await expect(validatePassword(mockAuth, 'Password123$')).rejects.toThrow( + await expect(mockAuth.validatePassword('Password123$')).rejects.toThrow( 'auth/unsupported-password-policy-schema-version', ); }); @@ -211,7 +227,7 @@ describe('validatePassword', () => { }; mockFetchPasswordPolicy.mockResolvedValueOnce(validPolicy); - const status = await validatePassword(mockAuth, 'Password123$'); + const status = await mockAuth.validatePassword('Password123$'); expect(status.isValid).toBe(true); }); }); @@ -219,7 +235,7 @@ describe('validatePassword', () => { describe('cache invalidation', () => { it('should refresh cache when _recachePasswordPolicy is called with existing cache', async () => { // First call caches the policy - await validatePassword(mockAuth, 'Password123$'); + await mockAuth.validatePassword('Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(1); // Simulate cache invalidation @@ -236,11 +252,11 @@ describe('validatePassword', () => { it('should refresh correct tenant cache on invalidation', async () => { // Cache for tenant-1 mockAuth._tenantId = 'tenant-1'; - await validatePassword(mockAuth, 'Password123$'); + await mockAuth.validatePassword('Password123$'); // Cache for tenant-2 mockAuth._tenantId = 'tenant-2'; - await validatePassword(mockAuth, 'Password123$'); + await mockAuth.validatePassword('Password123$'); expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(2); @@ -252,18 +268,39 @@ describe('validatePassword', () => { expect(mockFetchPasswordPolicy).toHaveBeenCalledTimes(3); }); }); +}); - describe('input validation', () => { - it('should throw error for null password', async () => { - await expect(validatePassword(mockAuth, null)).rejects.toThrow( - "firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.", - ); - }); +describe('validatePassword (modular API)', () => { + let validatePassword; + let mockAuth; - it('should throw error for undefined password', async () => { - await expect(validatePassword(mockAuth, undefined)).rejects.toThrow( - "firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.", - ); - }); + beforeEach(async () => { + const modular = await import('../lib/modular/index.js'); + validatePassword = modular.validatePassword; + + mockAuth = { + validatePassword: jest.fn(), + }; + }); + + it('should throw error for null password', async () => { + await expect(validatePassword(mockAuth, null)).rejects.toThrow( + "firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.", + ); + }); + + it('should throw error for undefined password', async () => { + await expect(validatePassword(mockAuth, undefined)).rejects.toThrow( + "firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.", + ); + }); + + it('should delegate to auth.validatePassword for valid password', async () => { + mockAuth.validatePassword.mockResolvedValue({ isValid: true }); + + const result = await validatePassword(mockAuth, 'Password123$'); + + expect(mockAuth.validatePassword).toHaveBeenCalledWith('Password123$'); + expect(result).toEqual({ isValid: true }); }); }); diff --git a/packages/auth/lib/index.js b/packages/auth/lib/index.js index 8a8a87e627..5bf86bd531 100644 --- a/packages/auth/lib/index.js +++ b/packages/auth/lib/index.js @@ -51,10 +51,7 @@ import TwitterAuthProvider from './providers/TwitterAuthProvider'; import { TotpSecret } from './TotpSecret'; import version from './version'; import fallBackModule from './web/RNFBAuthModule'; -import { fetchPasswordPolicy } from './password-policy/passwordPolicyApi'; -import { PasswordPolicyImpl } from './password-policy/PasswordPolicyImpl'; - -const EXPECTED_PASSWORD_POLICY_SCHEMA_VERSION = 1; +import { PasswordPolicyMixin } from './password-policy/PasswordPolicyMixin'; const PhoneAuthState = { CODE_SENT: 'sent', @@ -342,39 +339,45 @@ class FirebaseAuthModule extends FirebaseModule { } createUserWithEmailAndPassword(email, password) { - return this.native - .createUserWithEmailAndPassword(email, password) - .then(userCredential => this._setUserCredential(userCredential)) - .catch(error => { - if (error.code === 'auth/password-does-not-meet-requirements') { - return this._recachePasswordPolicy() - .catch(() => { - // Silently ignore recache failures - the original error matters more - }) - .then(() => { - throw error; - }); - } - throw error; - }); + return ( + this.native + .createUserWithEmailAndPassword(email, password) + .then(userCredential => this._setUserCredential(userCredential)) + /* istanbul ignore next - native error handling cannot be unit tested */ + .catch(error => { + if (error.code === 'auth/password-does-not-meet-requirements') { + return this._recachePasswordPolicy() + .catch(() => { + // Silently ignore recache failures - the original error matters more + }) + .then(() => { + throw error; + }); + } + throw error; + }) + ); } signInWithEmailAndPassword(email, password) { - return this.native - .signInWithEmailAndPassword(email, password) - .then(userCredential => this._setUserCredential(userCredential)) - .catch(error => { - if (error.code === 'auth/password-does-not-meet-requirements') { - return this._recachePasswordPolicy() - .catch(() => { - // Silently ignore recache failures - the original error matters more - }) - .then(() => { - throw error; - }); - } - throw error; - }); + return ( + this.native + .signInWithEmailAndPassword(email, password) + .then(userCredential => this._setUserCredential(userCredential)) + /* istanbul ignore next - native error handling cannot be unit tested */ + .catch(error => { + if (error.code === 'auth/password-does-not-meet-requirements') { + return this._recachePasswordPolicy() + .catch(() => { + // Silently ignore recache failures - the original error matters more + }) + .then(() => { + throw error; + }); + } + throw error; + }) + ); } signInWithCustomToken(customToken) { @@ -412,18 +415,23 @@ class FirebaseAuthModule extends FirebaseModule { } confirmPasswordReset(code, newPassword) { - return this.native.confirmPasswordReset(code, newPassword).catch(error => { - if (error.code === 'auth/password-does-not-meet-requirements') { - return this._recachePasswordPolicy() - .catch(() => { - // Silently ignore recache failures - the original error matters more - }) - .then(() => { - throw error; - }); - } - throw error; - }); + return ( + this.native + .confirmPasswordReset(code, newPassword) + /* istanbul ignore next - native error handling cannot be unit tested */ + .catch(error => { + if (error.code === 'auth/password-does-not-meet-requirements') { + return this._recachePasswordPolicy() + .catch(() => { + // Silently ignore recache failures - the original error matters more + }) + .then(() => { + throw error; + }); + } + throw error; + }) + ); } applyActionCode(code) { @@ -532,46 +540,11 @@ class FirebaseAuthModule extends FirebaseModule { getCustomAuthDomain() { return this.native.getCustomAuthDomain(); } - - _getPasswordPolicyInternal() { - if (this._tenantId === null) { - return this._projectPasswordPolicy; - } - return this._tenantPasswordPolicies[this._tenantId]; - } - - async _updatePasswordPolicy() { - const response = await fetchPasswordPolicy(this); - const passwordPolicy = new PasswordPolicyImpl(response); - if (this._tenantId === null) { - this._projectPasswordPolicy = passwordPolicy; - } else { - this._tenantPasswordPolicies[this._tenantId] = passwordPolicy; - } - } - - async _recachePasswordPolicy() { - if (this._getPasswordPolicyInternal()) { - await this._updatePasswordPolicy(); - } - } - - async validatePassword(password) { - if (!this._getPasswordPolicyInternal()) { - await this._updatePasswordPolicy(); - } - const passwordPolicy = this._getPasswordPolicyInternal(); - - if (passwordPolicy.schemaVersion !== EXPECTED_PASSWORD_POLICY_SCHEMA_VERSION) { - throw new Error( - 'auth/unsupported-password-policy-schema-version: The password policy received from the backend uses a schema version that is not supported by this version of the SDK.', - ); - } - - return passwordPolicy.validatePassword(password); - } } +// Apply password policy mixin to FirebaseAuthModule +Object.assign(FirebaseAuthModule.prototype, PasswordPolicyMixin); + // import { SDK_VERSION } from '@react-native-firebase/auth'; export const SDK_VERSION = version; diff --git a/packages/auth/lib/password-policy/PasswordPolicyMixin.js b/packages/auth/lib/password-policy/PasswordPolicyMixin.js new file mode 100644 index 0000000000..e8c916dd3a --- /dev/null +++ b/packages/auth/lib/password-policy/PasswordPolicyMixin.js @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { fetchPasswordPolicy } from './passwordPolicyApi'; +import { PasswordPolicyImpl } from './PasswordPolicyImpl'; + +const EXPECTED_PASSWORD_POLICY_SCHEMA_VERSION = 1; + +/** + * Password policy mixin - provides password policy caching and validation. + * Expects the target object to have: _tenantId, _projectPasswordPolicy, + * _tenantPasswordPolicies, and app.options.apiKey + */ +export const PasswordPolicyMixin = { + _getPasswordPolicyInternal() { + if (this._tenantId === null) { + return this._projectPasswordPolicy; + } + return this._tenantPasswordPolicies[this._tenantId]; + }, + + async _updatePasswordPolicy() { + const response = await fetchPasswordPolicy(this); + const passwordPolicy = new PasswordPolicyImpl(response); + if (this._tenantId === null) { + this._projectPasswordPolicy = passwordPolicy; + } else { + this._tenantPasswordPolicies[this._tenantId] = passwordPolicy; + } + }, + + async _recachePasswordPolicy() { + if (this._getPasswordPolicyInternal()) { + await this._updatePasswordPolicy(); + } + }, + + async validatePassword(password) { + if (!this._getPasswordPolicyInternal()) { + await this._updatePasswordPolicy(); + } + const passwordPolicy = this._getPasswordPolicyInternal(); + + if (passwordPolicy.schemaVersion !== EXPECTED_PASSWORD_POLICY_SCHEMA_VERSION) { + throw new Error( + 'auth/unsupported-password-policy-schema-version: The password policy received from the backend uses a schema version that is not supported by this version of the SDK.', + ); + } + + return passwordPolicy.validatePassword(password); + }, +}; From 00db8126e26948135cdfd1b5648692dfb21b8650 Mon Sep 17 00:00:00 2001 From: Gyula Soos Date: Tue, 2 Dec 2025 14:58:00 +0000 Subject: [PATCH 4/6] feat(auth): update validatePassword to include modular method call argument --- packages/auth/__tests__/validatePassword.test.js | 5 ++++- packages/auth/lib/modular/index.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/auth/__tests__/validatePassword.test.js b/packages/auth/__tests__/validatePassword.test.js index 3cc0aae4ae..bc6a78eb52 100644 --- a/packages/auth/__tests__/validatePassword.test.js +++ b/packages/auth/__tests__/validatePassword.test.js @@ -300,7 +300,10 @@ describe('validatePassword (modular API)', () => { const result = await validatePassword(mockAuth, 'Password123$'); - expect(mockAuth.validatePassword).toHaveBeenCalledWith('Password123$'); + expect(mockAuth.validatePassword).toHaveBeenCalledWith( + 'Password123$', + 'react-native-firebase-modular-method-call', + ); expect(result).toEqual({ isValid: true }); }); }); diff --git a/packages/auth/lib/modular/index.js b/packages/auth/lib/modular/index.js index 42bbf5d11d..79cbcac38b 100644 --- a/packages/auth/lib/modular/index.js +++ b/packages/auth/lib/modular/index.js @@ -641,5 +641,5 @@ export async function validatePassword(auth, password) { ); } - return auth.validatePassword(password); + return auth.validatePassword.call(auth, password, MODULAR_DEPRECATION_ARG); } From e5223edddcba33a7b96014862fea1f145aa68a8d Mon Sep 17 00:00:00 2001 From: Gyula Soos Date: Tue, 2 Dec 2025 15:11:42 +0000 Subject: [PATCH 5/6] feat(auth): remove module.exports for fetchPasswordPolicy function --- packages/auth/lib/password-policy/passwordPolicyApi.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/auth/lib/password-policy/passwordPolicyApi.js b/packages/auth/lib/password-policy/passwordPolicyApi.js index a890309c5f..7d375deae8 100644 --- a/packages/auth/lib/password-policy/passwordPolicyApi.js +++ b/packages/auth/lib/password-policy/passwordPolicyApi.js @@ -42,5 +42,3 @@ export async function fetchPasswordPolicy(auth) { ); } } - -module.exports = { fetchPasswordPolicy }; From 4c482af2ab6550976f6512722dc1e0fa10f0c65d Mon Sep 17 00:00:00 2001 From: Gyula Soos Date: Mon, 8 Dec 2025 09:36:57 +0000 Subject: [PATCH 6/6] feat(auth): add validation for auth instance in validatePassword function --- packages/auth/__tests__/validatePassword.test.js | 13 +++++++++++++ packages/auth/lib/modular/index.js | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/packages/auth/__tests__/validatePassword.test.js b/packages/auth/__tests__/validatePassword.test.js index bc6a78eb52..daecca77dd 100644 --- a/packages/auth/__tests__/validatePassword.test.js +++ b/packages/auth/__tests__/validatePassword.test.js @@ -279,10 +279,23 @@ describe('validatePassword (modular API)', () => { validatePassword = modular.validatePassword; mockAuth = { + app: { name: '[DEFAULT]', options: { apiKey: 'test-api-key' } }, validatePassword: jest.fn(), }; }); + it('should throw error for undefined auth', async () => { + await expect(validatePassword(undefined, 'Password123$')).rejects.toThrow( + "firebase.auth().validatePassword(*) 'auth' must be a valid Auth instance with an 'app' property", + ); + }); + + it('should throw error for auth without app property', async () => { + await expect(validatePassword({}, 'Password123$')).rejects.toThrow( + "firebase.auth().validatePassword(*) 'auth' must be a valid Auth instance with an 'app' property", + ); + }); + it('should throw error for null password', async () => { await expect(validatePassword(mockAuth, null)).rejects.toThrow( "firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.", diff --git a/packages/auth/lib/modular/index.js b/packages/auth/lib/modular/index.js index 79cbcac38b..f99e4293ec 100644 --- a/packages/auth/lib/modular/index.js +++ b/packages/auth/lib/modular/index.js @@ -635,6 +635,12 @@ export function getCustomAuthDomain(auth) { * @returns {Promise} */ export async function validatePassword(auth, password) { + if (!auth || !auth.app) { + throw new Error( + "firebase.auth().validatePassword(*) 'auth' must be a valid Auth instance with an 'app' property. Received: undefined", + ); + } + if (password === null || password === undefined) { throw new Error( "firebase.auth().validatePassword(*) expected 'password' to be a non-null or a defined value.",