1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-10-23 06:53:23 +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())
.getObject();
final passwordPolicy = response["password_policy"];
final dataConservationPolicy = response["data_conservation_policy"];
_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(
minInactiveAccountLifetime:
dataConservationPolicy["min_inactive_account_lifetime"],

View File

@ -4,6 +4,35 @@ import 'package:flutter/widgets.dart';
///
/// @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 {
final int minInactiveAccountLifetime;
final int minNotificationLifetime;
@ -28,9 +57,12 @@ class ServerDataConservationPolicy {
}
class ServerConfig {
final PasswordPolicy passwordPolicy;
final ServerDataConservationPolicy dataConservationPolicy;
const ServerConfig({
@required this.passwordPolicy,
@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/server_config_helper.dart';
import 'package:comunic/models/config.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/intl_utils.dart';
import 'package:comunic/utils/ui_utils.dart';
@ -34,7 +37,7 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _passwordInputKey = GlobalKey<NewPasswordInputWidgetState>();
final _verifyPasswordController = TextEditingController();
bool _acceptedTOS = false;
@ -49,10 +52,11 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
bool get _isEmailValid => validateEmail(_emailController.text);
bool get _isPasswordValid => _passwordController.text.length > 3;
bool get _isPasswordValid => _passwordInputKey.currentState.valid;
bool get _isPasswordConfirmationValid =>
_passwordController.text == _verifyPasswordController.text;
_passwordInputKey.currentState != null &&
_passwordInputKey.currentState.value == _verifyPasswordController.text;
bool get _isFormValid =>
_isFirstNameValid &&
@ -69,10 +73,16 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
? tr(
"Too many accounts have been created from this IP address for now. Please try again later.")
: tr(
"An error occured while creating your account. Please try again.");
"An error occurred while creating your account. Please try again.");
@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)
return Center(
child: CircularProgressIndicator(),
@ -124,15 +134,14 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
),
// Password
_InputEntry(
controller: _passwordController,
NewPasswordInputWidget(
key: _passwordInputKey,
label: tr("Password"),
onEdited: _updateUI,
icon: Icon(Icons.lock),
isPassword: true,
error: _showErrors && !_isPasswordValid
? tr("Invalid password!")
: null,
email: _emailController.text,
firstName: _firstNameController.text,
lastName: _lastNameController.text,
),
// Verify password
@ -206,7 +215,7 @@ class __CreateAccountRouteBodyState extends State<_CreateAccountRouteBody> {
firstName: _firstNameController.text,
lastName: _lastNameController.text,
email: _emailController.text,
password: _passwordController.text,
password: _passwordInputKey.currentState.value,
));
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;
}
}