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'),
# ...
]
1from datetime import datetime
2
3from plain.runtime import settings
4from plain.utils.crypto import constant_time_compare, salted_hmac
5from plain.utils.http import base36_to_int, int_to_base36
6
7
8class PasswordResetTokenGenerator:
9 """
10 Strategy object used to generate and check tokens for the password
11 reset mechanism.
12 """
13
14 key_salt = "plain.auth.tokens.PasswordResetTokenGenerator"
15 algorithm = None
16 _secret = None
17 _secret_fallbacks = None
18
19 def __init__(self):
20 self.algorithm = self.algorithm or "sha256"
21
22 def _get_secret(self):
23 return self._secret or settings.SECRET_KEY
24
25 def _set_secret(self, secret):
26 self._secret = secret
27
28 secret = property(_get_secret, _set_secret)
29
30 def _get_fallbacks(self):
31 if self._secret_fallbacks is None:
32 return settings.SECRET_KEY_FALLBACKS
33 return self._secret_fallbacks
34
35 def _set_fallbacks(self, fallbacks):
36 self._secret_fallbacks = fallbacks
37
38 secret_fallbacks = property(_get_fallbacks, _set_fallbacks)
39
40 def make_token(self, user):
41 """
42 Return a token that can be used once to do a password reset
43 for the given user.
44 """
45 return self._make_token_with_timestamp(
46 user,
47 self._num_seconds(self._now()),
48 self.secret,
49 )
50
51 def check_token(self, user, token):
52 """
53 Check that a password reset token is correct for a given user.
54 """
55 if not (user and token):
56 return False
57 # Parse the token
58 try:
59 ts_b36, _ = token.split("-")
60 except ValueError:
61 return False
62
63 try:
64 ts = base36_to_int(ts_b36)
65 except ValueError:
66 return False
67
68 # Check that the timestamp/uid has not been tampered with
69 for secret in [self.secret, *self.secret_fallbacks]:
70 if constant_time_compare(
71 self._make_token_with_timestamp(user, ts, secret),
72 token,
73 ):
74 break
75 else:
76 return False
77
78 # Check the timestamp is within limit.
79 if (self._num_seconds(self._now()) - ts) > settings.PASSWORD_RESET_TIMEOUT:
80 return False
81
82 return True
83
84 def _make_token_with_timestamp(self, user, timestamp, secret):
85 # timestamp is number of seconds since 2001-1-1. Converted to base 36,
86 # this gives us a 6 digit string until about 2069.
87 ts_b36 = int_to_base36(timestamp)
88 hash_string = salted_hmac(
89 self.key_salt,
90 self._make_hash_value(user, timestamp),
91 secret=secret,
92 algorithm=self.algorithm,
93 ).hexdigest()[::2] # Limit to shorten the URL.
94 return f"{ts_b36}-{hash_string}"
95
96 def _make_hash_value(self, user, timestamp):
97 """
98 Hash the user's primary key, email (if available), and some user state
99 that's sure to change after a password reset to produce a token that is
100 invalidated when it's used:
101 1. The password field will change upon a password reset (even if the
102 same password is chosen, due to password salting).
103 Failing those things, settings.PASSWORD_RESET_TIMEOUT eventually
104 invalidates the token.
105
106 Running this data through salted_hmac() prevents password cracking
107 attempts using the reset token, provided the secret isn't compromised.
108 """
109 return f"{user.pk}{user.password}{timestamp}{user.email}"
110
111 def _num_seconds(self, dt):
112 return int((dt - datetime(2001, 1, 1)).total_seconds())
113
114 def _now(self):
115 # Used for mocking in tests
116 return datetime.now()
117
118
119default_token_generator = PasswordResetTokenGenerator()