1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-11-26 14:59:22 +00:00

Implement password policy for account creation

This commit is contained in:
Pierre HUBERT 2021-02-18 18:20:50 +01:00
parent 482e938744
commit 277c08048d
4 changed files with 232 additions and 13 deletions

View File

@ -16,9 +16,23 @@ class ServerConfigurationHelper {
(await APIRequest.withoutLogin("server/config").execWithThrow()) (await APIRequest.withoutLogin("server/config").execWithThrow())
.getObject(); .getObject();
final passwordPolicy = response["password_policy"];
final dataConservationPolicy = response["data_conservation_policy"]; final dataConservationPolicy = response["data_conservation_policy"];
_config = ServerConfig( _config = ServerConfig(
passwordPolicy: PasswordPolicy(
allowMailInPassword: passwordPolicy["allow_email_in_password"],
allowNameInPassword: passwordPolicy["allow_name_in_password"],
minPasswordLength: passwordPolicy["min_password_length"],
minNumberUpperCaseLetters:
passwordPolicy["min_number_upper_case_letters"],
minNumberLowerCaseLetters:
passwordPolicy["min_number_lower_case_letters"],
minNumberDigits: passwordPolicy["min_number_digits"],
minNumberSpecialCharacters:
passwordPolicy["min_number_special_characters"],
minCategoriesPresence: passwordPolicy["min_categories_presence"],
),
dataConservationPolicy: ServerDataConservationPolicy( dataConservationPolicy: ServerDataConservationPolicy(
minInactiveAccountLifetime: minInactiveAccountLifetime:
dataConservationPolicy["min_inactive_account_lifetime"], dataConservationPolicy["min_inactive_account_lifetime"],

View File

@ -4,6 +4,35 @@ import 'package:flutter/widgets.dart';
/// ///
/// @author Pierre Hubert /// @author Pierre Hubert
class PasswordPolicy {
final bool allowMailInPassword;
final bool allowNameInPassword;
final int minPasswordLength;
final int minNumberUpperCaseLetters;
final int minNumberLowerCaseLetters;
final int minNumberDigits;
final int minNumberSpecialCharacters;
final int minCategoriesPresence;
const PasswordPolicy({
@required this.allowMailInPassword,
@required this.allowNameInPassword,
@required this.minPasswordLength,
@required this.minNumberUpperCaseLetters,
@required this.minNumberLowerCaseLetters,
@required this.minNumberDigits,
@required this.minNumberSpecialCharacters,
@required this.minCategoriesPresence,
}) : assert(allowMailInPassword != null),
assert(allowNameInPassword != null),
assert(minPasswordLength != null),
assert(minNumberUpperCaseLetters != null),
assert(minNumberLowerCaseLetters != null),
assert(minNumberDigits != null),
assert(minNumberSpecialCharacters != null),
assert(minCategoriesPresence != null);
}
class ServerDataConservationPolicy { class ServerDataConservationPolicy {
final int minInactiveAccountLifetime; final int minInactiveAccountLifetime;
final int minNotificationLifetime; final int minNotificationLifetime;
@ -28,9 +57,12 @@ class ServerDataConservationPolicy {
} }
class ServerConfig { class ServerConfig {
final PasswordPolicy passwordPolicy;
final ServerDataConservationPolicy dataConservationPolicy; final ServerDataConservationPolicy dataConservationPolicy;
const ServerConfig({ const ServerConfig({
@required this.passwordPolicy,
@required this.dataConservationPolicy, @required this.dataConservationPolicy,
}) : assert(dataConservationPolicy != null); }) : assert(passwordPolicy != null),
assert(dataConservationPolicy != null);
} }

View File

@ -1,6 +1,9 @@
import 'package:comunic/helpers/account_helper.dart'; import 'package:comunic/helpers/account_helper.dart';
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/models/config.dart'; import 'package:comunic/models/config.dart';
import 'package:comunic/models/new_account.dart'; import 'package:comunic/models/new_account.dart';
import 'package:comunic/ui/widgets/async_screen_widget.dart';
import 'package:comunic/ui/widgets/new_password_input_widget.dart';
import 'package:comunic/utils/input_utils.dart'; import 'package:comunic/utils/input_utils.dart';
import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart'; import 'package:comunic/utils/ui_utils.dart';
@ -34,7 +37,7 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
final _firstNameController = TextEditingController(); final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController(); final _lastNameController = TextEditingController();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordInputKey = GlobalKey<NewPasswordInputWidgetState>();
final _verifyPasswordController = TextEditingController(); final _verifyPasswordController = TextEditingController();
bool _acceptedTOS = false; bool _acceptedTOS = false;
@ -49,10 +52,11 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
bool get _isEmailValid => validateEmail(_emailController.text); bool get _isEmailValid => validateEmail(_emailController.text);
bool get _isPasswordValid => _passwordController.text.length > 3; bool get _isPasswordValid => _passwordInputKey.currentState.valid;
bool get _isPasswordConfirmationValid => bool get _isPasswordConfirmationValid =>
_passwordController.text == _verifyPasswordController.text; _passwordInputKey.currentState != null &&
_passwordInputKey.currentState.value == _verifyPasswordController.text;
bool get _isFormValid => bool get _isFormValid =>
_isFirstNameValid && _isFirstNameValid &&
@ -69,10 +73,16 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
? tr( ? tr(
"Too many accounts have been created from this IP address for now. Please try again later.") "Too many accounts have been created from this IP address for now. Please try again later.")
: tr( : tr(
"An error occured while creating your account. Please try again."); "An error occurred while creating your account. Please try again.");
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => AsyncScreenWidget(
onReload: () => ServerConfigurationHelper.ensureLoaded(),
onBuild: () => _buildForm(),
errorMessage: tr("Failed to load server configuration !"),
);
Widget _buildForm() {
if (_isCreating) if (_isCreating)
return Center( return Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
@ -124,15 +134,14 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
), ),
// Password // Password
_InputEntry( NewPasswordInputWidget(
controller: _passwordController, key: _passwordInputKey,
label: tr("Password"), label: tr("Password"),
onEdited: _updateUI, onEdited: _updateUI,
icon: Icon(Icons.lock), icon: Icon(Icons.lock),
isPassword: true, email: _emailController.text,
error: _showErrors && !_isPasswordValid firstName: _firstNameController.text,
? tr("Invalid password!") lastName: _lastNameController.text,
: null,
), ),
// Verify password // Verify password
@ -206,7 +215,7 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
firstName: _firstNameController.text, firstName: _firstNameController.text,
lastName: _lastNameController.text, lastName: _lastNameController.text,
email: _emailController.text, email: _emailController.text,
password: _passwordController.text, password: _passwordInputKey.currentState.value,
)); ));
setState(() { setState(() {

View File

@ -0,0 +1,164 @@
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/models/server_config.dart';
import 'package:comunic/models/user.dart';
import 'package:comunic/utils/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart';
import 'package:flutter/material.dart';
/// New password input widget
///
/// @author Pierre Hubert
class NewPasswordInputWidget extends StatefulWidget {
final Widget icon;
final VoidCallback onEdited;
final String label;
final User user;
final String firstName;
final String lastName;
final String email;
const NewPasswordInputWidget({
Key key,
this.icon,
this.onEdited,
@required this.label,
this.user,
this.firstName,
this.lastName,
this.email,
}) : super(key: key);
@override
NewPasswordInputWidgetState createState() => NewPasswordInputWidgetState();
}
class NewPasswordInputWidgetState extends State<NewPasswordInputWidget> {
final TextEditingController _controller = TextEditingController();
bool _ready = false;
String get value => _controller.text;
bool get valid => _ready && value.isNotEmpty && (_errorMessage ?? "").isEmpty;
PasswordPolicy get _policy => ServerConfigurationHelper.config.passwordPolicy;
@override
void initState() {
super.initState();
_init();
}
/// Make sure server configuration is loaded
void _init() async {
try {
await ServerConfigurationHelper.ensureLoaded();
setState(() => _ready = true);
} catch (e, s) {
print("Failed to initialize NewPasswordInputWidget! : $e - $s");
showSimpleSnack(context, tr("Failed to retrieve server configuration!"));
}
}
@override
void didUpdateWidget(covariant NewPasswordInputWidget oldWidget) {
super.didUpdateWidget(oldWidget);
setState(() {});
}
@override
Widget build(BuildContext context) => TextField(
controller: _controller,
obscureText: true,
onChanged: (s) => _onChanged(),
decoration: InputDecoration(
errorText: _errorMessage,
errorMaxLines: 3,
icon: widget.icon,
labelText: widget.label,
),
);
void _onChanged() {
setState(() {});
if (widget.onEdited != null) widget.onEdited();
}
/// Generate an error message associated with current password
String get _errorMessage {
if (!_ready || value.isEmpty) return null;
// Mandatory checks
if (!_policy.allowMailInPassword &&
(widget.email ?? "").isNotEmpty &&
(widget.email.toLowerCase().contains(value.toLowerCase()) ||
value.toLowerCase().contains(widget.email.toLowerCase()))) {
return tr("Your password must not contains part of your email address!");
}
if (!_policy.allowMailInPassword &&
(widget.email ?? "").isNotEmpty &&
(widget.email.toLowerCase().contains(value.toLowerCase()) ||
value.toLowerCase().contains(widget.email.toLowerCase()))) {
return tr("Your password must not contains part of your email address!");
}
if (!_policy.allowNameInPassword &&
(widget.firstName ?? "").isNotEmpty &&
value.toLowerCase().contains(widget.firstName.toLowerCase())) {
return tr("Your password must not contains your first name!");
}
if (!_policy.allowNameInPassword &&
(widget.lastName ?? "").isNotEmpty &&
value.toLowerCase().contains(widget.lastName.toLowerCase())) {
return tr("Your password must not contains your last name!");
}
if (_policy.minPasswordLength > value.length) {
return tr(
"Your password must be composed of at least %num% characters!",
args: {
"num": _policy.minPasswordLength.toString(),
},
);
}
// Characteristics check
var count = 0;
if (_hasCharacteristic(RegExp(r'[A-Z]'), _policy.minNumberUpperCaseLetters))
count++;
if (_hasCharacteristic(RegExp(r'[a-z]'), _policy.minNumberLowerCaseLetters))
count++;
if (_hasCharacteristic(RegExp(r'[0-9]'), _policy.minNumberDigits)) count++;
if (_hasCharacteristic(
RegExp(r'[^A-Za-z0-9]'), _policy.minNumberSpecialCharacters)) count++;
if (count >= _policy.minCategoriesPresence) return null;
return tr(
"Your password must contains characters of at least %num% of the following categories : %upper% upper case letter, %lower% lowercase letter, %digit% digit, %special% special character.",
args: {
"num": (_policy.minCategoriesPresence == 4
? tr("ALL")
: _policy.minCategoriesPresence.toString()),
"upper": _policy.minNumberUpperCaseLetters.toString(),
"lower": _policy.minNumberLowerCaseLetters.toString(),
"digit": _policy.minNumberDigits.toString(),
"special": _policy.minNumberSpecialCharacters.toString(),
},
);
}
bool _hasCharacteristic(RegExp exp, int requiredCount) {
if (requiredCount < 1) return true;
return exp.allMatches(value).length >= requiredCount;
}
}