plain.password
Password authentication for Plain.
Usage
To enable password authentication in your Plain application, add the PasswordLoginView
to your urls.py
:
# app/urls.py
from plain.urls import path
from plain.passwords.views import PasswordLoginView
urlpatterns = [
path('login/', PasswordLoginView.as_view(), name='login'),
# ...
]
This sets up a basic login view where users can authenticate using their username and password.
FAQs
How do I customize the login form?
To customize the login form, you can subclass PasswordLoginForm
and override its fields or methods as needed. Then, set the form_class
attribute in your PasswordLoginView
to use your custom form.
# app/forms.py
from plain.passwords.forms import PasswordLoginForm
class MyCustomLoginForm(PasswordLoginForm):
# Add custom fields or override methods here
pass
# app/views.py
from plain.passwords.views import PasswordLoginView
from .forms import MyCustomLoginForm
class MyPasswordLoginView(PasswordLoginView):
form_class = MyCustomLoginForm
Update your urls.py
to use your custom view:
# app/urls.py
from plain.urls import path
from .views import MyPasswordLoginView
urlpatterns = [
path('login/', MyPasswordLoginView.as_view(), name='login'),
# ...
]
1import gzip
2from pathlib import Path
3
4from plain.exceptions import (
5 ValidationError,
6)
7from plain.utils.deconstruct import deconstructible
8from plain.utils.functional import cached_property
9from plain.utils.text import pluralize
10
11
12@deconstructible
13class MinimumLengthValidator:
14 """
15 Validate that the password is of a minimum length.
16 """
17
18 def __init__(self, min_length=8):
19 self.min_length = min_length
20
21 def __call__(self, password):
22 if len(password) < self.min_length:
23 raise ValidationError(
24 pluralize(
25 "This password is too short. It must contain at least "
26 "%(min_length)d character.",
27 "This password is too short. It must contain at least "
28 "%(min_length)d characters.",
29 self.min_length,
30 ),
31 code="password_too_short",
32 params={"min_length": self.min_length},
33 )
34
35
36# def exceeds_maximum_length_ratio(password, max_similarity, value):
37# """
38# Test that value is within a reasonable range of password.
39
40# The following ratio calculations are based on testing SequenceMatcher like
41# this:
42
43# for i in range(0,6):
44# print(10**i, SequenceMatcher(a='A', b='A'*(10**i)).quick_ratio())
45
46# which yields:
47
48# 1 1.0
49# 10 0.18181818181818182
50# 100 0.019801980198019802
51# 1000 0.001998001998001998
52# 10000 0.00019998000199980003
53# 100000 1.999980000199998e-05
54
55# This means a length_ratio of 10 should never yield a similarity higher than
56# 0.2, for 100 this is down to 0.02 and for 1000 it is 0.002. This can be
57# calculated via 2 / length_ratio. As a result we avoid the potentially
58# expensive sequence matching.
59# """
60# pwd_len = len(password)
61# length_bound_similarity = max_similarity / 2 * pwd_len
62# value_len = len(value)
63# return pwd_len >= 10 * value_len and value_len < length_bound_similarity
64
65
66# @deconstructible
67# class UserAttributeSimilarityValidator:
68# """
69# Validate that the password is sufficiently different from the user's
70# attributes.
71
72# If no specific attributes are provided, look at a sensible list of
73# defaults. Attributes that don't exist are ignored. Comparison is made to
74# not only the full attribute value, but also its components, so that, for
75# example, a password is validated against either part of an email address,
76# as well as the full address.
77# """
78
79# DEFAULT_USER_ATTRIBUTES = ("username", "email")
80
81# def __init__(self, user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7):
82# self.user_attributes = user_attributes
83# if max_similarity < 0.1:
84# raise ValueError("max_similarity must be at least 0.1")
85# self.max_similarity = max_similarity
86
87# def validate(self, password, user=None):
88# if not user:
89# return
90
91# password = password.lower()
92# for attribute_name in self.user_attributes:
93# value = getattr(user, attribute_name, None)
94# if not value or not isinstance(value, str):
95# continue
96# value_lower = value.lower()
97# value_parts = re.split(r"\W+", value_lower) + [value_lower]
98# for value_part in value_parts:
99# if exceeds_maximum_length_ratio(
100# password, self.max_similarity, value_part
101# ):
102# continue
103# if (
104# SequenceMatcher(a=password, b=value_part).quick_ratio()
105# >= self.max_similarity
106# ):
107# try:
108# verbose_name = str(
109# user._meta.get_field(attribute_name).verbose_name
110# )
111# except FieldDoesNotExist:
112# verbose_name = attribute_name
113# raise ValidationError(
114# "The password is too similar to the %(verbose_name)s.",
115# code="password_too_similar",
116# params={"verbose_name": verbose_name},
117# )
118
119# def get_help_text(self):
120# return "Your password can’t be too similar to your other personal information."
121
122
123@deconstructible
124class CommonPasswordValidator:
125 """
126 Validate that the password is not a common password.
127
128 The password is rejected if it occurs in a provided list of passwords,
129 which may be gzipped. The list Plain ships with contains 20000 common
130 passwords (lowercased and deduplicated), created by Royce Williams:
131 https://gist.github.com/roycewilliams/226886fd01572964e1431ac8afc999ce
132 The password list must be lowercased to match the comparison in validate().
133 """
134
135 @cached_property
136 def DEFAULT_PASSWORD_LIST_PATH(self):
137 return Path(__file__).resolve().parent / "common-passwords.txt.gz"
138
139 def __init__(self, password_list_path=DEFAULT_PASSWORD_LIST_PATH):
140 if password_list_path is CommonPasswordValidator.DEFAULT_PASSWORD_LIST_PATH:
141 password_list_path = self.DEFAULT_PASSWORD_LIST_PATH
142 try:
143 with gzip.open(password_list_path, "rt", encoding="utf-8") as f:
144 self.passwords = {x.strip() for x in f}
145 except OSError:
146 with open(password_list_path) as f:
147 self.passwords = {x.strip() for x in f}
148
149 def __call__(self, password):
150 if password.lower().strip() in self.passwords:
151 raise ValidationError(
152 "This password is too common.",
153 code="password_too_common",
154 )
155
156
157@deconstructible
158class NumericPasswordValidator:
159 """
160 Validate that the password is not entirely numeric.
161 """
162
163 def __call__(self, password):
164 if password.isdigit():
165 raise ValidationError(
166 "This password is entirely numeric.",
167 code="password_entirely_numeric",
168 )