0001-create-AssistedPassword-AssistedPasswordFormMixin-an.patch
src/authentic2/app_settings.py | ||
---|---|---|
140 | 140 |
A2_VALIDATE_EMAIL=Setting(default=False, definition='Validate user email server by doing an RCPT command'), |
141 | 141 |
A2_VALIDATE_EMAIL_DOMAIN=Setting(default=True, definition='Validate user email domain'), |
142 | 142 |
A2_PASSWORD_POLICY_MIN_CLASSES=Setting(default=3, definition='Minimum number of characters classes to be present in passwords'), |
143 |
A2_PASSWORD_DISPLAY_LAST_CHAR_DURATION=Setting(default=1000, definition='Minimum duration in milliseconds to display last character'), |
|
144 |
A2_PASSWORD_DISPLAY_SHOW_ALL=Setting(default=True, definition='Boolean to display a button showing all the password characters'), |
|
145 |
A2_PASSWORD_DISPLAY_CHECK_POLICY=Setting(default=True, definition='Boolean to display password validation policy while typing'), |
|
146 |
A2_PASSWORD_DISPLAY_CHECK_EQUALITY=Setting(default=True, definition='Boolean to display password equality check while typing'), |
|
147 |
A2_PASSWORD_POLICY_MESSAGE_TPL=Setting(default='authentic2/password_help_text.html', definition='Django template path to the password policy text html'), |
|
143 | 148 |
A2_PASSWORD_POLICY_MIN_LENGTH=Setting(default=8, definition='Minimum number of characters in a password'), |
144 | 149 |
A2_PASSWORD_POLICY_REGEX=Setting(default=None, definition='Regular expression for validating passwords'), |
145 | 150 |
A2_PASSWORD_POLICY_REGEX_ERROR_MSG=Setting(default=None, definition='Error message to show when the password do not validate the regular expression'), |
src/authentic2/registration_backend/forms.py | ||
---|---|---|
1 | 1 |
import re |
2 | 2 |
import copy |
3 | 3 |
from collections import OrderedDict |
4 |
import json |
|
4 | 5 | |
5 | 6 |
from django.conf import settings |
6 | 7 |
from django.core.exceptions import ValidationError |
7 | 8 |
from django.utils.translation import ugettext_lazy as _, ugettext |
8 | 9 |
from django.forms import ModelForm, Form, CharField, PasswordInput, EmailField |
9 | 10 |
from django.utils.datastructures import SortedDict |
11 |
from django.utils.encoding import force_text |
|
10 | 12 |
from django.db.models.fields import FieldDoesNotExist |
11 | 13 |
from django.forms.util import ErrorList |
12 | 14 | |
13 | 15 |
from django.contrib.auth.models import BaseUserManager, Group |
14 | 16 |
from django.contrib.auth import forms as auth_forms, get_user_model, REDIRECT_FIELD_NAME |
17 |
from django.utils.safestring import mark_safe |
|
15 | 18 |
from django.core.mail import send_mail |
16 | 19 |
from django.core import signing |
17 | 20 |
from django.template import RequestContext |
... | ... | |
24 | 27 | |
25 | 28 |
User = compat.get_user_model() |
26 | 29 | |
30 |
class AssistedPasswordInput(PasswordInput): |
|
31 |
""" |
|
32 |
Custom Password Input with extra functionnality |
|
33 |
Inspired by Django >= 1.11 new-style rendering, and ensuring an easy future compatibility |
|
34 |
https://docs.djangoproject.com/fr/1.11/ref/forms/renderers/#overriding-built-in-widget-templates |
|
35 |
""" |
|
36 |
password_policy = { |
|
37 |
'A2_PASSWORD_POLICY_MIN_LENGTH': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, |
|
38 |
'A2_PASSWORD_POLICY_MIN_CLASSES': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES, |
|
39 |
'A2_PASSWORD_POLICY_REGEX': app_settings.A2_PASSWORD_POLICY_REGEX, |
|
40 |
} |
|
41 |
messages_class = 'a2-passwords-messages' |
|
42 |
template_name = 'authentic2/widgets/assisted_password.html' |
|
43 | ||
44 |
def render(self, name, value, attrs=None): |
|
45 |
""" |
|
46 |
Overridding render() to have a template-based widget |
|
47 |
https://docs.djangoproject.com/en/1.8/ref/forms/widgets/#django.forms.Widget.render |
|
48 |
""" |
|
49 |
if self.attrs.get('data-check-equality-against'): |
|
50 |
attrs['checkEquality'] = True |
|
51 |
# Remove this part down when dropping Django 1.8, 1.9, 1.10 compatibility |
|
52 |
if value is None: |
|
53 |
value = '' |
|
54 |
context = { |
|
55 |
'password_policy': json.dumps(self.password_policy), # to configure password.js using app_settings |
|
56 |
'messages_class': self.messages_class, # to configure the block for interactive messages |
|
57 |
'template_name': self.template_name, # to be used in templates to know who they are |
|
58 |
'widget': {} |
|
59 |
} |
|
60 |
context['widget']['attrs'] = self.build_attrs(extra_attrs=attrs, name=name, type=self.input_type) |
|
61 |
if value != '': |
|
62 |
# Only add the 'value' attribute if a value is non-empty. |
|
63 |
context['widget']['value'] = force_text(self._format_value(value)) |
|
64 |
return mark_safe(render_to_string(self.template_name, context)) |
|
65 | ||
66 | ||
67 |
class AssistedPasswordFormMixin(Form): |
|
68 |
class Media: |
|
69 |
js = ('authentic2/js/password.js',) |
|
70 |
css = {'all': ('authentic2/css/password.css',)} |
|
71 | ||
72 |
password1 = CharField( |
|
73 |
widget=AssistedPasswordInput(attrs={ |
|
74 |
'data-show-last': app_settings.A2_PASSWORD_DISPLAY_LAST_CHAR_DURATION, |
|
75 |
'data-show-all': app_settings.A2_PASSWORD_DISPLAY_SHOW_ALL, |
|
76 |
'data-check-policy': app_settings.A2_PASSWORD_DISPLAY_CHECK_POLICY, |
|
77 |
}), |
|
78 |
label=_("Password"), |
|
79 |
validators=[validators.validate_password], |
|
80 |
help_text=validators.password_help_text()) |
|
81 | ||
82 |
password2 = CharField( |
|
83 |
widget=AssistedPasswordInput(attrs={ |
|
84 |
'data-check-equality-against': 'password1' if app_settings.A2_PASSWORD_DISPLAY_CHECK_EQUALITY else False, |
|
85 |
}), |
|
86 |
label=_("Password (again)")) |
|
87 | ||
27 | 88 | |
28 | 89 |
class RegistrationForm(Form): |
29 | 90 |
error_css_class = 'form-field-error' |
... | ... | |
114 | 175 |
return user |
115 | 176 | |
116 | 177 | |
117 |
class RegistrationCompletionForm(RegistrationCompletionFormNoPassword): |
|
118 |
password1 = CharField(widget=PasswordInput, label=_("Password"), |
|
119 |
validators=[validators.validate_password], |
|
120 |
help_text=validators.password_help_text()) |
|
121 |
password2 = CharField(widget=PasswordInput, label=_("Password (again)")) |
|
122 | ||
178 |
class RegistrationCompletionForm(RegistrationCompletionFormNoPassword, AssistedPasswordFormMixin): |
|
123 | 179 |
def clean(self): |
124 | 180 |
""" |
125 | 181 |
Verifiy that the values entered into the two password fields |
src/authentic2/static/authentic2/css/password.css | ||
---|---|---|
1 |
input[data-check-policy] ~ .helptext { |
|
2 |
display: block; |
|
3 |
} |
|
4 | ||
5 |
.a2-min-class-policy, .a2-min-length-policy, .a2-regexp-policy { |
|
6 |
display: inline; |
|
7 |
} |
|
8 | ||
9 |
.password-error { |
|
10 |
font-weight: bold; |
|
11 |
color: red; |
|
12 |
} |
|
13 | ||
14 |
.password-error:before { |
|
15 |
content: "\f071"; |
|
16 |
margin-right: 0.3em; |
|
17 |
font-family: FontAwesome; |
|
18 |
font-size: 100%; |
|
19 |
color: red; |
|
20 |
} |
|
21 | ||
22 |
.password-ok:before { |
|
23 |
content: "\f00c"; |
|
24 |
font-family: FontAwesome; |
|
25 |
font-size: 100%; |
|
26 |
color: green; |
|
27 |
} |
|
28 | ||
29 |
.a2-show-password-button { |
|
30 |
padding-left: 0.5em; |
|
31 |
cursor: pointer; |
|
32 |
} |
|
33 | ||
34 |
.a2-show-password-button:after { |
|
35 |
content: "\f06e"; /* eye */ |
|
36 |
font-family: FontAwesome; |
|
37 |
font-size: 150%; |
|
38 |
} |
|
39 | ||
40 |
.hide-password-button:after { |
|
41 |
content: "\f070"; /* crossed eye */ |
|
42 |
font-family: FontAwesome; |
|
43 |
font-size: 150%; |
|
44 |
} |
|
45 | ||
46 |
.a2-passwords-messages { |
|
47 |
display: none; |
|
48 |
} |
|
49 | ||
50 |
.a2-passwords-unmatched { |
|
51 |
display: none; |
|
52 |
color: red; |
|
53 |
} |
|
54 | ||
55 |
.a2-passwords-matched { |
|
56 |
display: none; |
|
57 |
color: green; |
|
58 |
} |
|
59 | ||
60 |
.password-error.a2-passwords-messages, .password-ok.a2-passwords-messages { |
|
61 |
display: block; |
|
62 |
} |
|
63 | ||
64 |
.password-error .a2-passwords-unmatched { |
|
65 |
display: inline; |
|
66 |
} |
|
67 | ||
68 |
.password-ok .a2-passwords-matched { |
|
69 |
display: inline; |
|
70 |
} |
|
71 | ||
72 |
.a2-password-show-last { |
|
73 |
opacity: 0; |
|
74 |
padding: 1em; |
|
75 |
} |
src/authentic2/static/authentic2/js/password.js | ||
---|---|---|
1 |
"use strict"; |
|
2 |
/* globals $, window, console */ |
|
3 | ||
4 |
$(function () { |
|
5 |
var toggleError = function($elt) { |
|
6 |
$elt.removeClass('password-ok'); |
|
7 |
$elt.addClass('password-error'); |
|
8 |
} |
|
9 |
var toggleOk = function($elt) { |
|
10 |
$elt.removeClass('password-error'); |
|
11 |
$elt.addClass('password-ok'); |
|
12 |
} |
|
13 |
var validatePassword = function () { |
|
14 |
var minClassElt = $(this).parents('form').find('.a2-min-class-policy'); |
|
15 |
var minLengthElt = $(this).parents('form').find('.a2-min-length-policy'); |
|
16 |
var regexpElt = $(this).parents('form').find('.a2-regexp-policy'); |
|
17 |
$(this) |
|
18 |
.each(function () { |
|
19 |
var $this = $(this); |
|
20 |
var settings = $this.data('passwordPolicy'); |
|
21 |
var password = $this.val(); |
|
22 |
var min_len = settings.A2_PASSWORD_POLICY_MIN_LENGTH; |
|
23 |
if (min_len && password.length < min_len) { |
|
24 |
toggleError(minLengthElt); |
|
25 |
} else { |
|
26 |
toggleOk(minLengthElt); |
|
27 |
} |
|
28 |
var digits = /\d/g; |
|
29 |
var lowerCase = /[a-z]/g; |
|
30 |
var upperCase = /[A-Z]/g; |
|
31 |
var punctuation = /'!"#\$%&\\'\(\)\*\+,-\.\/:;<=>\?@\[\]\^_`\{\|\}~'/g; |
|
32 |
var minClassCount = settings.A2_PASSWORD_POLICY_MIN_CLASSES; |
|
33 |
var classCount = 0; |
|
34 |
if (minClassCount) { |
|
35 |
[digits, lowerCase, upperCase, punctuation].forEach(function (cls) { |
|
36 |
if (cls.test(password)) classCount++; |
|
37 |
}) |
|
38 |
if (classCount < minClassCount) { |
|
39 |
toggleError(minClassElt); |
|
40 |
} else { |
|
41 |
toggleOk(minClassElt); |
|
42 |
} |
|
43 |
} |
|
44 |
if (settings.A2_PASSWORD_POLICY_REGEX) { |
|
45 |
var regExpObject = new RegExp(settings.A2_PASSWORD_POLICY_REGEX, 'g'); |
|
46 |
if (!regExpObject.test(password)) { |
|
47 |
toggleError(regexpElt); |
|
48 |
} else { |
|
49 |
toggleOk(regexpElt); |
|
50 |
} |
|
51 |
} |
|
52 |
}); |
|
53 |
} |
|
54 |
var passwordEquality = function () { |
|
55 |
$(this) |
|
56 |
.each(function () { |
|
57 |
var input = $(this); |
|
58 |
var form = input.parents('form'); |
|
59 |
var messages = form.find('.'+input.data('passwordMessagesClass')); |
|
60 |
var inputTarget = form.find('input[name='+input.data('checkEqualityAgainst')+']'); |
|
61 |
if (!input.val() || !inputTarget.val()) return; |
|
62 |
if (inputTarget.val() !== input.val()) { |
|
63 |
toggleError(messages); |
|
64 |
} else { |
|
65 |
toggleOk(messages); |
|
66 |
} |
|
67 |
}); |
|
68 |
} |
|
69 |
var showPassword = function () { |
|
70 |
$(this).addClass('hide-password-button'); |
|
71 |
$(this).prevUntil().filter('input[data-show-all]').last().attr('type', 'text'); |
|
72 |
} |
|
73 |
var hidePassword = function () { |
|
74 |
$(this).removeClass('hide-password-button'); |
|
75 |
$(this).prevUntil().filter('input[data-show-all]').last().attr('type', 'password'); |
|
76 |
} |
|
77 |
/* |
|
78 |
* Show the last character |
|
79 |
*/ |
|
80 |
var debounce = function (func, milliseconds) { |
|
81 |
var timer; |
|
82 |
return function() { |
|
83 |
window.clearTimeout(timer); |
|
84 |
timer = window.setTimeout(function() { |
|
85 |
func(); |
|
86 |
}, milliseconds); |
|
87 |
}; |
|
88 |
} |
|
89 |
var showLastChar = function(event) { |
|
90 |
if (event.keyCode == 32 || event.key === undefined || event.key == "" || event.key == "Unidentified" || event.key.length > 1) { |
|
91 |
return; |
|
92 |
} |
|
93 |
var duration = Number.isNaN(parseInt($(this).data('showLast'))) ? 300 : parseInt($(this).data('showLast')); |
|
94 |
$('#a2-show-last-'+$(this).attr('id')) |
|
95 |
.html(event.key) |
|
96 |
.animate({'opacity': 1}, { |
|
97 |
duration: 50, |
|
98 |
queue: false, |
|
99 |
complete: function () { |
|
100 |
var $this = $(this); |
|
101 |
window.setTimeout( |
|
102 |
debounce(function () { |
|
103 |
$this.animate({'opacity': 0}, {duration: 50}); |
|
104 |
}, duration), duration); |
|
105 |
} |
|
106 |
}); |
|
107 |
} |
|
108 |
/* add password validation and equality check event handlers */ |
|
109 |
$('form').on('keyup', 'input[data-check-policy]', validatePassword); |
|
110 |
$('form').on('keyup', 'input[data-check-equality-against]', passwordEquality); |
|
111 |
/* while editing the first password, toggleError if the second one is not empty */ |
|
112 |
$('input[data-check-equality-against]') |
|
113 |
.each(function () { |
|
114 |
var input2 = $(this); |
|
115 |
var input1 = $('form').find('input[name='+input2.data('checkEqualityAgainst')+']'); |
|
116 |
$('form').on('keyup', input1, function () { |
|
117 |
var form = $(this) |
|
118 |
var messages = form.find('.'+input2.data('passwordMessagesClass')); |
|
119 |
if (input2.val().length) { |
|
120 |
if (input1.val() !== input2.val()) { |
|
121 |
toggleError(messages); |
|
122 |
} else { |
|
123 |
toggleOk(messages); |
|
124 |
} |
|
125 |
} |
|
126 |
}); |
|
127 |
}); |
|
128 | ||
129 |
/* add the a2-show-password-button after the first input */ |
|
130 |
$('input[data-show-all]') |
|
131 |
.each(function () { |
|
132 |
var $this = $(this); |
|
133 |
if (!$('#a2-show-password-button-'+$this.attr('id')).length) { |
|
134 |
$(this).after($('<i class="a2-show-password-button" id="a2-show-password-button-'+$this.attr('id')+'"></i>') |
|
135 |
.on('mousedown', showPassword) |
|
136 |
.on('mouseup', hidePassword) |
|
137 |
); |
|
138 |
} |
|
139 |
}); |
|
140 |
/* show the last character on keypress */ |
|
141 |
$('input[data-show-last]') |
|
142 |
.each(function () { |
|
143 |
var $this = $(this); |
|
144 |
if (!$('#a2-show-last-'+$this.attr('id')).length) { |
|
145 |
$this |
|
146 |
.after($('<b class="a2-password-show-last" id="a2-show-last-'+$this.attr('id')+'"> </b>')); |
|
147 |
} |
|
148 |
}); |
|
149 |
$('form').on('keyup', 'input[data-show-last]', showLastChar); |
|
150 |
}); |
src/authentic2/templates/authentic2/password_help_text.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
{% if A2_PASSWORD_POLICY_MIN_LENGTH and A2_PASSWORD_POLICY_MIN_CLASSES %} |
|
3 |
<span class="a2-min-length-policy">{% blocktrans %}Your password must contain at least {{ A2_PASSWORD_POLICY_MIN_LENGTH }} characters{% endblocktrans %}</span> {% trans 'from' %} <span class="a2-min-class-policy">{% blocktrans %}at least {{ A2_PASSWORD_POLICY_MIN_CLASSES }} classes among: lowercase letters, uppercase letters, digits and punctuations.{% endblocktrans %}</span> |
|
4 |
{% else %} |
|
5 |
{% if A2_PASSWORD_POLICY_MIN_LENGTH %} |
|
6 |
<span class="a2-min-length-policy">{% blocktrans %}Your password must contain at least {{ A2_PASSWORD_POLICY_MIN_LENGTH }} characters.{% endblocktrans %}</span> |
|
7 |
{% endif %} |
|
8 |
{% if A2_PASSWORD_POLICY_MIN_CLASSES %} |
|
9 |
<span class="a2-min-class-policy">{% blocktrans %}Your password must contain at least {{ A2_PASSWORD_POLICY_MIN_CLASSES }} classes among: lowercase letters, uppercase letters, digits and punctuations.{% endblocktrans %}</span> |
|
10 |
{% endif %} |
|
11 |
{% endif %} |
|
12 |
{% if A2_PASSWORD_POLICY_REGEX %} |
|
13 |
{% if A2_PASSWORD_POLICY_REGEX_ERROR_MSG %} |
|
14 |
<span class="a2-regexp-policy">{% blocktrans %}{{ A2_PASSWORD_POLICY_REGEX_ERROR_MSG }}{% endblocktrans %}</span> |
|
15 |
{% else %} |
|
16 |
<span class="a2-regexp-policy">{% blocktrans %}Your password must match the regular expression: {{ A2_PASSWORD_POLICY_REGEX }}, please change this message using the A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.'{% endblocktrans %}</span> |
|
17 |
{% endif %} |
|
18 | ||
19 |
{% endif %} |
src/authentic2/templates/authentic2/widgets/assisted_password.html | ||
---|---|---|
1 |
{% load i18n %} |
|
2 |
<input {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "authentic2/widgets/attrs.html" %} data-password-policy='{% autoescape off %}{{ password_policy }}{% endautoescape %}' data-password-messages-class='{{ messages_class }}'> |
|
3 |
{% if widget.attrs.checkEquality %} |
|
4 |
<div class="{{ messages_class }}"> |
|
5 |
<span class="a2-passwords-matched">{% trans 'Passwords match.' %}</span> |
|
6 |
<span class="a2-passwords-unmatched">{% trans 'Passwords do not match.' %}</span> |
|
7 |
</div> |
|
8 |
{% endif %} |
src/authentic2/templates/authentic2/widgets/attrs.html | ||
---|---|---|
1 |
{% comment %}Will be deprecated in Django 1.11 : replace with django/forms/widgets/attrs.html{% endcomment %} |
|
2 |
{% for name, value in widget.attrs.items %}{% if value != False %} {{ name }}{% if value != True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %} |
src/authentic2/validators.py | ||
---|---|---|
5 | 5 | |
6 | 6 |
import smtplib |
7 | 7 | |
8 |
from django.utils.translation import ugettext_lazy as _, ugettext
|
|
8 |
from django.utils.translation import ugettext_lazy as _ |
|
9 | 9 |
from django.utils.encoding import force_text |
10 | 10 |
from django.core.exceptions import ValidationError |
11 | 11 |
from django.core.validators import RegexValidator |
12 |
from django.utils.functional import lazy |
|
12 |
from django.template.loader import render_to_string |
|
13 |
from django.utils.safestring import mark_safe |
|
13 | 14 | |
14 | 15 |
import socket |
15 | 16 |
import dns.resolver |
... | ... | |
119 | 120 |
super(UsernameValidator, self).__init__(*args, **kwargs) |
120 | 121 | |
121 | 122 | |
122 |
def __password_help_text_helper(): |
|
123 |
if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH and \ |
|
124 |
app_settings.A2_PASSWORD_POLICY_MIN_CLASSES: |
|
125 |
yield ugettext('Your password must contain at least %(min_length)d characters from at ' |
|
126 |
'least %(min_classes)d classes among: lowercase letters, uppercase letters, ' |
|
127 |
'digits and punctuations.') % { |
|
128 |
'min_length': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, |
|
129 |
'min_classes': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES} |
|
130 |
else: |
|
131 |
if app_settings.A2_PASSWORD_POLICY_MIN_LENGTH: |
|
132 |
yield ugettext('Your password must contain at least %(min_length)d characters.') % {'min_length': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH} |
|
133 |
if app_settings.A2_PASSWORD_POLICY_MIN_CLASSES: |
|
134 |
yield ugettext('Your password must contain characters from at least %(min_classes)d ' |
|
135 |
'classes among: lowercase letters, uppercase letters, digits ' |
|
136 |
'and punctuations.') % {'min_classes': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES} |
|
137 |
if app_settings.A2_PASSWORD_POLICY_REGEX: |
|
138 |
yield ugettext(app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG) or \ |
|
139 |
ugettext('Your password must match the regular expression: ' |
|
140 |
'%(regexp)s, please change this message using the ' |
|
141 |
'A2_PASSWORD_POLICY_REGEX_ERROR_MSG setting.') % \ |
|
142 |
{'regexp': app_settings.A2_PASSWORD_POLICY_REGEX} |
|
143 | ||
144 | 123 |
def password_help_text(): |
145 |
return ' '.join(__password_help_text_helper()) |
|
146 | ||
147 |
password_help_text = lazy(password_help_text, six.text_type) |
|
124 |
''' |
|
125 |
Password fields help_text |
|
126 |
''' |
|
127 |
context = { |
|
128 |
'A2_PASSWORD_POLICY_MIN_LENGTH': app_settings.A2_PASSWORD_POLICY_MIN_LENGTH, |
|
129 |
'A2_PASSWORD_POLICY_MIN_CLASSES': app_settings.A2_PASSWORD_POLICY_MIN_CLASSES, |
|
130 |
'A2_PASSWORD_POLICY_REGEX': app_settings.A2_PASSWORD_POLICY_REGEX, |
|
131 |
'A2_PASSWORD_POLICY_REGEX_ERROR_MSG': app_settings.A2_PASSWORD_POLICY_REGEX_ERROR_MSG |
|
132 |
} |
|
133 |
return mark_safe(render_to_string(app_settings.A2_PASSWORD_POLICY_MESSAGE_TPL, context)) |
|
148 |
- |