From c1116f1b58ec34bf14b1db71ad5d9b8bd5572c50 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 19 Jun 2025 14:29:42 +0100 Subject: [PATCH 01/19] feat(auth): PasswordPolicy Support --- .../password_policy/password_policy_api.dart | 36 +++++++++++++++++++ .../password_policy/password_policy_impl.dart | 0 .../firebase_auth/firebase_auth/pubspec.yaml | 2 +- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart create mode 100644 packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart new file mode 100644 index 000000000000..73802f93391f --- /dev/null +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart @@ -0,0 +1,36 @@ +import 'package:http/http.dart' as http; +import 'dart:convert'; + +class PasswordPolicyApi { + final String _apiKey; + final String _apiUrl = 'https://identitytoolkit.googleapis.com/v2/passwordPolicy?key='; + + PasswordPolicyApi(this._apiKey); + + final int schemaVersion = 1; + + Future> fetchPasswordPolicy() async { + try { + final response = await http.get(Uri.parse('$_apiUrl$_apiKey')); + if (response.statusCode == 200) { + final policy = json.decode(response.body); + + // Validate schema version + final _schemaVersion = policy['schemaVersion']; + if (!isCorrectSchemaVersion(_schemaVersion)) { + throw Exception('Schema Version mismatch, expected version 1 but got $policy'); + } + + return json.decode(response.body); + } else { + throw Exception('Failed to fetch password policy, status code: ${response.statusCode}'); + } + } catch (e) { + throw Exception('Failed to fetch password policy: $e'); + } + } + + bool isCorrectSchemaVersion(int _schemaVersion) { + return schemaVersion == _schemaVersion; + } +} diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/firebase_auth/firebase_auth/pubspec.yaml b/packages/firebase_auth/firebase_auth/pubspec.yaml index 6a6ee6148c08..b5c504e62d31 100755 --- a/packages/firebase_auth/firebase_auth/pubspec.yaml +++ b/packages/firebase_auth/firebase_auth/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: flutter: sdk: flutter meta: ^1.8.0 - + http: ^1.1.0 dev_dependencies: async: ^2.5.0 flutter_test: From 76aef6751fb4a10b30ed213bd8664ebaf5718975 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 19 Jun 2025 14:54:32 +0100 Subject: [PATCH 02/19] feat: license headers and impl start --- .../src/password_policy/password_policy_api.dart | 4 ++++ .../password_policy/password_policy_impl.dart | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart index 73802f93391f..8f0d3bc48526 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart @@ -1,3 +1,7 @@ +// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + import 'package:http/http.dart' as http; import 'dart:convert'; diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart index e69de29bb2d1..6828c38b0379 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart @@ -0,0 +1,16 @@ +// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_auth/firebase_auth.dart'; + +class PasswordPolicyImpl { + final Map _passwordPolicyApi; + + PasswordPolicyImpl(this._passwordPolicyApi); + + // Minimum length of the password which is enforced by the backend regardless or what is set or if there is none. + final int MIN_LENGTH = 6; + + +} \ No newline at end of file From 19da0d6fbcbf0f7e48f3b72991537cbf9b865663 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Fri, 20 Jun 2025 12:05:37 +0100 Subject: [PATCH 03/19] feat: Password Policy Logic finished --- .../password_policy/password_policy_api.dart | 1 + .../password_policy/password_policy_impl.dart | 128 ++++++++++++++++-- 2 files changed, 121 insertions(+), 8 deletions(-) diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart index 8f0d3bc48526..396d50af8baa 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart @@ -4,6 +4,7 @@ import 'package:http/http.dart' as http; import 'dart:convert'; +import 'dart:core'; class PasswordPolicyApi { final String _apiKey; diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart index 6828c38b0379..08fade95f22c 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart @@ -1,16 +1,128 @@ // Copyright 2025, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. - -import 'package:firebase_auth/firebase_auth.dart'; +import 'dart:core'; +import 'dart:convert'; class PasswordPolicyImpl { - final Map _passwordPolicyApi; + final Map policy; + + // Backend enforced minimum + final int MIN_PASSWORD_LENGTH = 6; + + final Map customStrengthOptions = {}; + late final String enforcementState; + late final bool forceUpgradeOnSignin; + late final int schemaVersion; + late final List allowedNonAlphanumericCharacters; + + PasswordPolicyImpl(this.policy) { + _setParametersFromResponse(); + } + + void _setParametersFromResponse() { + final responseOptions = policy['customStrengthOptions'] ?? {}; + + customStrengthOptions['minPasswordLength'] = responseOptions['minPasswordLength'] ?? MIN_PASSWORD_LENGTH; + if (responseOptions['maxPasswordLength'] != null) { + customStrengthOptions['maxPasswordLength'] = responseOptions['maxPasswordLength']; + } + if (responseOptions['containsLowercaseCharacter'] != null) { + customStrengthOptions['requireLowercase'] = responseOptions['containsLowercaseCharacter']; + } + if (responseOptions['containsUppercaseCharacter'] != null) { + customStrengthOptions['requireUppercase'] = responseOptions['containsUppercaseCharacter']; + } + if (responseOptions['containsNumericCharacter'] != null) { + customStrengthOptions['requireDigits'] = responseOptions['containsNumericCharacter']; + } + if (responseOptions['containsNonAlphanumericCharacter'] != null) { + customStrengthOptions['requireSymbols'] = responseOptions['containsNonAlphanumericCharacter']; + } + + enforcementState = policy['enforcementState'] == 'ENFORCEMENT_STATE_UNSPECIFIED' + ? 'OFF' + : policy['enforcementState']; + + allowedNonAlphanumericCharacters = responseOptions['allowedNonAlphanumericCharacters'] ?? []; + + forceUpgradeOnSignin = policy['forceUpgradeOnSignin'] ?? false; + schemaVersion = policy['schemaVersion']; + } + + Map isPasswordValid(String password) { + Map status = { + 'status': true, + 'passwordPolicy': policy, + }; + + validatePasswordLengthOptions(password, status); + validatePasswordCharacterOptions(password, status); + + return status; + } + + void validatePasswordLengthOptions(String password, Map status) { + int? minPasswordLength = customStrengthOptions['minPasswordLength']; + int? maxPasswordLength = customStrengthOptions['maxPasswordLength']; - PasswordPolicyImpl(this._passwordPolicyApi); + if (minPasswordLength != null) { + status['meetsMinPasswordLength'] = password.length >= minPasswordLength; + if (!(status['meetsMinPasswordLength'] as bool)) { + status['status'] = false; + } + } + if (maxPasswordLength != null) { + status['meetsMaxPasswordLength'] = password.length <= maxPasswordLength; + if (!(status['meetsMaxPasswordLength'] as bool)) { + status['status'] = false; + } + } + } - // Minimum length of the password which is enforced by the backend regardless or what is set or if there is none. - final int MIN_LENGTH = 6; + void validatePasswordCharacterOptions(String password, Map status) { + bool? requireLowercase = customStrengthOptions['requireLowercase']; + bool? requireUppercase = customStrengthOptions['requireUppercase']; + bool? requireDigits = customStrengthOptions['requireDigits']; + bool? requireSymbols = customStrengthOptions['requireSymbols']; - -} \ No newline at end of file + if (requireLowercase == true) { + status['meetsLowercaseRequirement'] = password.contains(RegExp(r'[a-z]')); + if (!(status['meetsLowercaseRequirement'] as bool)) { + status['status'] = false; + } + } + if (requireUppercase == true) { + status['meetsUppercaseRequirement'] = password.contains(RegExp(r'[A-Z]')); + if (!(status['meetsUppercaseRequirement'] as bool)) { + status['status'] = false; + } + } + if (requireDigits == true) { + status['meetsDigitsRequirement'] = password.contains(RegExp(r'[0-9]')); + if (!(status['meetsDigitsRequirement'] as bool)) { + status['status'] = false; + } + } + if (requireSymbols == true) { + // Check if password contains any non-alphanumeric characters + bool hasSymbol = false; + if (allowedNonAlphanumericCharacters.isNotEmpty) { + // Check against allowed symbols + for (String symbol in allowedNonAlphanumericCharacters) { + if (password.contains(symbol)) { + hasSymbol = true; + break; + } + } + } else { + // Check for any non-alphanumeric character + hasSymbol = password.contains(RegExp(r'[^a-zA-Z0-9]')); + } + status['meetsSymbolsRequirement'] = hasSymbol; + if (!hasSymbol) { + status['status'] = false; + } + } + } +} From ed0124d0e6366cf34727face420457e2bc035db0 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Fri, 20 Jun 2025 13:03:56 +0100 Subject: [PATCH 04/19] feat: add unit tests --- .../password_policy/password_policy_impl.dart | 19 ++++-- .../test/firebase_auth_test.dart | 59 +++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart index 08fade95f22c..2533be189325 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart @@ -40,14 +40,21 @@ class PasswordPolicyImpl { customStrengthOptions['requireSymbols'] = responseOptions['containsNonAlphanumericCharacter']; } - enforcementState = policy['enforcementState'] == 'ENFORCEMENT_STATE_UNSPECIFIED' - ? 'OFF' - : policy['enforcementState']; - - allowedNonAlphanumericCharacters = responseOptions['allowedNonAlphanumericCharacters'] ?? []; + // Handle both 'enforcementState' and 'enforcement' field names + final enforcement = policy['enforcementState'] ?? policy['enforcement']; + enforcementState = enforcement == 'ENFORCEMENT_STATE_UNSPECIFIED' + ? 'OFF' + : (enforcement ?? 'OFF'); + + // allowedNonAlphanumericCharacters can be at top level or in customStrengthOptions + allowedNonAlphanumericCharacters = List.from( + policy['allowedNonAlphanumericCharacters'] ?? + responseOptions['allowedNonAlphanumericCharacters'] ?? + [] + ); forceUpgradeOnSignin = policy['forceUpgradeOnSignin'] ?? false; - schemaVersion = policy['schemaVersion']; + schemaVersion = policy['schemaVersion'] ?? 1; // Default to 1 if not provided } Map isPasswordValid(String password) { diff --git a/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart b/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart index 3cb31a469c09..a15cd4d8f67f 100644 --- a/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart +++ b/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart @@ -14,6 +14,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import '../lib/src/password_policy/password_policy_impl.dart'; import './mock.dart'; @@ -37,6 +38,25 @@ void main() { const String kMockOobCode = 'oobcode'; const String kMockURL = 'http://www.example.com'; const String kMockHost = 'www.example.com'; + const String kMockValidPassword = 'Password123!'; // For password policy impl testing + const String kMockInvalidPassword = 'Pa1!'; + const String kMockInvalidPassword2 = 'password123!'; + const String kMockInvalidPassword3 = 'PASSWORD123!'; + const String kMockInvalidPassword4 = 'password!'; + const String kMockInvalidPassword5 = 'Password123'; + const Map kMockPasswordPolicy = { + 'customStrengthOptions': { + 'minPasswordLength': 6, + 'maxPasswordLength': 12, + 'containsLowercaseCharacter': true, + 'containsUppercaseCharacter': true, + 'containsNumericCharacter': true, + 'containsNonAlphanumericCharacter': true, + }, + 'allowedNonAlphanumericCharacters': ['!'], + 'schemaVersion': 1, + 'enforcement': 'OFF', + }; const int kMockPort = 31337; final TestAuthProvider testAuthProvider = TestAuthProvider(); @@ -767,6 +787,45 @@ void main() { }); }); + group('passwordPolicy', () { + test('passwordPolicy should be initialized with correct parameters', () async { + PasswordPolicyImpl passwordPolicy = PasswordPolicyImpl(kMockPasswordPolicy); + expect(passwordPolicy.policy, equals(kMockPasswordPolicy)); + }); + + PasswordPolicyImpl passwordPolicy = PasswordPolicyImpl(kMockPasswordPolicy); + + test('should return true for valid password', () async { + final status = passwordPolicy.isPasswordValid(kMockValidPassword); + expect(status['status'], isTrue); + }); + + test('should return false for invalid password that is too short', () async { + final status = passwordPolicy.isPasswordValid(kMockInvalidPassword); + expect(status['status'], isFalse); + }); + + test('should return false for invalid password with no capital characters', () async { + final status = passwordPolicy.isPasswordValid(kMockInvalidPassword2); + expect(status['status'], isFalse); + }); + + test('should return false for invalid password with no lowercase characters', () async { + final status = passwordPolicy.isPasswordValid(kMockInvalidPassword3); + expect(status['status'], isFalse); + }); + + test('should return false for invalid password with no numbers', () async { + final status = passwordPolicy.isPasswordValid(kMockInvalidPassword4); + expect(status['status'], isFalse); + }); + + test('should return false for invalid password with no symbols', () async { + final status = passwordPolicy.isPasswordValid(kMockInvalidPassword5); + expect(status['status'], isFalse); + }); + }); + test('toString()', () async { expect( auth.toString(), From b625dfd3095a8f94833f0213962ae1e7318ea058 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Fri, 20 Jun 2025 13:21:42 +0100 Subject: [PATCH 05/19] feat: expose method --- .../firebase_auth/lib/src/firebase_auth.dart | 29 +++++++++++++++++++ .../password_policy/password_policy_api.dart | 6 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index c0f72b4b27b2..7749973c1ce5 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -5,6 +5,9 @@ part of '../firebase_auth.dart'; +import 'password_policy/password_policy_impl.dart'; +import 'password_policy/password_policy_api.dart'; + /// The entry point of the Firebase Authentication SDK. class FirebaseAuth extends FirebasePluginPlatform { // Cached instances of [FirebaseAuth]. @@ -713,6 +716,32 @@ class FirebaseAuth extends FirebasePluginPlatform { await _delegate.signOut(); } + /// Validates the password against the password policy configured for the project or tenant. + /// + /// If no tenant ID is set on the Auth instance, then this method will use the password policy configured for the project. + /// Otherwise, this method will use the policy configured for the tenant. If a password policy has not been configured, + /// then the default policy configured for all projects will be used. + /// + /// If an auth flow fails because a submitted password does not meet the password policy requirements and + /// this method has previously been called, then this method will use the most recent policy available when called again. + /// + /// Returns a map with the following keys: + /// - **status**: A boolean indicating if the password is valid. + /// - **passwordPolicy**: The password policy used to validate the password. + /// - **meetsMinPasswordLength**: A boolean indicating if the password meets the minimum length requirement. + /// - **meetsMaxPasswordLength**: A boolean indicating if the password meets the maximum length requirement. + /// - **meetsLowercaseRequirement**: A boolean indicating if the password meets the lowercase requirement. + /// - **meetsUppercaseRequirement**: A boolean indicating if the password meets the uppercase requirement. + /// - **meetsDigitsRequirement**: A boolean indicating if the password meets the digits requirement. + /// - **meetsSymbolsRequirement**: A boolean indicating if the password meets the symbols requirement. + Future Map passwordPolicy(FirebaseAuth auth, String password) async { + PasswordPolicyApi passwordPolicyApi = PasswordPolicyApi(auth); + Map passwordPolicy = await passwordPolicyApi.fetchPasswordPolicy(); + + PasswordPolicyImpl passwordPolicyImpl = PasswordPolicyImpl(passwordPolicy); + return passwordPolicyImpl.isPasswordValid(password); + } + /// Checks a password reset code sent to the user by email or other /// out-of-band mechanism. /// diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart index 396d50af8baa..0aab41118dd8 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart @@ -2,20 +2,22 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:firebase_auth/firebase_auth.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'dart:core'; class PasswordPolicyApi { - final String _apiKey; + final FirebaseAuth auth; final String _apiUrl = 'https://identitytoolkit.googleapis.com/v2/passwordPolicy?key='; - PasswordPolicyApi(this._apiKey); + PasswordPolicyApi(this.auth); final int schemaVersion = 1; Future> fetchPasswordPolicy() async { try { + final String _apiKey = auth.app.options.apiKey; final response = await http.get(Uri.parse('$_apiUrl$_apiKey')); if (response.statusCode == 200) { final policy = json.decode(response.body); From c5ff16888c6611f494c20ccfb7c60655d59a0f04 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Fri, 20 Jun 2025 13:28:00 +0100 Subject: [PATCH 06/19] fix: rename method --- packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index 7749973c1ce5..385f3d0ff251 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -734,7 +734,7 @@ class FirebaseAuth extends FirebasePluginPlatform { /// - **meetsUppercaseRequirement**: A boolean indicating if the password meets the uppercase requirement. /// - **meetsDigitsRequirement**: A boolean indicating if the password meets the digits requirement. /// - **meetsSymbolsRequirement**: A boolean indicating if the password meets the symbols requirement. - Future Map passwordPolicy(FirebaseAuth auth, String password) async { + Future Map validatePassword(FirebaseAuth auth, String password) async { PasswordPolicyApi passwordPolicyApi = PasswordPolicyApi(auth); Map passwordPolicy = await passwordPolicyApi.fetchPasswordPolicy(); From 4896be54c6b2564e3decfe880487f1892242b67c Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Fri, 20 Jun 2025 14:54:32 +0100 Subject: [PATCH 07/19] chore: refactor, make explicit as possible --- .../firebase_auth/lib/src/firebase_auth.dart | 18 +-- .../src/password_policy/password_policy.dart | 41 ++++++ .../password_policy/password_policy_api.dart | 16 ++- .../password_policy/password_policy_impl.dart | 120 ++++++------------ .../password_policy_status.dart | 15 +++ .../test/firebase_auth_test.dart | 33 ++--- .../firebase_auth_instance_e2e_test.dart | 13 ++ 7 files changed, 144 insertions(+), 112 deletions(-) create mode 100644 packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart create mode 100644 packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_status.dart diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index 385f3d0ff251..104aa64f373f 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -5,8 +5,10 @@ part of '../firebase_auth.dart'; -import 'password_policy/password_policy_impl.dart'; -import 'password_policy/password_policy_api.dart'; +// import 'password_policy/password_policy_impl.dart'; +// import 'password_policy/password_policy_api.dart'; +// import 'password_policy/password_policy.dart'; +// import 'password_policy/password_policy_status.dart'; /// The entry point of the Firebase Authentication SDK. class FirebaseAuth extends FirebasePluginPlatform { @@ -734,13 +736,13 @@ class FirebaseAuth extends FirebasePluginPlatform { /// - **meetsUppercaseRequirement**: A boolean indicating if the password meets the uppercase requirement. /// - **meetsDigitsRequirement**: A boolean indicating if the password meets the digits requirement. /// - **meetsSymbolsRequirement**: A boolean indicating if the password meets the symbols requirement. - Future Map validatePassword(FirebaseAuth auth, String password) async { - PasswordPolicyApi passwordPolicyApi = PasswordPolicyApi(auth); - Map passwordPolicy = await passwordPolicyApi.fetchPasswordPolicy(); + // Future PasswordPolicyStatus validatePassword(FirebaseAuth auth, String password) async { + // PasswordPolicyApi passwordPolicyApi = PasswordPolicyApi(auth); + // PasswordPolicy passwordPolicy = await passwordPolicyApi.fetchPasswordPolicy(); - PasswordPolicyImpl passwordPolicyImpl = PasswordPolicyImpl(passwordPolicy); - return passwordPolicyImpl.isPasswordValid(password); - } + // PasswordPolicyImpl passwordPolicyImpl = PasswordPolicyImpl(passwordPolicy); + // return passwordPolicyImpl.isPasswordValid(password); + // } /// Checks a password reset code sent to the user by email or other /// out-of-band mechanism. diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart new file mode 100644 index 000000000000..fbd7202a16f5 --- /dev/null +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart @@ -0,0 +1,41 @@ +class PasswordPolicy { + final Map policy; + + // Backend enforced minimum + late final int minPasswordLength; + late final int? maxPasswordLength; + late final bool? containsLowercaseCharacter; + late final bool? containsUppercaseCharacter; + late final bool? containsNumericCharacter; + late final bool? containsNonAlphanumericCharacter; + late final int schemaVersion; + late final List allowedNonAlphanumericCharacters; + late final String enforcementState; + + PasswordPolicy(this.policy){ + initialize(); + } + + void initialize() { + final Map customStrengthOptions = policy['customStrengthOptions'] ?? {}; + + minPasswordLength = customStrengthOptions['minPasswordLength'] ?? 6; + maxPasswordLength = customStrengthOptions['maxPasswordLength']; + containsLowercaseCharacter = customStrengthOptions['containsLowercaseCharacter']; + containsUppercaseCharacter = customStrengthOptions['containsUppercaseCharacter']; + containsNumericCharacter = customStrengthOptions['containsNumericCharacter']; + containsNonAlphanumericCharacter = customStrengthOptions['containsNonAlphanumericCharacter']; + + schemaVersion = policy['schemaVersion'] ?? 1; + allowedNonAlphanumericCharacters = List.from( + policy['allowedNonAlphanumericCharacters'] ?? + customStrengthOptions['allowedNonAlphanumericCharacters'] ?? + [] + ); + + final enforcement = policy['enforcement'] ?? policy['enforcementState']; + enforcementState = enforcement == 'ENFORCEMENT_STATE_UNSPECIFIED' + ? 'OFF' + : (enforcement ?? 'OFF'); + } +} \ No newline at end of file diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart index 0aab41118dd8..f1bec9c5a5b1 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart @@ -4,20 +4,21 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:http/http.dart' as http; +import 'password_policy.dart'; import 'dart:convert'; import 'dart:core'; class PasswordPolicyApi { - final FirebaseAuth auth; + final FirebaseAuth _auth; final String _apiUrl = 'https://identitytoolkit.googleapis.com/v2/passwordPolicy?key='; - PasswordPolicyApi(this.auth); + PasswordPolicyApi(this._auth); - final int schemaVersion = 1; + final int _schemaVersion = 1; Future> fetchPasswordPolicy() async { try { - final String _apiKey = auth.app.options.apiKey; + final String _apiKey = _auth.app.options.apiKey; final response = await http.get(Uri.parse('$_apiUrl$_apiKey')); if (response.statusCode == 200) { final policy = json.decode(response.body); @@ -28,6 +29,9 @@ class PasswordPolicyApi { throw Exception('Schema Version mismatch, expected version 1 but got $policy'); } + Map rawPolicy = json.decode(response.body); + + return json.decode(response.body); } else { throw Exception('Failed to fetch password policy, status code: ${response.statusCode}'); @@ -37,7 +41,7 @@ class PasswordPolicyApi { } } - bool isCorrectSchemaVersion(int _schemaVersion) { - return schemaVersion == _schemaVersion; + bool isCorrectSchemaVersion(int schemaVersion) { + return _schemaVersion == schemaVersion; } } diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart index 2533be189325..092109552ea4 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart @@ -3,120 +3,74 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:core'; import 'dart:convert'; +import 'password_policy.dart'; +import 'password_policy_status.dart'; class PasswordPolicyImpl { - final Map policy; + final PasswordPolicy _policy; - // Backend enforced minimum - final int MIN_PASSWORD_LENGTH = 6; + PasswordPolicyImpl(this._policy); - final Map customStrengthOptions = {}; - late final String enforcementState; - late final bool forceUpgradeOnSignin; - late final int schemaVersion; - late final List allowedNonAlphanumericCharacters; + // Getter to access the policy + PasswordPolicy get policy => _policy; - PasswordPolicyImpl(this.policy) { - _setParametersFromResponse(); - } - - void _setParametersFromResponse() { - final responseOptions = policy['customStrengthOptions'] ?? {}; - - customStrengthOptions['minPasswordLength'] = responseOptions['minPasswordLength'] ?? MIN_PASSWORD_LENGTH; - if (responseOptions['maxPasswordLength'] != null) { - customStrengthOptions['maxPasswordLength'] = responseOptions['maxPasswordLength']; - } - if (responseOptions['containsLowercaseCharacter'] != null) { - customStrengthOptions['requireLowercase'] = responseOptions['containsLowercaseCharacter']; - } - if (responseOptions['containsUppercaseCharacter'] != null) { - customStrengthOptions['requireUppercase'] = responseOptions['containsUppercaseCharacter']; - } - if (responseOptions['containsNumericCharacter'] != null) { - customStrengthOptions['requireDigits'] = responseOptions['containsNumericCharacter']; - } - if (responseOptions['containsNonAlphanumericCharacter'] != null) { - customStrengthOptions['requireSymbols'] = responseOptions['containsNonAlphanumericCharacter']; - } - - // Handle both 'enforcementState' and 'enforcement' field names - final enforcement = policy['enforcementState'] ?? policy['enforcement']; - enforcementState = enforcement == 'ENFORCEMENT_STATE_UNSPECIFIED' - ? 'OFF' - : (enforcement ?? 'OFF'); - - // allowedNonAlphanumericCharacters can be at top level or in customStrengthOptions - allowedNonAlphanumericCharacters = List.from( - policy['allowedNonAlphanumericCharacters'] ?? - responseOptions['allowedNonAlphanumericCharacters'] ?? - [] - ); - - forceUpgradeOnSignin = policy['forceUpgradeOnSignin'] ?? false; - schemaVersion = policy['schemaVersion'] ?? 1; // Default to 1 if not provided - } - - Map isPasswordValid(String password) { - Map status = { - 'status': true, - 'passwordPolicy': policy, - }; + PasswordPolicyStatus isPasswordValid(String password) { + PasswordPolicyStatus status = PasswordPolicyStatus(true, _policy); - validatePasswordLengthOptions(password, status); - validatePasswordCharacterOptions(password, status); + _validatePasswordLengthOptions(password, status); + _validatePasswordCharacterOptions(password, status); return status; } - void validatePasswordLengthOptions(String password, Map status) { - int? minPasswordLength = customStrengthOptions['minPasswordLength']; - int? maxPasswordLength = customStrengthOptions['maxPasswordLength']; + void _validatePasswordLengthOptions(String password, PasswordPolicyStatus status) { + int? minPasswordLength = _policy.minPasswordLength; + int? maxPasswordLength = _policy.maxPasswordLength; if (minPasswordLength != null) { - status['meetsMinPasswordLength'] = password.length >= minPasswordLength; - if (!(status['meetsMinPasswordLength'] as bool)) { - status['status'] = false; + status.meetsMinPasswordLength = password.length >= minPasswordLength; + if (!(status.meetsMinPasswordLength as bool)) { + status.status = false; } } if (maxPasswordLength != null) { - status['meetsMaxPasswordLength'] = password.length <= maxPasswordLength; - if (!(status['meetsMaxPasswordLength'] as bool)) { - status['status'] = false; + status.meetsMaxPasswordLength = password.length <= maxPasswordLength; + if (!(status.meetsMaxPasswordLength as bool)) { + status.status = false; } } } - void validatePasswordCharacterOptions(String password, Map status) { - bool? requireLowercase = customStrengthOptions['requireLowercase']; - bool? requireUppercase = customStrengthOptions['requireUppercase']; - bool? requireDigits = customStrengthOptions['requireDigits']; - bool? requireSymbols = customStrengthOptions['requireSymbols']; + void _validatePasswordCharacterOptions(String password, PasswordPolicyStatus status) { + bool? requireLowercase = _policy.containsLowercaseCharacter; + bool? requireUppercase = _policy.containsUppercaseCharacter; + bool? requireDigits = _policy.containsNumericCharacter; + bool? requireSymbols = _policy.containsNonAlphanumericCharacter; if (requireLowercase == true) { - status['meetsLowercaseRequirement'] = password.contains(RegExp(r'[a-z]')); - if (!(status['meetsLowercaseRequirement'] as bool)) { - status['status'] = false; + status.meetsLowercaseRequirement = password.contains(RegExp(r'[a-z]')); + if (!(status.meetsLowercaseRequirement as bool)) { + status.status = false; } } if (requireUppercase == true) { - status['meetsUppercaseRequirement'] = password.contains(RegExp(r'[A-Z]')); - if (!(status['meetsUppercaseRequirement'] as bool)) { - status['status'] = false; + status.meetsUppercaseRequirement = password.contains(RegExp(r'[A-Z]')); + if (!(status.meetsUppercaseRequirement as bool)) { + status.status = false; } } if (requireDigits == true) { - status['meetsDigitsRequirement'] = password.contains(RegExp(r'[0-9]')); - if (!(status['meetsDigitsRequirement'] as bool)) { - status['status'] = false; + status.meetsDigitsRequirement = password.contains(RegExp(r'[0-9]')); + if (!(status.meetsDigitsRequirement as bool)) { + status.status = false; } } if (requireSymbols == true) { // Check if password contains any non-alphanumeric characters bool hasSymbol = false; - if (allowedNonAlphanumericCharacters.isNotEmpty) { + if (_policy.allowedNonAlphanumericCharacters.isNotEmpty) { // Check against allowed symbols - for (String symbol in allowedNonAlphanumericCharacters) { + for (String symbol in _policy.allowedNonAlphanumericCharacters) { if (password.contains(symbol)) { hasSymbol = true; break; @@ -126,9 +80,9 @@ class PasswordPolicyImpl { // Check for any non-alphanumeric character hasSymbol = password.contains(RegExp(r'[^a-zA-Z0-9]')); } - status['meetsSymbolsRequirement'] = hasSymbol; + status.meetsSymbolsRequirement = hasSymbol; if (!hasSymbol) { - status['status'] = false; + status.status = false; } } } diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_status.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_status.dart new file mode 100644 index 000000000000..e15cf8197d14 --- /dev/null +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_status.dart @@ -0,0 +1,15 @@ +import 'password_policy.dart'; + +class PasswordPolicyStatus { + bool status; + final PasswordPolicy passwordPolicy; + + late bool meetsMinPasswordLength; + late bool meetsMaxPasswordLength; + late bool meetsLowercaseRequirement; + late bool meetsUppercaseRequirement; + late bool meetsDigitsRequirement; + late bool meetsSymbolsRequirement; + + PasswordPolicyStatus(this.status, this.passwordPolicy); +} diff --git a/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart b/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart index a15cd4d8f67f..2473112f94ad 100644 --- a/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart +++ b/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart @@ -15,6 +15,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import '../lib/src/password_policy/password_policy_impl.dart'; +import '../lib/src/password_policy/password_policy.dart'; +import '../lib/src/password_policy/password_policy_status.dart'; import './mock.dart'; @@ -57,6 +59,7 @@ void main() { 'schemaVersion': 1, 'enforcement': 'OFF', }; + final PasswordPolicy kMockPasswordPolicyObject = PasswordPolicy(kMockPasswordPolicy); const int kMockPort = 31337; final TestAuthProvider testAuthProvider = TestAuthProvider(); @@ -789,40 +792,40 @@ void main() { group('passwordPolicy', () { test('passwordPolicy should be initialized with correct parameters', () async { - PasswordPolicyImpl passwordPolicy = PasswordPolicyImpl(kMockPasswordPolicy); - expect(passwordPolicy.policy, equals(kMockPasswordPolicy)); + PasswordPolicyImpl passwordPolicy = PasswordPolicyImpl(kMockPasswordPolicyObject); + expect(passwordPolicy.policy, equals(kMockPasswordPolicyObject)); }); - PasswordPolicyImpl passwordPolicy = PasswordPolicyImpl(kMockPasswordPolicy); + PasswordPolicyImpl passwordPolicy = PasswordPolicyImpl(kMockPasswordPolicyObject); test('should return true for valid password', () async { - final status = passwordPolicy.isPasswordValid(kMockValidPassword); - expect(status['status'], isTrue); + final PasswordPolicyStatus status = passwordPolicy.isPasswordValid(kMockValidPassword); + expect(status.status, isTrue); }); test('should return false for invalid password that is too short', () async { - final status = passwordPolicy.isPasswordValid(kMockInvalidPassword); - expect(status['status'], isFalse); + final PasswordPolicyStatus status = passwordPolicy.isPasswordValid(kMockInvalidPassword); + expect(status.status, isFalse); }); test('should return false for invalid password with no capital characters', () async { - final status = passwordPolicy.isPasswordValid(kMockInvalidPassword2); - expect(status['status'], isFalse); + final PasswordPolicyStatus status = passwordPolicy.isPasswordValid(kMockInvalidPassword2); + expect(status.status, isFalse); }); test('should return false for invalid password with no lowercase characters', () async { - final status = passwordPolicy.isPasswordValid(kMockInvalidPassword3); - expect(status['status'], isFalse); + final PasswordPolicyStatus status = passwordPolicy.isPasswordValid(kMockInvalidPassword3); + expect(status.status, isFalse); }); test('should return false for invalid password with no numbers', () async { - final status = passwordPolicy.isPasswordValid(kMockInvalidPassword4); - expect(status['status'], isFalse); + final PasswordPolicyStatus status = passwordPolicy.isPasswordValid(kMockInvalidPassword4); + expect(status.status, isFalse); }); test('should return false for invalid password with no symbols', () async { - final status = passwordPolicy.isPasswordValid(kMockInvalidPassword5); - expect(status['status'], isFalse); + final PasswordPolicyStatus status = passwordPolicy.isPasswordValid(kMockInvalidPassword5); + expect(status.status, isFalse); }); }); diff --git a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart index feef347f78e3..2e308db682ca 100644 --- a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart +++ b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart @@ -1062,6 +1062,19 @@ void main() { }, skip: true, ); + + group('validatePassword()', () { + test('should validate password', () async { + final status = await FirebaseAuth.instance.validatePassword(FirebaseAuth.instance, testPassword); + expect(status['status'], isTrue); + expect(status['meetsMinPasswordLength'], isTrue); + expect(status['meetsMaxPasswordLength'], isTrue); + expect(status['meetsLowercaseRequirement'], isTrue); + expect(status['meetsUppercaseRequirement'], isTrue); + expect(status['meetsDigitsRequirement'], isTrue); + expect(status['meetsSymbolsRequirement'], isTrue); + }); + }); }, // macOS skipped because it needs keychain sharing entitlement. See: https://github.com/firebase/flutterfire/issues/9538 skip: defaultTargetPlatform == TargetPlatform.macOS, From 00d1c968a8a86c3068c14595a12c3da7e7eb4686 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 26 Jun 2025 13:13:06 +0100 Subject: [PATCH 08/19] feat: add e2e --- .../firebase_auth/lib/firebase_auth.dart | 5 +++ .../firebase_auth/lib/src/firebase_auth.dart | 45 ++++++++++++++++--- .../password_policy/password_policy_api.dart | 6 +-- .../firebase_auth_instance_e2e_test.dart | 14 +++--- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart index dc842c43a8e6..0fcc9b6e419e 100755 --- a/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart @@ -9,6 +9,11 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; import 'package:flutter/foundation.dart'; +import 'src/password_policy/password_policy_impl.dart'; +import 'src/password_policy/password_policy_api.dart'; +import 'src/password_policy/password_policy.dart'; +import 'src/password_policy/password_policy_status.dart'; + export 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart' show FirebaseAuthException, diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index 104aa64f373f..427295197a7a 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -5,11 +5,6 @@ part of '../firebase_auth.dart'; -// import 'password_policy/password_policy_impl.dart'; -// import 'password_policy/password_policy_api.dart'; -// import 'password_policy/password_policy.dart'; -// import 'password_policy/password_policy_status.dart'; - /// The entry point of the Firebase Authentication SDK. class FirebaseAuth extends FirebasePluginPlatform { // Cached instances of [FirebaseAuth]. @@ -856,6 +851,46 @@ class FirebaseAuth extends FirebasePluginPlatform { return _delegate.initializeRecaptchaConfig(); } + /// Validates a password against the password policy configured for the project or tenant. + /// + /// If no tenant ID is set on the Auth instance, then this method will use the password policy configured for the project. + /// Otherwise, this method will use the policy configured for the tenant. If a password policy has not been configured, + /// then the default policy configured for all projects will be used. + /// + /// If an auth flow fails because a submitted password does not meet the password policy requirements and this method has previously been called, + /// then this method will use the most recent policy available when called again. + /// + /// Returns a map with the following keys: + /// - **status**: A boolean indicating if the password is valid. + /// - **passwordPolicy**: The password policy used to validate the password. + /// - **meetsMinPasswordLength**: A boolean indicating if the password meets the minimum length requirement. + /// - **meetsMaxPasswordLength**: A boolean indicating if the password meets the maximum length requirement. + /// - **meetsLowercaseRequirement**: A boolean indicating if the password meets the lowercase requirement. + /// - **meetsUppercaseRequirement**: A boolean indicating if the password meets the uppercase requirement. + /// - **meetsDigitsRequirement**: A boolean indicating if the password meets the digits requirement. + /// - **meetsSymbolsRequirement**: A boolean indicating if the password meets the symbols requirement. + /// + /// A [FirebaseAuthException] maybe thrown with the following error code: + /// - **invalid-password**: + /// - Thrown if the password is invalid. + /// - **network-request-failed**: + /// - Thrown if there was a network request error, for example the user + /// doesn't have internet connection + /// - **INVALID_LOGIN_CREDENTIALS** or **invalid-credential**: + /// - Thrown if the password is invalid for the given email, or the account + /// corresponding to the email does not have a password set. + /// Depending on if you are using firebase emulator or not the code is + /// different + /// - **operation-not-allowed**: + /// - Thrown if email/password accounts are not enabled. Enable + /// email/password accounts in the Firebase Console, under the Auth tab. + Future validatePassword(FirebaseAuth auth, String password) async { + PasswordPolicyApi passwordPolicyApi = PasswordPolicyApi(auth); + PasswordPolicy passwordPolicy = await passwordPolicyApi.fetchPasswordPolicy(); + PasswordPolicyImpl passwordPolicyImpl = PasswordPolicyImpl(passwordPolicy); + return passwordPolicyImpl.isPasswordValid(password); + } + @override String toString() { return 'FirebaseAuth(app: ${app.name})'; diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart index f1bec9c5a5b1..fc13d9c92472 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart @@ -16,7 +16,7 @@ class PasswordPolicyApi { final int _schemaVersion = 1; - Future> fetchPasswordPolicy() async { + Future fetchPasswordPolicy() async { try { final String _apiKey = _auth.app.options.apiKey; final response = await http.get(Uri.parse('$_apiUrl$_apiKey')); @@ -30,9 +30,7 @@ class PasswordPolicyApi { } Map rawPolicy = json.decode(response.body); - - - return json.decode(response.body); + return PasswordPolicy(rawPolicy); } else { throw Exception('Failed to fetch password policy, status code: ${response.statusCode}'); } diff --git a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart index 2e308db682ca..6ab662ce2b23 100644 --- a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart +++ b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart @@ -1066,13 +1066,13 @@ void main() { group('validatePassword()', () { test('should validate password', () async { final status = await FirebaseAuth.instance.validatePassword(FirebaseAuth.instance, testPassword); - expect(status['status'], isTrue); - expect(status['meetsMinPasswordLength'], isTrue); - expect(status['meetsMaxPasswordLength'], isTrue); - expect(status['meetsLowercaseRequirement'], isTrue); - expect(status['meetsUppercaseRequirement'], isTrue); - expect(status['meetsDigitsRequirement'], isTrue); - expect(status['meetsSymbolsRequirement'], isTrue); + expect(status.status, isTrue); + expect(status.meetsMinPasswordLength, isTrue); + expect(status.meetsMaxPasswordLength, isTrue); + expect(status.meetsLowercaseRequirement, isTrue); + expect(status.meetsUppercaseRequirement, isTrue); + expect(status.meetsDigitsRequirement, isTrue); + expect(status.meetsSymbolsRequirement, isTrue); }); }); }, From d49d57ed51a82ec57d15836472f9c775521c3eab Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 26 Jun 2025 13:21:46 +0100 Subject: [PATCH 09/19] feat: change field types --- .../src/password_policy/password_policy_status.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_status.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_status.dart index e15cf8197d14..77828f9e6701 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_status.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_status.dart @@ -4,12 +4,13 @@ class PasswordPolicyStatus { bool status; final PasswordPolicy passwordPolicy; - late bool meetsMinPasswordLength; - late bool meetsMaxPasswordLength; - late bool meetsLowercaseRequirement; - late bool meetsUppercaseRequirement; - late bool meetsDigitsRequirement; - late bool meetsSymbolsRequirement; + // Initialize all fields to true by default (meaning they pass validation) + bool meetsMinPasswordLength = true; + bool meetsMaxPasswordLength = true; + bool meetsLowercaseRequirement = true; + bool meetsUppercaseRequirement = true; + bool meetsDigitsRequirement = true; + bool meetsSymbolsRequirement = true; PasswordPolicyStatus(this.status, this.passwordPolicy); } From 934bd1f50d971d17152d64a8c6411a25d686a274 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 26 Jun 2025 13:22:35 +0100 Subject: [PATCH 10/19] chore: add license headers --- .../firebase_auth/lib/src/password_policy/password_policy.dart | 3 +++ .../lib/src/password_policy/password_policy_status.dart | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart index fbd7202a16f5..3cb7e74c5267 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart @@ -1,3 +1,6 @@ +// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. class PasswordPolicy { final Map policy; diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_status.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_status.dart index 77828f9e6701..33ae7d2273ba 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_status.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_status.dart @@ -1,3 +1,6 @@ +// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. import 'password_policy.dart'; class PasswordPolicyStatus { From 79f23155b35ea733dfb9ca0d0e71503ec31a2f16 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 26 Jun 2025 13:37:15 +0100 Subject: [PATCH 11/19] chore: fix analyze --- .../firebase_auth/lib/firebase_auth.dart | 4 +++ .../src/password_policy/password_policy.dart | 4 +-- .../password_policy/password_policy_impl.dart | 29 +++++++++---------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart index 0fcc9b6e419e..dd6aea302777 100755 --- a/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart @@ -69,6 +69,10 @@ export 'package:firebase_auth_platform_interface/firebase_auth_platform_interfac export 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart' show FirebaseException; +// Export password policy classes +export 'src/password_policy/password_policy.dart'; +export 'src/password_policy/password_policy_status.dart'; + part 'src/confirmation_result.dart'; part 'src/firebase_auth.dart'; part 'src/multi_factor.dart'; diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart index 3cb7e74c5267..b89fc244fb5f 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart @@ -33,7 +33,7 @@ class PasswordPolicy { allowedNonAlphanumericCharacters = List.from( policy['allowedNonAlphanumericCharacters'] ?? customStrengthOptions['allowedNonAlphanumericCharacters'] ?? - [] + [], ); final enforcement = policy['enforcement'] ?? policy['enforcementState']; @@ -41,4 +41,4 @@ class PasswordPolicy { ? 'OFF' : (enforcement ?? 'OFF'); } -} \ No newline at end of file +} diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart index 092109552ea4..d427d0f9355e 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart @@ -2,7 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:core'; -import 'dart:convert'; import 'password_policy.dart'; import 'password_policy_status.dart'; @@ -29,13 +28,13 @@ class PasswordPolicyImpl { if (minPasswordLength != null) { status.meetsMinPasswordLength = password.length >= minPasswordLength; - if (!(status.meetsMinPasswordLength as bool)) { + if (!status.meetsMinPasswordLength) { status.status = false; } } if (maxPasswordLength != null) { status.meetsMaxPasswordLength = password.length <= maxPasswordLength; - if (!(status.meetsMaxPasswordLength as bool)) { + if (!status.meetsMaxPasswordLength) { status.status = false; } } @@ -47,30 +46,30 @@ class PasswordPolicyImpl { bool? requireDigits = _policy.containsNumericCharacter; bool? requireSymbols = _policy.containsNonAlphanumericCharacter; - if (requireLowercase == true) { - status.meetsLowercaseRequirement = password.contains(RegExp(r'[a-z]')); - if (!(status.meetsLowercaseRequirement as bool)) { + if (requireLowercase ?? false) { + status.meetsLowercaseRequirement = password.contains(RegExp('[a-z]')); + if (!status.meetsLowercaseRequirement) { status.status = false; } } - if (requireUppercase == true) { - status.meetsUppercaseRequirement = password.contains(RegExp(r'[A-Z]')); - if (!(status.meetsUppercaseRequirement as bool)) { + if (requireUppercase ?? false) { + status.meetsUppercaseRequirement = password.contains(RegExp('[A-Z]')); + if (!status.meetsUppercaseRequirement) { status.status = false; } } - if (requireDigits == true) { - status.meetsDigitsRequirement = password.contains(RegExp(r'[0-9]')); - if (!(status.meetsDigitsRequirement as bool)) { + if (requireDigits ?? false) { + status.meetsDigitsRequirement = password.contains(RegExp('[0-9]')); + if (!status.meetsDigitsRequirement) { status.status = false; } } - if (requireSymbols == true) { + if (requireSymbols ?? false) { // Check if password contains any non-alphanumeric characters bool hasSymbol = false; if (_policy.allowedNonAlphanumericCharacters.isNotEmpty) { // Check against allowed symbols - for (String symbol in _policy.allowedNonAlphanumericCharacters) { + for (final String symbol in _policy.allowedNonAlphanumericCharacters) { if (password.contains(symbol)) { hasSymbol = true; break; @@ -78,7 +77,7 @@ class PasswordPolicyImpl { } } else { // Check for any non-alphanumeric character - hasSymbol = password.contains(RegExp(r'[^a-zA-Z0-9]')); + hasSymbol = password.contains(RegExp('[^a-zA-Z0-9]')); } status.meetsSymbolsRequirement = hasSymbol; if (!hasSymbol) { From 74725c6f63d8371d79afb84379fc0c79a94d8ff1 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 26 Jun 2025 13:58:55 +0100 Subject: [PATCH 12/19] chore: format-ci --- .../firebase_auth/lib/src/firebase_auth.dart | 18 ++++--- .../src/password_policy/password_policy.dart | 33 ++++++------ .../password_policy/password_policy_api.dart | 10 ++-- .../password_policy/password_policy_impl.dart | 16 +++--- .../test/firebase_auth_test.dart | 50 +++++++++++++------ .../firebase_auth_instance_e2e_test.dart | 41 ++++++++++++++- 6 files changed, 116 insertions(+), 52 deletions(-) diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index 427295197a7a..72a49dfb0bdc 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -715,11 +715,11 @@ class FirebaseAuth extends FirebasePluginPlatform { /// Validates the password against the password policy configured for the project or tenant. /// - /// If no tenant ID is set on the Auth instance, then this method will use the password policy configured for the project. - /// Otherwise, this method will use the policy configured for the tenant. If a password policy has not been configured, + /// If no tenant ID is set on the Auth instance, then this method will use the password policy configured for the project. + /// Otherwise, this method will use the policy configured for the tenant. If a password policy has not been configured, /// then the default policy configured for all projects will be used. /// - /// If an auth flow fails because a submitted password does not meet the password policy requirements and + /// If an auth flow fails because a submitted password does not meet the password policy requirements and /// this method has previously been called, then this method will use the most recent policy available when called again. /// /// Returns a map with the following keys: @@ -853,11 +853,11 @@ class FirebaseAuth extends FirebasePluginPlatform { /// Validates a password against the password policy configured for the project or tenant. /// - /// If no tenant ID is set on the Auth instance, then this method will use the password policy configured for the project. - /// Otherwise, this method will use the policy configured for the tenant. If a password policy has not been configured, + /// If no tenant ID is set on the Auth instance, then this method will use the password policy configured for the project. + /// Otherwise, this method will use the policy configured for the tenant. If a password policy has not been configured, /// then the default policy configured for all projects will be used. /// - /// If an auth flow fails because a submitted password does not meet the password policy requirements and this method has previously been called, + /// If an auth flow fails because a submitted password does not meet the password policy requirements and this method has previously been called, /// then this method will use the most recent policy available when called again. /// /// Returns a map with the following keys: @@ -884,9 +884,11 @@ class FirebaseAuth extends FirebasePluginPlatform { /// - **operation-not-allowed**: /// - Thrown if email/password accounts are not enabled. Enable /// email/password accounts in the Firebase Console, under the Auth tab. - Future validatePassword(FirebaseAuth auth, String password) async { + Future validatePassword( + FirebaseAuth auth, String password) async { PasswordPolicyApi passwordPolicyApi = PasswordPolicyApi(auth); - PasswordPolicy passwordPolicy = await passwordPolicyApi.fetchPasswordPolicy(); + PasswordPolicy passwordPolicy = + await passwordPolicyApi.fetchPasswordPolicy(); PasswordPolicyImpl passwordPolicyImpl = PasswordPolicyImpl(passwordPolicy); return passwordPolicyImpl.isPasswordValid(password); } diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart index b89fc244fb5f..2688d22a466a 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy.dart @@ -15,30 +15,35 @@ class PasswordPolicy { late final List allowedNonAlphanumericCharacters; late final String enforcementState; - PasswordPolicy(this.policy){ + PasswordPolicy(this.policy) { initialize(); } void initialize() { - final Map customStrengthOptions = policy['customStrengthOptions'] ?? {}; - + final Map customStrengthOptions = + policy['customStrengthOptions'] ?? {}; + minPasswordLength = customStrengthOptions['minPasswordLength'] ?? 6; maxPasswordLength = customStrengthOptions['maxPasswordLength']; - containsLowercaseCharacter = customStrengthOptions['containsLowercaseCharacter']; - containsUppercaseCharacter = customStrengthOptions['containsUppercaseCharacter']; - containsNumericCharacter = customStrengthOptions['containsNumericCharacter']; - containsNonAlphanumericCharacter = customStrengthOptions['containsNonAlphanumericCharacter']; - + containsLowercaseCharacter = + customStrengthOptions['containsLowercaseCharacter']; + containsUppercaseCharacter = + customStrengthOptions['containsUppercaseCharacter']; + containsNumericCharacter = + customStrengthOptions['containsNumericCharacter']; + containsNonAlphanumericCharacter = + customStrengthOptions['containsNonAlphanumericCharacter']; + schemaVersion = policy['schemaVersion'] ?? 1; allowedNonAlphanumericCharacters = List.from( - policy['allowedNonAlphanumericCharacters'] ?? - customStrengthOptions['allowedNonAlphanumericCharacters'] ?? - [], + policy['allowedNonAlphanumericCharacters'] ?? + customStrengthOptions['allowedNonAlphanumericCharacters'] ?? + [], ); - + final enforcement = policy['enforcement'] ?? policy['enforcementState']; - enforcementState = enforcement == 'ENFORCEMENT_STATE_UNSPECIFIED' - ? 'OFF' + enforcementState = enforcement == 'ENFORCEMENT_STATE_UNSPECIFIED' + ? 'OFF' : (enforcement ?? 'OFF'); } } diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart index fc13d9c92472..ffe2a417f1f1 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart @@ -4,13 +4,13 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:http/http.dart' as http; -import 'password_policy.dart'; import 'dart:convert'; import 'dart:core'; class PasswordPolicyApi { final FirebaseAuth _auth; - final String _apiUrl = 'https://identitytoolkit.googleapis.com/v2/passwordPolicy?key='; + final String _apiUrl = + 'https://identitytoolkit.googleapis.com/v2/passwordPolicy?key='; PasswordPolicyApi(this._auth); @@ -26,13 +26,15 @@ class PasswordPolicyApi { // Validate schema version final _schemaVersion = policy['schemaVersion']; if (!isCorrectSchemaVersion(_schemaVersion)) { - throw Exception('Schema Version mismatch, expected version 1 but got $policy'); + throw Exception( + 'Schema Version mismatch, expected version 1 but got $policy'); } Map rawPolicy = json.decode(response.body); return PasswordPolicy(rawPolicy); } else { - throw Exception('Failed to fetch password policy, status code: ${response.statusCode}'); + throw Exception( + 'Failed to fetch password policy, status code: ${response.statusCode}'); } } catch (e) { throw Exception('Failed to fetch password policy: $e'); diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart index d427d0f9355e..e4cbb585d2b2 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart @@ -22,15 +22,14 @@ class PasswordPolicyImpl { return status; } - void _validatePasswordLengthOptions(String password, PasswordPolicyStatus status) { - int? minPasswordLength = _policy.minPasswordLength; + void _validatePasswordLengthOptions( + String password, PasswordPolicyStatus status) { + int minPasswordLength = _policy.minPasswordLength; int? maxPasswordLength = _policy.maxPasswordLength; - if (minPasswordLength != null) { - status.meetsMinPasswordLength = password.length >= minPasswordLength; - if (!status.meetsMinPasswordLength) { - status.status = false; - } + status.meetsMinPasswordLength = password.length >= minPasswordLength; + if (!status.meetsMinPasswordLength) { + status.status = false; } if (maxPasswordLength != null) { status.meetsMaxPasswordLength = password.length <= maxPasswordLength; @@ -40,7 +39,8 @@ class PasswordPolicyImpl { } } - void _validatePasswordCharacterOptions(String password, PasswordPolicyStatus status) { + void _validatePasswordCharacterOptions( + String password, PasswordPolicyStatus status) { bool? requireLowercase = _policy.containsLowercaseCharacter; bool? requireUppercase = _policy.containsUppercaseCharacter; bool? requireDigits = _policy.containsNumericCharacter; diff --git a/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart b/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart index 2473112f94ad..f653ba0adbaf 100644 --- a/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart +++ b/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart @@ -40,7 +40,8 @@ void main() { const String kMockOobCode = 'oobcode'; const String kMockURL = 'http://www.example.com'; const String kMockHost = 'www.example.com'; - const String kMockValidPassword = 'Password123!'; // For password policy impl testing + const String kMockValidPassword = + 'Password123!'; // For password policy impl testing const String kMockInvalidPassword = 'Pa1!'; const String kMockInvalidPassword2 = 'password123!'; const String kMockInvalidPassword3 = 'PASSWORD123!'; @@ -59,7 +60,8 @@ void main() { 'schemaVersion': 1, 'enforcement': 'OFF', }; - final PasswordPolicy kMockPasswordPolicyObject = PasswordPolicy(kMockPasswordPolicy); + final PasswordPolicy kMockPasswordPolicyObject = + PasswordPolicy(kMockPasswordPolicy); const int kMockPort = 31337; final TestAuthProvider testAuthProvider = TestAuthProvider(); @@ -791,40 +793,56 @@ void main() { }); group('passwordPolicy', () { - test('passwordPolicy should be initialized with correct parameters', () async { - PasswordPolicyImpl passwordPolicy = PasswordPolicyImpl(kMockPasswordPolicyObject); + test('passwordPolicy should be initialized with correct parameters', + () async { + PasswordPolicyImpl passwordPolicy = + PasswordPolicyImpl(kMockPasswordPolicyObject); expect(passwordPolicy.policy, equals(kMockPasswordPolicyObject)); }); - PasswordPolicyImpl passwordPolicy = PasswordPolicyImpl(kMockPasswordPolicyObject); + PasswordPolicyImpl passwordPolicy = + PasswordPolicyImpl(kMockPasswordPolicyObject); test('should return true for valid password', () async { - final PasswordPolicyStatus status = passwordPolicy.isPasswordValid(kMockValidPassword); + final PasswordPolicyStatus status = + passwordPolicy.isPasswordValid(kMockValidPassword); expect(status.status, isTrue); }); - test('should return false for invalid password that is too short', () async { - final PasswordPolicyStatus status = passwordPolicy.isPasswordValid(kMockInvalidPassword); + test('should return false for invalid password that is too short', + () async { + final PasswordPolicyStatus status = + passwordPolicy.isPasswordValid(kMockInvalidPassword); expect(status.status, isFalse); }); - test('should return false for invalid password with no capital characters', () async { - final PasswordPolicyStatus status = passwordPolicy.isPasswordValid(kMockInvalidPassword2); + test( + 'should return false for invalid password with no capital characters', + () async { + final PasswordPolicyStatus status = + passwordPolicy.isPasswordValid(kMockInvalidPassword2); expect(status.status, isFalse); }); - test('should return false for invalid password with no lowercase characters', () async { - final PasswordPolicyStatus status = passwordPolicy.isPasswordValid(kMockInvalidPassword3); + test( + 'should return false for invalid password with no lowercase characters', + () async { + final PasswordPolicyStatus status = + passwordPolicy.isPasswordValid(kMockInvalidPassword3); expect(status.status, isFalse); }); - test('should return false for invalid password with no numbers', () async { - final PasswordPolicyStatus status = passwordPolicy.isPasswordValid(kMockInvalidPassword4); + test('should return false for invalid password with no numbers', + () async { + final PasswordPolicyStatus status = + passwordPolicy.isPasswordValid(kMockInvalidPassword4); expect(status.status, isFalse); }); - test('should return false for invalid password with no symbols', () async { - final PasswordPolicyStatus status = passwordPolicy.isPasswordValid(kMockInvalidPassword5); + test('should return false for invalid password with no symbols', + () async { + final PasswordPolicyStatus status = + passwordPolicy.isPasswordValid(kMockInvalidPassword5); expect(status.status, isFalse); }); }); diff --git a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart index 6ab662ce2b23..f5af0bca616a 100644 --- a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart +++ b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart @@ -1064,8 +1064,16 @@ void main() { ); group('validatePassword()', () { - test('should validate password', () async { - final status = await FirebaseAuth.instance.validatePassword(FirebaseAuth.instance, testPassword); + + const String validPassword = 'Password123!'; // For password policy impl testing + const String invalidPassword = 'Pa1!'; + const String invalidPassword2 = 'password123!'; + const String invalidPassword3 = 'PASSWORD123!'; + const String invalidPassword4 = 'password!'; + const String invalidPassword5 = 'Password123'; + + test('should validate password that is correct', () async { + final PasswordPolicyStatus status = await FirebaseAuth.instance.validatePassword(FirebaseAuth.instance, validPassword); expect(status.status, isTrue); expect(status.meetsMinPasswordLength, isTrue); expect(status.meetsMaxPasswordLength, isTrue); @@ -1074,6 +1082,35 @@ void main() { expect(status.meetsDigitsRequirement, isTrue); expect(status.meetsSymbolsRequirement, isTrue); }); + + test('should not validate a password that is too short', () async { + final PasswordPolicyStatus status = await FirebaseAuth.instance.validatePassword(FirebaseAuth.instance, invalidPassword); + expect(status.status, isFalse); + expect(status.meetsMinPasswordLength, isFalse); + }); + + test('should not validate a password that has no uppercase characters', () async { + final PasswordPolicyStatus status = await FirebaseAuth.instance.validatePassword(FirebaseAuth.instance, invalidPassword2); + expect(status.status, isFalse); + expect(status.meetsUppercaseRequirement, isFalse); + }); + + test('should not validate a password that has no lowercase characters', () async { + final PasswordPolicyStatus status = await FirebaseAuth.instance.validatePassword(FirebaseAuth.instance, invalidPassword3); + expect(status.status, isFalse); + }); + + test('should not validate a password that has no digits', () async { + final PasswordPolicyStatus status = await FirebaseAuth.instance.validatePassword(FirebaseAuth.instance, invalidPassword4); + expect(status.status, isFalse); + expect(status.meetsDigitsRequirement, isFalse); + }); + + test('should not validate a password that has no symbols', () async { + final PasswordPolicyStatus status = await FirebaseAuth.instance.validatePassword(FirebaseAuth.instance, invalidPassword5); + expect(status.status, isFalse); + expect(status.meetsSymbolsRequirement, isFalse); + }); }); }, // macOS skipped because it needs keychain sharing entitlement. See: https://github.com/firebase/flutterfire/issues/9538 From 02d567d28f8e656062cf8e678dc153de4c8c3fa7 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 26 Jun 2025 14:03:00 +0100 Subject: [PATCH 13/19] chore: remove duplicate --- .../firebase_auth/lib/src/firebase_auth.dart | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index 72a49dfb0bdc..2744addd9cc1 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -704,41 +704,6 @@ class FirebaseAuth extends FirebasePluginPlatform { } } - /// Signs out the current user. - /// - /// If successful, it also updates - /// any [authStateChanges], [idTokenChanges] or [userChanges] stream - /// listeners. - Future signOut() async { - await _delegate.signOut(); - } - - /// Validates the password against the password policy configured for the project or tenant. - /// - /// If no tenant ID is set on the Auth instance, then this method will use the password policy configured for the project. - /// Otherwise, this method will use the policy configured for the tenant. If a password policy has not been configured, - /// then the default policy configured for all projects will be used. - /// - /// If an auth flow fails because a submitted password does not meet the password policy requirements and - /// this method has previously been called, then this method will use the most recent policy available when called again. - /// - /// Returns a map with the following keys: - /// - **status**: A boolean indicating if the password is valid. - /// - **passwordPolicy**: The password policy used to validate the password. - /// - **meetsMinPasswordLength**: A boolean indicating if the password meets the minimum length requirement. - /// - **meetsMaxPasswordLength**: A boolean indicating if the password meets the maximum length requirement. - /// - **meetsLowercaseRequirement**: A boolean indicating if the password meets the lowercase requirement. - /// - **meetsUppercaseRequirement**: A boolean indicating if the password meets the uppercase requirement. - /// - **meetsDigitsRequirement**: A boolean indicating if the password meets the digits requirement. - /// - **meetsSymbolsRequirement**: A boolean indicating if the password meets the symbols requirement. - // Future PasswordPolicyStatus validatePassword(FirebaseAuth auth, String password) async { - // PasswordPolicyApi passwordPolicyApi = PasswordPolicyApi(auth); - // PasswordPolicy passwordPolicy = await passwordPolicyApi.fetchPasswordPolicy(); - - // PasswordPolicyImpl passwordPolicyImpl = PasswordPolicyImpl(passwordPolicy); - // return passwordPolicyImpl.isPasswordValid(password); - // } - /// Checks a password reset code sent to the user by email or other /// out-of-band mechanism. /// From 64ca4ead590702e7c3187c04eb5e6499faef2a9f Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 26 Jun 2025 14:10:17 +0100 Subject: [PATCH 14/19] chore: undo accidental deletion --- .../firebase_auth/lib/src/firebase_auth.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index 2744addd9cc1..838ee8e5727c 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -810,6 +810,15 @@ class FirebaseAuth extends FirebasePluginPlatform { return _delegate.revokeTokenWithAuthorizationCode(authorizationCode); } + /// Signs out the current user. + /// + /// If successful, it also updates + /// any [authStateChanges], [idTokenChanges] or [userChanges] stream + /// listeners. + Future signOut() async { + await _delegate.signOut(); + } + /// Initializes the reCAPTCHA Enterprise client proactively to enhance reCAPTCHA signal collection and /// to complete reCAPTCHA-protected flows in a single attempt. Future initializeRecaptchaConfig() { From b19782366b0d835e42ffd1b2c46a74d96b1e8039 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 26 Jun 2025 14:20:03 +0100 Subject: [PATCH 15/19] chore: fix analyze --- .../lib/src/password_policy/password_policy_api.dart | 4 ++-- .../lib/src/password_policy/password_policy_impl.dart | 4 ++-- .../firebase_auth/firebase_auth/test/firebase_auth_test.dart | 4 +--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart index ffe2a417f1f1..067b53e30633 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart @@ -27,14 +27,14 @@ class PasswordPolicyApi { final _schemaVersion = policy['schemaVersion']; if (!isCorrectSchemaVersion(_schemaVersion)) { throw Exception( - 'Schema Version mismatch, expected version 1 but got $policy'); + 'Schema Version mismatch, expected version 1 but got $policy',); } Map rawPolicy = json.decode(response.body); return PasswordPolicy(rawPolicy); } else { throw Exception( - 'Failed to fetch password policy, status code: ${response.statusCode}'); + 'Failed to fetch password policy, status code: ${response.statusCode}',); } } catch (e) { throw Exception('Failed to fetch password policy: $e'); diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart index e4cbb585d2b2..ff3fcbda4908 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart @@ -23,7 +23,7 @@ class PasswordPolicyImpl { } void _validatePasswordLengthOptions( - String password, PasswordPolicyStatus status) { + String password, PasswordPolicyStatus status,) { int minPasswordLength = _policy.minPasswordLength; int? maxPasswordLength = _policy.maxPasswordLength; @@ -40,7 +40,7 @@ class PasswordPolicyImpl { } void _validatePasswordCharacterOptions( - String password, PasswordPolicyStatus status) { + String password, PasswordPolicyStatus status,) { bool? requireLowercase = _policy.containsLowercaseCharacter; bool? requireUppercase = _policy.containsUppercaseCharacter; bool? requireDigits = _policy.containsNumericCharacter; diff --git a/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart b/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart index f653ba0adbaf..a5203d6b3ac5 100644 --- a/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart +++ b/packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart @@ -14,9 +14,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import '../lib/src/password_policy/password_policy_impl.dart'; -import '../lib/src/password_policy/password_policy.dart'; -import '../lib/src/password_policy/password_policy_status.dart'; +// Password policy classes are now exported from the main library import './mock.dart'; From 1c168ae4062f373431e4c4926e405b4eff61db73 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 26 Jun 2025 14:32:56 +0100 Subject: [PATCH 16/19] fix: expose apis --- packages/firebase_auth/firebase_auth/lib/firebase_auth.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart index dd6aea302777..f27e7df3fbea 100755 --- a/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/firebase_auth.dart @@ -72,6 +72,8 @@ export 'package:firebase_core_platform_interface/firebase_core_platform_interfac // Export password policy classes export 'src/password_policy/password_policy.dart'; export 'src/password_policy/password_policy_status.dart'; +export 'src/password_policy/password_policy_impl.dart'; +export 'src/password_policy/password_policy_api.dart'; part 'src/confirmation_result.dart'; part 'src/firebase_auth.dart'; From 1b2478d95d747acafaaa3cfa2ef2f91756f82b8a Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 26 Jun 2025 14:35:14 +0100 Subject: [PATCH 17/19] chore: formatting --- .../lib/src/password_policy/password_policy_api.dart | 6 ++++-- .../lib/src/password_policy/password_policy_impl.dart | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart index 067b53e30633..3bc55dfc6e84 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_api.dart @@ -27,14 +27,16 @@ class PasswordPolicyApi { final _schemaVersion = policy['schemaVersion']; if (!isCorrectSchemaVersion(_schemaVersion)) { throw Exception( - 'Schema Version mismatch, expected version 1 but got $policy',); + 'Schema Version mismatch, expected version 1 but got $policy', + ); } Map rawPolicy = json.decode(response.body); return PasswordPolicy(rawPolicy); } else { throw Exception( - 'Failed to fetch password policy, status code: ${response.statusCode}',); + 'Failed to fetch password policy, status code: ${response.statusCode}', + ); } } catch (e) { throw Exception('Failed to fetch password policy: $e'); diff --git a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart index ff3fcbda4908..6ac7c63c468a 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/password_policy/password_policy_impl.dart @@ -23,7 +23,9 @@ class PasswordPolicyImpl { } void _validatePasswordLengthOptions( - String password, PasswordPolicyStatus status,) { + String password, + PasswordPolicyStatus status, + ) { int minPasswordLength = _policy.minPasswordLength; int? maxPasswordLength = _policy.maxPasswordLength; @@ -40,7 +42,9 @@ class PasswordPolicyImpl { } void _validatePasswordCharacterOptions( - String password, PasswordPolicyStatus status,) { + String password, + PasswordPolicyStatus status, + ) { bool? requireLowercase = _policy.containsLowercaseCharacter; bool? requireUppercase = _policy.containsUppercaseCharacter; bool? requireDigits = _policy.containsNumericCharacter; From f6643109c6675ef2f3e7adf49394bb8d8a51ddd2 Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 26 Jun 2025 14:49:49 +0100 Subject: [PATCH 18/19] chore: sort dependencies alphabeticaly --- packages/firebase_auth/firebase_auth/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firebase_auth/firebase_auth/pubspec.yaml b/packages/firebase_auth/firebase_auth/pubspec.yaml index b5c504e62d31..f2a0d52693ff 100755 --- a/packages/firebase_auth/firebase_auth/pubspec.yaml +++ b/packages/firebase_auth/firebase_auth/pubspec.yaml @@ -26,8 +26,8 @@ dependencies: firebase_core_platform_interface: ^5.3.1 flutter: sdk: flutter - meta: ^1.8.0 http: ^1.1.0 + meta: ^1.8.0 dev_dependencies: async: ^2.5.0 flutter_test: From 6e99ae4f1b33d7396cfa823cbe866d778bc4859d Mon Sep 17 00:00:00 2001 From: MichaelVerdon Date: Thu, 26 Jun 2025 15:26:31 +0100 Subject: [PATCH 19/19] chore: more e2e tests --- .../firebase_auth/lib/src/firebase_auth.dart | 10 +++++- .../firebase_auth_instance_e2e_test.dart | 36 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index 838ee8e5727c..3a2b203d8723 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -859,7 +859,15 @@ class FirebaseAuth extends FirebasePluginPlatform { /// - Thrown if email/password accounts are not enabled. Enable /// email/password accounts in the Firebase Console, under the Auth tab. Future validatePassword( - FirebaseAuth auth, String password) async { + FirebaseAuth auth, + String? password, + ) async { + if (password == null || password.isEmpty) { + throw FirebaseAuthException( + code: 'invalid-password', + message: 'Password cannot be null or empty', + ); + } PasswordPolicyApi passwordPolicyApi = PasswordPolicyApi(auth); PasswordPolicy passwordPolicy = await passwordPolicyApi.fetchPasswordPolicy(); diff --git a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart index f5af0bca616a..e92fcc010a2c 100644 --- a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart +++ b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart @@ -1111,6 +1111,42 @@ void main() { expect(status.status, isFalse); expect(status.meetsSymbolsRequirement, isFalse); }); + + test('should throw an exception if the password is empty', () async { + try { + await FirebaseAuth.instance.validatePassword( + FirebaseAuth.instance, + '', + ); + } catch (e) { + expect( + e, + isA().having( + (e) => e.code, + 'code', + equals('invalid-password'), + ), + ); + } + }); + + test('should throw an exception if the password is null', () async { + try { + await FirebaseAuth.instance.validatePassword( + FirebaseAuth.instance, + null, + ); + } catch (e) { + expect( + e, + isA().having( + (e) => e.code, + 'code', + equals('invalid-password'), + ), + ); + } + }); }); }, // macOS skipped because it needs keychain sharing entitlement. See: https://github.com/firebase/flutterfire/issues/9538