passwords: enhances passwords rules
- based on entropy (min 60 bits) - adds a coloured progress bar if Js is enabled
This commit is contained in:
parent
4117ec4b15
commit
0896a6b163
|
@ -72,7 +72,7 @@ class UpdateAccountForm(FlaskForm):
|
|||
],
|
||||
)
|
||||
password = PasswordField(
|
||||
'Mot de passe',
|
||||
'Nouveau mot de passe',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.password,
|
||||
|
|
|
@ -35,6 +35,10 @@ class User(UserMixin, db.Model):
|
|||
# Other fields populated automatically through relations:
|
||||
# <posts> relationship populated from the Post class.
|
||||
|
||||
# Minimum and maximum user name length
|
||||
NAME_MINLEN = 3
|
||||
NAME_MAXLEN = 32
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': __tablename__,
|
||||
'polymorphic_on': type
|
||||
|
@ -54,7 +58,7 @@ class Guest(User):
|
|||
id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||
# Reusable username, cannot be chosen as the name of a member
|
||||
# but will be distinguished at rendering time if a member take it later
|
||||
name = db.Column(db.Unicode(64))
|
||||
name = db.Column(db.Unicode(User.NAME_MAXLEN))
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
@ -73,8 +77,8 @@ class Member(User):
|
|||
id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
|
||||
|
||||
# Primary attributes (needed for the system to work)
|
||||
name = db.Column(db.Unicode(V5Config.USER_NAME_MAXLEN), index=True)
|
||||
norm = db.Column(db.Unicode(V5Config.USER_NAME_MAXLEN), index=True,
|
||||
name = db.Column(db.Unicode(User.NAME_MAXLEN), index=True)
|
||||
norm = db.Column(db.Unicode(User.NAME_MAXLEN), index=True,
|
||||
unique=True)
|
||||
email = db.Column(db.Unicode(120), index=True, unique=True)
|
||||
email_confirmed = db.Column(db.Boolean)
|
||||
|
|
|
@ -35,7 +35,8 @@ def edit_account():
|
|||
else:
|
||||
flash('Erreur lors de la modification', 'error')
|
||||
|
||||
return render('account/account.html', form=form)
|
||||
return render('account/account.html', scripts=["+scripts/entropy.js"],
|
||||
form=form)
|
||||
|
||||
@app.route('/compte/reinitialiser', methods=['GET', 'POST'])
|
||||
@guest_only
|
||||
|
@ -74,7 +75,8 @@ def reset_password(token):
|
|||
else:
|
||||
flash('Erreur lors de la modification', 'error')
|
||||
|
||||
return render('account/reset_password.html', form=form)
|
||||
return render('account/reset_password.html',
|
||||
scripts=["+scripts/entropy.js"], form=form)
|
||||
|
||||
|
||||
@app.route('/compte/supprimer', methods=['GET', 'POST'])
|
||||
|
@ -113,7 +115,8 @@ def register():
|
|||
send_validation_mail(member.name, member.email)
|
||||
|
||||
return redirect(url_for('validation') + "?email=" + form.email.data)
|
||||
return render('account/register.html', title='Register', form=form)
|
||||
return render('account/register.html', title='Register',
|
||||
scripts=["+scripts/entropy.js"], form=form)
|
||||
|
||||
|
||||
@app.route('/inscription/validation', methods=['GET'])
|
||||
|
|
|
@ -107,9 +107,10 @@ def adm_edit_account(user_id):
|
|||
for g in user.groups:
|
||||
groups_owned.add(f"g{g.id}")
|
||||
|
||||
return render('admin/edit_account.html', user=user, form=form,
|
||||
trophy_form=trophy_form, trophies_owned=trophies_owned,
|
||||
group_form=group_form, groups_owned=groups_owned)
|
||||
return render('admin/edit_account.html', scripts=["+scripts/entropy.js"],
|
||||
user=user, form=form, trophy_form=trophy_form,
|
||||
trophies_owned=trophies_owned, group_form=group_form,
|
||||
groups_owned=groups_owned)
|
||||
|
||||
|
||||
@app.route('/admin/compte/<user_id>/supprimer', methods=['GET', 'POST'])
|
||||
|
|
|
@ -50,6 +50,31 @@
|
|||
resize: vertical;
|
||||
}
|
||||
|
||||
.form progress.entropy {
|
||||
display: none; /* display with Js enabled */
|
||||
width: 100%; margin-top: 5px;
|
||||
background: var(--background);
|
||||
border: var(--border);
|
||||
}
|
||||
.form progress.entropy.low::-moz-progress-bar {
|
||||
background: var(--error);
|
||||
}
|
||||
.form progress.entropy.low::-webkit-progress-bar {
|
||||
background: var(--error);
|
||||
}
|
||||
.form progress.entropy.medium::-moz-progress-bar {
|
||||
background: var(--warn);
|
||||
}
|
||||
.form progress.entropy.medium::-webkit-progress-bar {
|
||||
background: var(--warn);
|
||||
}
|
||||
.form progress.entropy.high::-moz-progress-bar {
|
||||
background: var(--ok);
|
||||
}
|
||||
.form progress.entropy.high::-webkit-progress-bar {
|
||||
background: var(--ok);
|
||||
}
|
||||
|
||||
.form input[type="checkbox"],
|
||||
.form input[type="radio"] {
|
||||
display: inline;
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
function entropy(password) {
|
||||
var chars = [
|
||||
"abcdefghijklmnopqrstuvwxyz",
|
||||
"ABCDFEGHIJKLMNOPQRSTUVWXYZ",
|
||||
"0123456789",
|
||||
" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", // OWASP special chars
|
||||
"áàâéèêíìîóòôúùûç"
|
||||
];
|
||||
|
||||
used = new Set();
|
||||
for(c in password) {
|
||||
for(k in chars) {
|
||||
if(chars[k].includes(password[c])) {
|
||||
used.add(chars[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Math.log(Math.pow(Array.from(used).join("").length, password.length)) / Math.log(2);
|
||||
}
|
||||
|
||||
function update_entropy(ev) {
|
||||
var i = document.querySelector(".entropy").previousElementSibling;
|
||||
var p = document.querySelector(".entropy");
|
||||
var e = entropy(i.value);
|
||||
|
||||
p.classList.remove('low');
|
||||
p.classList.remove('medium');
|
||||
p.classList.remove('high');
|
||||
|
||||
if(e < 60) {
|
||||
p.classList.add('low');
|
||||
} else if(e < 100) {
|
||||
p.classList.add('medium');
|
||||
} else {
|
||||
p.classList.add('high');
|
||||
}
|
||||
|
||||
p.value = e;
|
||||
}
|
||||
|
||||
document.querySelector(".entropy").previousElementSibling.addEventListener('input', update_entropy);
|
||||
document.querySelector(".entropy").style.display = "block";
|
|
@ -30,6 +30,7 @@
|
|||
<div>
|
||||
{{ form.password.label }}
|
||||
{{ form.password(placeholder='************') }}
|
||||
<progress class="entropy" value="0" max="100"></progress>
|
||||
{% for error in form.password.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
<div>
|
||||
{{ form.password.label }}
|
||||
{{ form.password() }}
|
||||
<progress class="entropy" value="0" max="100"></progress>
|
||||
{% for error in form.password.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
{{ form.password.label }}
|
||||
<div class=desc>{{ form.password.description }}</div>
|
||||
{{ form.password() }}
|
||||
<progress class="entropy" value="0" max="100"></progress>
|
||||
{% for error in form.password.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
{{ form.password.label }}
|
||||
<div class=desc>{{ form.password.description }}</div>
|
||||
{{ form.password(placeholder='************') }}
|
||||
<progress class="entropy" value="0" max="100"></progress>
|
||||
{% for error in form.password.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
|
|
|
@ -36,14 +36,12 @@ def render(*args, styles=[], scripts=[], **kwargs):
|
|||
]
|
||||
|
||||
for s in styles:
|
||||
print(s[1:])
|
||||
if s[0] == '-':
|
||||
styles_.remove(s[1:])
|
||||
if s[0] == '+':
|
||||
styles_.append(s[1:])
|
||||
|
||||
for s in scripts:
|
||||
print(s[1:])
|
||||
if s[0] == '-':
|
||||
scripts_.remove(s[1:])
|
||||
if s[0] == '+':
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from flask_login import current_user
|
||||
from wtforms.validators import ValidationError
|
||||
from PIL import Image
|
||||
from app.models.users import Member
|
||||
from app.models.users import Member, User
|
||||
from app.utils.valid_name import valid_name
|
||||
from app.utils.unicode_names import normalize
|
||||
from math import log
|
||||
import app.utils.ldap as ldap
|
||||
from config import V5Config
|
||||
|
||||
|
@ -14,10 +15,10 @@ def name_valid(form, name):
|
|||
msg = {
|
||||
"too-short":
|
||||
"Le nom d'utilisateur doit faire au moins "
|
||||
f"{V5Config.USER_NAME_MINLEN} caractères.",
|
||||
f"{User.USER_NAME_MINLEN} caractères.",
|
||||
"too-long":
|
||||
"Le nom d'utilisateur doit faire au plus "
|
||||
f"{V5Config.USER_NAME_MAXLEN} caractères.",
|
||||
f"{User.USER_NAME_MAXLEN} caractères.",
|
||||
"cant-normalize":
|
||||
"Ce nom d'utilisateur contient des caractères interdits. Les "
|
||||
"caractères autorisés sont les lettres, lettres accentuées, "
|
||||
|
@ -63,37 +64,26 @@ def password(form, password):
|
|||
if len(password.data) == 0:
|
||||
return
|
||||
|
||||
errors = []
|
||||
if len(password.data) < V5Config.PASSWORD_MINLEN:
|
||||
errors.append('Le mot de passe doit faire au moins '
|
||||
f'{V5Config.PASSWORD_MINLEN} caractères.')
|
||||
def entropy(password):
|
||||
"""Estimate entropy of a password, in bits"""
|
||||
# If you edit this function, please edit accordingly the JS one
|
||||
# in static/script/entropy.js
|
||||
chars = [
|
||||
"abcdefghijklmnopqrstuvwxyz",
|
||||
"ABCDFEGHIJKLMNOPQRSTUVWXYZ",
|
||||
"0123456789",
|
||||
" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", # OWASP special chars
|
||||
"áàâéèêíìîóòôúùûç",
|
||||
]
|
||||
used = set()
|
||||
for c in password:
|
||||
for i in chars:
|
||||
if c in i:
|
||||
used.add(i)
|
||||
return log(len(''.join(used)) ** len(password), 2)
|
||||
|
||||
checks = set()
|
||||
for c in password.data:
|
||||
if c in "abcdefghijklmnopqrstuvwxyz":
|
||||
checks.add('lower')
|
||||
elif c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
||||
checks.add('upper')
|
||||
elif c in "0123456789":
|
||||
checks.add('numeric')
|
||||
else:
|
||||
checks.add('other')
|
||||
|
||||
missing = []
|
||||
if 'lower' not in checks:
|
||||
missing.append('une minuscule')
|
||||
if 'upper' not in checks:
|
||||
missing.append('une majuscule')
|
||||
if 'numeric' not in checks:
|
||||
missing.append('un chiffre')
|
||||
if 'other' not in checks:
|
||||
missing.append('un caractère spécial')
|
||||
|
||||
if missing != []:
|
||||
errors.append('Le mot de passe doit aussi contenir ' + ', '.join(missing) + '.')
|
||||
|
||||
if errors != []:
|
||||
raise ValidationError(' '.join(errors))
|
||||
if entropy(password.data) < 60:
|
||||
raise ValidationError("Mot de passe pas assez complexe")
|
||||
|
||||
|
||||
def avatar(form, avatar):
|
||||
|
|
|
@ -30,11 +30,6 @@ class DefaultConfig(object):
|
|||
FORBIDDEN_USERNAMES = ["admin", "root", "webmaster", "contact"]
|
||||
# Unauthorized message (@priv_required)
|
||||
UNAUTHORIZED_MSG = "Vous n'avez pas l'autorisation d'effectuer cette action !"
|
||||
# Minimum and maximum user name length
|
||||
USER_NAME_MINLEN = 3
|
||||
USER_NAME_MAXLEN = 32
|
||||
# Minimum password length for new users and new passwords
|
||||
PASSWORD_MINLEN = 10
|
||||
# Maximum thread name length
|
||||
THREAD_NAME_MAXLEN = 32
|
||||
# Amount of comments per thread page
|
||||
|
|
Loading…
Reference in New Issue