Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ec8d519
feat(auth): PasswordPolicy Support
MichaelVerdon Jun 19, 2025
55e8400
feat: license headers and impl start
MichaelVerdon Jun 19, 2025
1823649
feat: Password Policy Logic finished
MichaelVerdon Jun 20, 2025
226fcde
feat: add unit tests
MichaelVerdon Jun 20, 2025
5ab2b37
feat: expose method
MichaelVerdon Jun 20, 2025
1d23e57
fix: rename method
MichaelVerdon Jun 20, 2025
928f5e2
chore: refactor, make explicit as possible
MichaelVerdon Jun 20, 2025
8060d70
feat: add e2e
MichaelVerdon Jun 26, 2025
21c9ad1
feat: change field types
MichaelVerdon Jun 26, 2025
7d8d60f
chore: add license headers
MichaelVerdon Jun 26, 2025
c21b3bc
chore: fix analyze
MichaelVerdon Jun 26, 2025
70b7e49
chore: format-ci
MichaelVerdon Jun 26, 2025
1be1baa
chore: remove duplicate
MichaelVerdon Jun 26, 2025
b0ef9e2
chore: undo accidental deletion
MichaelVerdon Jun 26, 2025
9936e46
chore: fix analyze
MichaelVerdon Jun 26, 2025
9c8554d
fix: expose apis
MichaelVerdon Jun 26, 2025
de5e14d
chore: formatting
MichaelVerdon Jun 26, 2025
0555134
chore: sort dependencies alphabeticaly
MichaelVerdon Jun 26, 2025
b13f33e
chore: more e2e tests
MichaelVerdon Jun 26, 2025
03e3f8d
chore: refactor
MichaelVerdon Jul 17, 2025
0b78622
chore: refactor
MichaelVerdon Jul 17, 2025
69391a1
chore: refactor
MichaelVerdon Jul 17, 2025
87899f8
chore: fix
MichaelVerdon Jul 18, 2025
0e68d2b
chore: create internals
MichaelVerdon Jul 21, 2025
fa7c0c2
chore: run format
MichaelVerdon Jul 21, 2025
27f3476
fix: shift into platform_interface
MichaelVerdon Jul 24, 2025
15705e1
fix: readd method
MichaelVerdon Jul 24, 2025
18c5d51
fix: pass apikey through method instead
MichaelVerdon Jul 24, 2025
cfb5190
format: melos run format
MichaelVerdon Jul 24, 2025
3cd99e1
chore: remove import
MichaelVerdon Jul 24, 2025
4871d79
chore: keep internals internal
MichaelVerdon Jul 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/firebase_auth/firebase_auth/lib/firebase_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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_validation_status.dart';

export 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'
show
FirebaseAuthException,
Expand Down Expand Up @@ -64,6 +69,12 @@ 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_validation_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';
part 'src/multi_factor.dart';
Expand Down
68 changes: 59 additions & 9 deletions packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -704,15 +704,6 @@ class FirebaseAuth extends FirebasePluginPlatform {
}
}

/// Signs out the current user.
///
/// If successful, it also updates
/// any [authStateChanges], [idTokenChanges] or [userChanges] stream
/// listeners.
Future<void> signOut() async {
await _delegate.signOut();
}

/// Checks a password reset code sent to the user by email or other
/// out-of-band mechanism.
///
Expand Down Expand Up @@ -819,12 +810,71 @@ 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<void> 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<void> initializeRecaptchaConfig() {
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<PasswordValidationStatus> validatePassword(
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();
PasswordPolicyImpl passwordPolicyImpl = PasswordPolicyImpl(passwordPolicy);
return passwordPolicyImpl.isPasswordValid(password);
}

@override
String toString() {
return 'FirebaseAuth(app: ${app.name})';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// 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<String, dynamic> 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<String> allowedNonAlphanumericCharacters;
late final String enforcementState;

PasswordPolicy(this.policy) {
initialize();
}

void initialize() {
final Map<String, dynamic> 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<String>.from(
policy['allowedNonAlphanumericCharacters'] ??
customStrengthOptions['allowedNonAlphanumericCharacters'] ??
[],
);

final enforcement = policy['enforcement'] ?? policy['enforcementState'];
enforcementState = enforcement == 'ENFORCEMENT_STATE_UNSPECIFIED'
? 'OFF'
: (enforcement ?? 'OFF');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// 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 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:core';

class PasswordPolicyApi {
final FirebaseAuth _auth;
final String _apiUrl =
'https://identitytoolkit.googleapis.com/v2/passwordPolicy?key=';

PasswordPolicyApi(this._auth);

final int _schemaVersion = 1;

Future<PasswordPolicy> 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);

// Validate schema version
final _schemaVersion = policy['schemaVersion'];
if (!isCorrectSchemaVersion(_schemaVersion)) {
throw Exception(
'Schema Version mismatch, expected version 1 but got $policy',
);
}

Map<String, dynamic> rawPolicy = json.decode(response.body);
return PasswordPolicy(rawPolicy);
} 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// 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 'dart:core';
import 'password_policy.dart';
import 'password_validation_status.dart';

class PasswordPolicyImpl {
final PasswordPolicy _policy;

PasswordPolicyImpl(this._policy);

// Getter to access the policy
PasswordPolicy get policy => _policy;

PasswordValidationStatus isPasswordValid(String password) {
PasswordValidationStatus status = PasswordValidationStatus(true, _policy);

_validatePasswordLengthOptions(password, status);
_validatePasswordCharacterOptions(password, status);

return status;
}

void _validatePasswordLengthOptions(
String password,
PasswordValidationStatus status,
) {
int minPasswordLength = _policy.minPasswordLength;
int? maxPasswordLength = _policy.maxPasswordLength;

status.meetsMinPasswordLength = password.length >= minPasswordLength;
if (!status.meetsMinPasswordLength) {
status.isValid = false;
}
if (maxPasswordLength != null) {
status.meetsMaxPasswordLength = password.length <= maxPasswordLength;
if (!status.meetsMaxPasswordLength) {
status.isValid = false;
}
}
}

void _validatePasswordCharacterOptions(
String password,
PasswordValidationStatus status,
) {
bool? requireLowercase = _policy.containsLowercaseCharacter;
bool? requireUppercase = _policy.containsUppercaseCharacter;
bool? requireDigits = _policy.containsNumericCharacter;
bool? requireSymbols = _policy.containsNonAlphanumericCharacter;

if (requireLowercase ?? false) {
status.meetsLowercaseRequirement = password.contains(RegExp('[a-z]'));
if (!status.meetsLowercaseRequirement) {
status.isValid = false;
}
}
if (requireUppercase ?? false) {
status.meetsUppercaseRequirement = password.contains(RegExp('[A-Z]'));
if (!status.meetsUppercaseRequirement) {
status.isValid = false;
}
}
if (requireDigits ?? false) {
status.meetsDigitsRequirement = password.contains(RegExp('[0-9]'));
if (!status.meetsDigitsRequirement) {
status.isValid = false;
}
}
if (requireSymbols ?? false) {
// Check if password contains any non-alphanumeric characters
bool hasSymbol = false;
if (_policy.allowedNonAlphanumericCharacters.isNotEmpty) {
// Check against allowed symbols
for (final String symbol in _policy.allowedNonAlphanumericCharacters) {
if (password.contains(symbol)) {
hasSymbol = true;
break;
}
}
} else {
// Check for any non-alphanumeric character
hasSymbol = password.contains(RegExp('[^a-zA-Z0-9]'));
}
status.meetsSymbolsRequirement = hasSymbol;
if (!hasSymbol) {
status.isValid = false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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 PasswordValidationStatus {
bool isValid;
final PasswordPolicy passwordPolicy;

// 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;

PasswordValidationStatus(this.isValid, this.passwordPolicy);
}
2 changes: 1 addition & 1 deletion packages/firebase_auth/firebase_auth/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ dependencies:
firebase_core_platform_interface: ^6.0.0
flutter:
sdk: flutter
http: ^1.1.0
meta: ^1.8.0

dev_dependencies:
async: ^2.5.0
flutter_test:
Expand Down
Loading
Loading