Skip to content

feat(auth): validatePassword method/PasswordPolicy Support #17439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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_policy_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_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';
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<PasswordPolicyStatus> 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_policy_status.dart';

class PasswordPolicyImpl {
final PasswordPolicy _policy;

PasswordPolicyImpl(this._policy);

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

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

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

return status;
}

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

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

void _validatePasswordCharacterOptions(
String password,
PasswordPolicyStatus 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.status = false;
}
}
if (requireUppercase ?? false) {
status.meetsUppercaseRequirement = password.contains(RegExp('[A-Z]'));
if (!status.meetsUppercaseRequirement) {
status.status = false;
}
}
if (requireDigits ?? false) {
status.meetsDigitsRequirement = password.contains(RegExp('[0-9]'));
if (!status.meetsDigitsRequirement) {
status.status = 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.status = 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 PasswordPolicyStatus {
bool status;
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;

PasswordPolicyStatus(this.status, 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: ^5.3.1
flutter:
sdk: flutter
http: ^1.1.0
meta: ^1.8.0

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