1from datetime import datetime
2
3from plain import signing
4from plain.auth import get_user_model
5from plain.auth.sessions import login as auth_login
6from plain.auth.sessions import update_session_auth_hash
7from plain.exceptions import BadRequest
8from plain.http import (
9 ResponseRedirect,
10)
11from plain.urls import reverse
12from plain.utils.cache import add_never_cache_headers
13from plain.utils.crypto import constant_time_compare
14from plain.views import CreateView, FormView
15
16from .forms import (
17 PasswordChangeForm,
18 PasswordLoginForm,
19 PasswordResetForm,
20 PasswordSetForm,
21 PasswordSignupForm,
22)
23
24
25class PasswordForgotView(FormView):
26 form_class = PasswordResetForm
27 reset_confirm_url_name: str
28
29 def generate_password_reset_token(self, user):
30 return signing.dumps(
31 {
32 "pk": user.pk,
33 "email": user.email,
34 "password": user.password, # Hashed password
35 "timestamp": datetime.now().timestamp(), # Makes each token unique
36 },
37 salt="password-reset",
38 compress=True,
39 )
40
41 def generate_password_reset_url(self, user):
42 token = self.generate_password_reset_token(user)
43 url = reverse(self.reset_confirm_url_name) + f"?token={token}"
44 return self.request.build_absolute_uri(url)
45
46 def form_valid(self, form):
47 form.save(
48 generate_reset_url=self.generate_password_reset_url,
49 )
50 return super().form_valid(form)
51
52
53class PasswordResetView(FormView):
54 form_class = PasswordSetForm
55 reset_token_max_age = 60 * 60 # 1 hour
56 _reset_token_session_key = "_password_reset_token"
57
58 def check_password_reset_token(self, token):
59 max_age = self.reset_token_max_age
60
61 try:
62 data = signing.loads(token, salt="password-reset", max_age=max_age)
63 except signing.SignatureExpired:
64 return
65 except signing.BadSignature:
66 return
67
68 UserModel = get_user_model()
69 try:
70 user = UserModel._default_manager.get(pk=data["pk"])
71 except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist):
72 return
73
74 # If the password has changed since the token was generated, the token is invalid.
75 # (These are the hashed passwords, not the raw passwords.)
76 if not constant_time_compare(user.password, data["password"]):
77 return
78
79 # If the email has changed since the token was generated, the token is invalid.
80 if not constant_time_compare(user.email, data["email"]):
81 return
82
83 return user
84
85 def get(self):
86 if self.request.user:
87 # Redirect if the user is already logged in
88 return ResponseRedirect(self.success_url)
89
90 # Tokens are initially passed as GET parameters and we
91 # immediately store them in the session and remove it from the URL.
92 if token := self.request.query_params.get("token", ""):
93 # Store the token in the session and redirect to the
94 # password reset form at a URL without the token. That
95 # avoids the possibility of leaking the token in the
96 # HTTP Referer header.
97 self.request.session[self._reset_token_session_key] = token
98 # Redirect to the path itself, without the GET parameters
99 response = ResponseRedirect(self.request.path)
100 add_never_cache_headers(response)
101 return response
102
103 return super().get()
104
105 def get_user(self):
106 session_token = self.request.session.get(self._reset_token_session_key, "")
107 if not session_token:
108 # No token in the session, so we can't check the password reset token.
109 raise BadRequest("No password reset token found.")
110
111 user = self.check_password_reset_token(session_token)
112 if not user:
113 # Remove it from the session if it is invalid.
114 del self.request.session[self._reset_token_session_key]
115 raise BadRequest("Password reset token is no longer valid.")
116
117 return user
118
119 def get_form_kwargs(self):
120 kwargs = super().get_form_kwargs()
121 kwargs["user"] = self.get_user()
122 return kwargs
123
124 def form_valid(self, form):
125 form.save()
126 del self.request.session[self._reset_token_session_key]
127 # If you wanted, you could log in the user here so they don't have to
128 # go through the log in form again.
129 return super().form_valid(form)
130
131
132class PasswordChangeView(FormView):
133 # Change to PasswordSetForm if you want to set new passwords
134 # without confirming the old one.
135 form_class = PasswordChangeForm
136
137 def get_form_kwargs(self):
138 kwargs = super().get_form_kwargs()
139 kwargs["user"] = self.request.user
140 return kwargs
141
142 def form_valid(self, form):
143 form.save()
144 # Updating the password logs out all other sessions for the user
145 # except the current one.
146 update_session_auth_hash(self.request, form.user)
147 return super().form_valid(form)
148
149
150class PasswordLoginView(FormView):
151 form_class = PasswordLoginForm
152 success_url = "/"
153
154 def get(self):
155 # Redirect if the user is already logged in
156 if self.request.user:
157 return ResponseRedirect(self.success_url)
158
159 return super().get()
160
161 def form_valid(self, form):
162 # Log the user in and redirect
163 auth_login(self.request, form.get_user())
164
165 return super().form_valid(form)
166
167
168class PasswordSignupView(CreateView):
169 form_class = PasswordSignupForm
170 success_url = "/"
171
172 def form_valid(self, form):
173 # # Log the user in and redirect
174 # auth_login(self.request, form.save())
175
176 return super().form_valid(form)