Skip to content

Commit e4fa67f

Browse files
authored
fix: invalidate password reset tokens (baserow#5164)
* fix: invalidate password reset tokens once used * fix: address feedback
1 parent a4887db commit e4fa67f

15 files changed

Lines changed: 459 additions & 44 deletions

File tree

backend/src/baserow/api/errors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
# These are not passwords
2121
BAD_TOKEN_SIGNATURE = "BAD_TOKEN_SIGNATURE" # nosec
2222
EXPIRED_TOKEN_SIGNATURE = "EXPIRED_TOKEN_SIGNATURE" # nosec
23+
ERROR_RESET_PASSWORD_TOKEN_USED = (
24+
"ERROR_RESET_PASSWORD_TOKEN_USED",
25+
HTTP_400_BAD_REQUEST,
26+
"The password reset link has already been used.",
27+
)
2328
ERROR_HOSTNAME_IS_NOT_ALLOWED = (
2429
"ERROR_HOSTNAME_IS_NOT_ALLOWED",
2530
HTTP_400_BAD_REQUEST,

backend/src/baserow/api/user/views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from baserow.api.errors import (
3131
BAD_TOKEN_SIGNATURE,
3232
ERROR_HOSTNAME_IS_NOT_ALLOWED,
33+
ERROR_RESET_PASSWORD_TOKEN_USED,
3334
EXPIRED_TOKEN_SIGNATURE,
3435
)
3536
from baserow.api.schemas import get_error_schema
@@ -82,6 +83,7 @@
8283
InvalidVerificationToken,
8384
RefreshTokenAlreadyBlacklisted,
8485
ResetPasswordDisabledError,
86+
ResetPasswordTokenAlreadyUsed,
8587
UserAlreadyExist,
8688
UserIsLastAdmin,
8789
UserNotFound,
@@ -434,6 +436,7 @@ class ResetPasswordView(APIView):
434436
"BAD_TOKEN_SIGNATURE",
435437
"EXPIRED_TOKEN_SIGNATURE",
436438
"ERROR_USER_NOT_FOUND",
439+
"ERROR_RESET_PASSWORD_TOKEN_USED",
437440
"ERROR_REQUEST_BODY_VALIDATION",
438441
]
439442
),
@@ -448,6 +451,7 @@ class ResetPasswordView(APIView):
448451
SignatureExpired: EXPIRED_TOKEN_SIGNATURE,
449452
UserNotFound: ERROR_USER_NOT_FOUND,
450453
ResetPasswordDisabledError: ERROR_DISABLED_RESET_PASSWORD,
454+
ResetPasswordTokenAlreadyUsed: ERROR_RESET_PASSWORD_TOKEN_USED,
451455
}
452456
)
453457
@validate_body(ResetPasswordBodyValidationSerializer)

backend/src/baserow/config/settings/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ def __setitem__(self, key, value):
807807
EXTRA_PUBLIC_WEB_FRONTEND_HOSTNAMES.append(hostname)
808808

809809
FROM_EMAIL = os.getenv("FROM_EMAIL", "no-reply@localhost")
810-
RESET_PASSWORD_TOKEN_MAX_AGE = 60 * 60 * 48 # 48 hours
810+
RESET_PASSWORD_TOKEN_MAX_AGE = 60 * 60 * 2 # 2 hours
811811
CHANGE_EMAIL_TOKEN_MAX_AGE = 60 * 60 * 12 # 12 hours
812812

813813
ROW_PAGE_SIZE_LIMIT = int(os.getenv("BASEROW_ROW_PAGE_SIZE_LIMIT", 200))

backend/src/baserow/contrib/database/data_sync/postgresql_data_sync_type.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def _connection(self, instance):
159159
cursor = None
160160
connection = None
161161

162-
if not is_hostname_safe(instance.postgresql_host):
162+
if not settings.TESTS and not is_hostname_safe(instance.postgresql_host):
163163
raise SyncError("It's not allowed to connect to this hostname.")
164164

165165
baserow_postgresql_connection = (
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
{% load i18n %}
2+
<!doctype html>
3+
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
4+
5+
<head>
6+
<title></title>
7+
<!--[if !mso]><!-->
8+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
9+
<!--<![endif]-->
10+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
11+
<meta name="viewport" content="width=device-width, initial-scale=1">
12+
<style type="text/css">
13+
#outlook a {
14+
padding: 0;
15+
}
16+
17+
body {
18+
margin: 0;
19+
padding: 0;
20+
-webkit-text-size-adjust: 100%;
21+
-ms-text-size-adjust: 100%;
22+
}
23+
24+
table,
25+
td {
26+
border-collapse: collapse;
27+
mso-table-lspace: 0pt;
28+
mso-table-rspace: 0pt;
29+
}
30+
31+
img {
32+
border: 0;
33+
height: auto;
34+
line-height: 100%;
35+
outline: none;
36+
text-decoration: none;
37+
-ms-interpolation-mode: bicubic;
38+
}
39+
40+
p {
41+
display: block;
42+
margin: 13px 0;
43+
}
44+
</style>
45+
<!--[if mso]>
46+
<noscript>
47+
<xml>
48+
<o:OfficeDocumentSettings>
49+
<o:AllowPNG/>
50+
<o:PixelsPerInch>96</o:PixelsPerInch>
51+
</o:OfficeDocumentSettings>
52+
</xml>
53+
</noscript>
54+
<![endif]-->
55+
<!--[if lte mso 11]>
56+
<style type="text/css">
57+
.mj-outlook-group-fix { width:100% !important; }
58+
</style>
59+
<![endif]-->
60+
<!--[if !mso]><!-->
61+
<link href="https://fonts.googleapis.com/css?family=Inter:400,600" rel="stylesheet" type="text/css">
62+
<style type="text/css">
63+
@import url(https://fonts.googleapis.com/css?family=Inter:400,600);
64+
</style>
65+
<!--<![endif]-->
66+
<style type="text/css">
67+
@media only screen and (min-width:480px) {
68+
.mj-column-px-190 {
69+
width: 190px !important;
70+
max-width: 190px;
71+
}
72+
73+
.mj-column-per-50 {
74+
width: 50% !important;
75+
max-width: 50%;
76+
}
77+
78+
.mj-column-per-100 {
79+
width: 100% !important;
80+
max-width: 100%;
81+
}
82+
}
83+
</style>
84+
<style media="screen and (min-width:480px)">
85+
.moz-text-html .mj-column-px-190 {
86+
width: 190px !important;
87+
max-width: 190px;
88+
}
89+
90+
.moz-text-html .mj-column-per-50 {
91+
width: 50% !important;
92+
max-width: 50%;
93+
}
94+
95+
.moz-text-html .mj-column-per-100 {
96+
width: 100% !important;
97+
max-width: 100%;
98+
}
99+
</style>
100+
<style type="text/css">
101+
@media only screen and (max-width:479px) {
102+
table.mj-full-width-mobile {
103+
width: 100% !important;
104+
}
105+
106+
td.mj-full-width-mobile {
107+
width: auto !important;
108+
}
109+
}
110+
</style>
111+
</head>
112+
113+
<body style="word-spacing:normal;">
114+
<div aria-roledescription="email" style="" role="article" lang="und" dir="auto">
115+
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
116+
<div style="margin:0px auto;max-width:600px;">
117+
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
118+
<tbody>
119+
<tr>
120+
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:left;">
121+
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:190px;" ><![endif]-->
122+
<div class="mj-column-px-190 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
123+
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
124+
<tbody>
125+
<tr>
126+
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;">
127+
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
128+
<tbody>
129+
<tr>
130+
<td style="width:140px;">
131+
<a href="{{ baserow_embedded_share_url }}" target="_blank">
132+
<img alt="" src="{{ logo_url }}" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="140" height="auto" />
133+
</a>
134+
</td>
135+
</tr>
136+
</tbody>
137+
</table>
138+
</td>
139+
</tr>
140+
</tbody>
141+
</table>
142+
</div>
143+
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
144+
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
145+
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
146+
<tbody>
147+
<tr>
148+
<td align="left" style="font-size:0px;padding:10px 25px;padding-left:0;word-break:break-word;">
149+
<div style="font-family:Inter,sans-serif;font-size:13px;line-height:170%;text-align:left;color:#070810;">{{ logo_additional_text }}</div>
150+
</td>
151+
</tr>
152+
</tbody>
153+
</table>
154+
</div>
155+
<!--[if mso | IE]></td></tr></table><![endif]-->
156+
</td>
157+
</tr>
158+
</tbody>
159+
</table>
160+
</div>
161+
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
162+
<div style="margin:0px auto;max-width:600px;">
163+
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
164+
<tbody>
165+
<tr>
166+
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
167+
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
168+
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
169+
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
170+
<tbody>
171+
<tr>
172+
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
173+
<div style="font-family:Inter,sans-serif;font-size:22px;font-weight:600;line-height:1;text-align:left;color:#070810;">{% trans "Password changed" %}</div>
174+
</td>
175+
</tr>
176+
<tr>
177+
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
178+
<div style="font-family:Inter,sans-serif;font-size:13px;line-height:170%;text-align:left;color:#070810;">{% blocktrans trimmed with user.username as username and baserow_embedded_share_hostname as baserow_embedded_share_hostname %} The password for your account ({{ username }}) on Baserow ({{ baserow_embedded_share_hostname }}) has been successfully changed. {% endblocktrans %}</div>
179+
</td>
180+
</tr>
181+
<tr>
182+
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
183+
<div style="font-family:Inter,sans-serif;font-size:13px;line-height:170%;text-align:left;color:#070810;">{% blocktrans trimmed %} If you did not make this change, please contact your administrator immediately to secure your account. {% endblocktrans %}</div>
184+
</td>
185+
</tr>
186+
<!-- htmlmin:ignore -->{% if show_baserow_description %}<!-- htmlmin:ignore -->
187+
<tr>
188+
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
189+
<div style="font-family:Inter,sans-serif;font-size:13px;line-height:170%;text-align:left;color:#070810;">{% blocktrans trimmed %} Baserow is an open source no-code database tool which allows you to collaborate on projects, customers and more. It gives you the powers of a developer without leaving your browser. {% endblocktrans %}</div>
190+
</td>
191+
</tr>
192+
<!-- htmlmin:ignore -->{% endif %}<!-- htmlmin:ignore -->
193+
</tbody>
194+
</table>
195+
</div>
196+
<!--[if mso | IE]></td></tr></table><![endif]-->
197+
</td>
198+
</tr>
199+
</tbody>
200+
</table>
201+
</div>
202+
<!--[if mso | IE]></td></tr></table><![endif]-->
203+
</div>
204+
</body>
205+
206+
</html>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<% layout("../../base.layout.eta") %>
2+
3+
<mj-section>
4+
<mj-column>
5+
<mj-text mj-class="title">{% trans "Password changed" %}</mj-text>
6+
<mj-text mj-class="text">
7+
{% blocktrans trimmed with user.username as username and baserow_embedded_share_hostname as baserow_embedded_share_hostname %}
8+
The password for your account ({{ username }}) on
9+
Baserow ({{ baserow_embedded_share_hostname }}) has been successfully changed.
10+
{% endblocktrans %}
11+
</mj-text>
12+
<mj-text mj-class="text">
13+
{% blocktrans trimmed %}
14+
If you did not make this change, please contact your administrator immediately
15+
to secure your account.
16+
{% endblocktrans %}
17+
</mj-text>
18+
<mj-raw><!-- htmlmin:ignore -->{% if show_baserow_description %}<!-- htmlmin:ignore --></mj-raw>
19+
<mj-text mj-class="text">
20+
{% blocktrans trimmed %}
21+
Baserow is an open source no-code database tool which allows you to collaborate
22+
on projects, customers and more. It gives you the powers of a developer without
23+
leaving your browser.
24+
{% endblocktrans %}
25+
</mj-text>
26+
<mj-raw><!-- htmlmin:ignore -->{% endif %}<!-- htmlmin:ignore --></mj-raw>
27+
</mj-column>
28+
</mj-section>

backend/src/baserow/core/user/emails.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,22 @@ def get_context(self):
2525
return context
2626

2727

28+
class PasswordChangedEmail(BaseEmailMessage):
29+
template_name = "baserow/core/user/password_changed.html"
30+
31+
def __init__(self, user, *args, **kwargs):
32+
self.user = user
33+
super().__init__(*args, **kwargs)
34+
35+
def get_subject(self):
36+
return _("Password changed - Baserow")
37+
38+
def get_context(self):
39+
context = super().get_context()
40+
context.update(user=self.user)
41+
return context
42+
43+
2844
class AccountDeletionScheduled(BaseEmailMessage):
2945
template_name = "baserow/core/user/account_deletion_scheduled.html"
3046

backend/src/baserow/core/user/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ class ResetPasswordDisabledError(Exception):
3838
"""Raised when a password reset is attempted but the password reset is disabled."""
3939

4040

41+
class ResetPasswordTokenAlreadyUsed(Exception):
42+
"""Raised when a password reset token has already been used."""
43+
44+
4145
class DeactivatedUserException(Exception):
4246
pass
4347

0 commit comments

Comments
 (0)