Skip to content

Commit 63718b2

Browse files
committed
Use WTF CSRF protection for login form instead of JWT CSRF protection
JWT CSRF protection does nothing if no JWT is passed in the request (i.e. if user is not logged in)
1 parent ba77b6a commit 63718b2

File tree

9 files changed

+21
-50
lines changed

9 files changed

+21
-50
lines changed

src/db_auth.py

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,6 @@ def tenant_base(self):
128128
prefix = self.app.session_interface.get_cookie_path(self.app)
129129
return prefix.rstrip('/') + '/'
130130

131-
def csrf_token(self):
132-
""" Inject CSRF token """
133-
token = (get_jwt() or {}).get("csrf")
134-
if token:
135-
return token
136-
else:
137-
return ""
138-
139131
def login(self):
140132
"""Authorize user and sign in."""
141133
target_url = url_path(request.args.get('url') or self.tenant_base())
@@ -241,8 +233,7 @@ def login(self):
241233

242234
return render_template('login.html', form=form, i18n=i18n,
243235
title=i18n.t("auth.login_page_title"),
244-
login_hint=self.login_hint,
245-
csrf_token=self.csrf_token())
236+
login_hint=self.login_hint)
246237

247238
def verify_login(self):
248239
"""Verify user login (e.g. from basic auth header)."""
@@ -314,8 +305,7 @@ def __verify(self, db_session, submit=True):
314305
return redirect(url_for('login'))
315306

316307
return render_template('verify.html', form=form, i18n=i18n,
317-
title=i18n.t("auth.verify_page_title"),
318-
csrf_token=self.csrf_token())
308+
title=i18n.t("auth.verify_page_title"))
319309

320310
def logout(self, identity):
321311
"""Sign out."""
@@ -393,8 +383,7 @@ def __setup_totp(self, db_session, submit=True):
393383
resp = make_response(render_template(
394384
'qrcode.html', form=form, i18n=i18n,
395385
title=i18n.t("auth.qrcode_page_title"),
396-
totp_secret=totp_secret,
397-
csrf_token=self.csrf_token()
386+
totp_secret=totp_secret
398387
))
399388
# do not cache in browser
400389
resp.headers.set(
@@ -488,8 +477,7 @@ def new_password(self):
488477
flash(i18n.t("auth.reset_mail_failed"))
489478
return render_template(
490479
'new_password.html', form=form, i18n=i18n,
491-
title=i18n.t("auth.new_password_page_title"),
492-
csrf_token=self.csrf_token()
480+
title=i18n.t("auth.new_password_page_title")
493481
)
494482
else:
495483
self.logger.info("User lookup failed")
@@ -500,8 +488,7 @@ def new_password(self):
500488

501489
return render_template(
502490
'new_password.html', form=form, i18n=i18n,
503-
title=i18n.t("auth.new_password_page_title"),
504-
csrf_token=self.csrf_token()
491+
title=i18n.t("auth.new_password_page_title")
505492
)
506493

507494
def edit_password(self, token, identity=None):
@@ -535,8 +522,7 @@ def edit_password(self, token, identity=None):
535522
else:
536523
return render_template(
537524
'edit_password.html', form=form, i18n=i18n,
538-
title=i18n.t("auth.edit_password_page_title"),
539-
csrf_token=self.csrf_token()
525+
title=i18n.t("auth.edit_password_page_title")
540526
)
541527

542528
if not self.password_accepted(
@@ -555,8 +541,7 @@ def edit_password(self, token, identity=None):
555541

556542
return render_template(
557543
'edit_password.html', form=form, i18n=i18n,
558-
title=i18n.t("auth.edit_password_page_title"),
559-
csrf_token=self.csrf_token()
544+
title=i18n.t("auth.edit_password_page_title")
560545
)
561546

562547
# save new password
@@ -581,16 +566,14 @@ def edit_password(self, token, identity=None):
581566
else:
582567
return render_template(
583568
'edit_password.html', form=form, i18n=i18n,
584-
title=i18n.t("auth.edit_password_page_title"),
585-
csrf_token=self.csrf_token()
569+
title=i18n.t("auth.edit_password_page_title")
586570
)
587571
else:
588572
# invalid reset token
589573
flash(i18n.t("auth.edit_password_invalid_token"))
590574
return render_template(
591575
'edit_password.html', form=form, i18n=i18n,
592-
title=i18n.t("auth.edit_password_page_title"),
593-
csrf_token=self.csrf_token()
576+
title=i18n.t("auth.edit_password_page_title")
594577
)
595578

596579
if token:
@@ -599,8 +582,7 @@ def edit_password(self, token, identity=None):
599582

600583
return render_template(
601584
'edit_password.html', form=form, i18n=i18n,
602-
title=i18n.t("auth.edit_password_page_title"),
603-
csrf_token=self.csrf_token()
585+
title=i18n.t("auth.edit_password_page_title")
604586
)
605587

606588
def require_password_change(self, user, reason, target_url):
@@ -633,8 +615,7 @@ def require_password_change(self, user, reason, target_url):
633615
flash(i18n.t('auth.edit_password_message'))
634616
return render_template(
635617
'edit_password.html', form=form, i18n=i18n,
636-
title=i18n.t("auth.edit_password_page_title"),
637-
csrf_token=self.csrf_token()
618+
title=i18n.t("auth.edit_password_page_title")
638619
)
639620

640621
def edit_password_form(self):
@@ -819,13 +800,11 @@ def __login_response(self, user, target_url):
819800
'notification.html', form=form, i18n=i18n,
820801
title=i18n.t("auth.notification_page_title"),
821802
message=i18n.t("auth.notification_expiry_notice", days=days),
822-
target_url=target_url,
823-
csrf_token=self.csrf_token()
803+
target_url=target_url
824804
)
825805
resp = make_response(page)
826806

827-
# Set the JWTs and the CSRF double submit protection cookies
828-
# in this response
807+
# Set the JWTs in this response
829808
set_access_cookies(resp, access_token)
830809

831810
return resp
@@ -869,14 +848,12 @@ def send_reset_passwort_instructions(self, user):
869848
msg.body = render_template(
870849
'reset_password_instructions.%s.txt' % i18n.get('locale'),
871850
user=user, reset_url=reset_url,
872-
unlock_url=unlock_url,
873-
csrf_token=self.csrf_token()
851+
unlock_url=unlock_url
874852
)
875853
except:
876854
msg.body = render_template(
877855
'reset_password_instructions.en.txt',
878-
user=user, reset_url=reset_url,
879-
csrf_token=self.csrf_token()
856+
user=user, reset_url=reset_url
880857
)
881858

882859
# send message

src/server.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
app.config['SESSION_COOKIE_SECURE'] = app.config['JWT_COOKIE_SECURE']
2727
app.config['SESSION_COOKIE_SAMESITE'] = app.config['JWT_COOKIE_SAMESITE']
2828

29+
# For login CSRF protection, use WTF CSRF protection (as no JWT is available before login)
30+
os.environ['JWT_COOKIE_CSRF_PROTECT'] = 'False'
31+
app.config['WTF_CSRF_ENABLED'] = True
32+
2933
jwt = auth_manager(app)
3034
app.secret_key = app.config['JWT_SECRET_KEY']
3135

@@ -35,9 +39,7 @@
3539
# *Enable* WTForms built-in messages translation
3640
# https://wtforms.readthedocs.io/en/2.3.x/i18n/
3741
app.config['WTF_I18N_ENABLED'] = False
38-
# WTF CSRF protection conflicts with JWT CSRF protection
39-
# https://github.com/vimalloc/flask-jwt-extended/issues/15
40-
app.config['WTF_CSRF_ENABLED'] = False
42+
4143

4244
# https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-v-user-logins
4345
login = LoginManager(app)
@@ -89,7 +91,6 @@ def load_user(id):
8991

9092

9193
@app.route('/login', methods=['GET', 'POST'])
92-
@optional_auth
9394
def login():
9495
return db_auth_handler().login()
9596

src/templates/base.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
{% if form.background %}
1818
<div class="form-background" style="background-image: url('{{form.background}}');">
1919
{% endif %}
20-
{% set form_errors = form.errors.get('csrf_token', []) if form else [] %}
21-
{% with messages = get_flashed_messages() + form_errors %}
20+
{% with messages = get_flashed_messages() %}
2221
{% if messages %}
2322
<ul>
2423
{% for message in messages %}

src/templates/edit_password.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
<h1>{{ i18n.t('auth.edit_password_page_title') }}</h1>
55
<form action="{{ url_for('edit_password') }}" method="post">
66
{{ form.hidden_tag() }}
7-
<input id="csrf_token" name="csrf_token" type="hidden" value="{{ csrf_token }}">
87
<div class="login edit-password">
98
<div class="login-screen">
109
<div class="app-title">

src/templates/login.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
{% block content %}
44
<form action="" method="post" autocomplete="off">
55
{{ form.hidden_tag() }}
6-
<input id="csrf_token" name="csrf_token" type="hidden" value="{{ csrf_token }}">
76
<div class="login">
87
<div class="login-screen">
98
<div class="app-title">

src/templates/new_password.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
<h1>{{ i18n.t('auth.new_password_page_title') }}</h1>
55
<form action="" method="post">
66
{{ form.hidden_tag() }}
7-
<input id="csrf_token" name="csrf_token" type="hidden" value="{{ csrf_token }}">
87
<div class="login new-password">
98
<div class="login-screen">
109
<div class="app-title">

src/templates/notification.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
{% block content %}
44
<h1>{{ i18n.t('auth.notification_page_title') }}</h1>
55
<form action="#" method="get">
6-
<input id="csrf_token" name="csrf_token" type="hidden" value="{{ csrf_token }}">
76
<div class="login notification">
87
<div class="login-screen">
98
<div class="app-title">

src/templates/qrcode.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
<h1>{{ i18n.t('auth.qrcode_page_title') }}</h1>
55
<form action="{{ url_for('setup_totp') }}" method="post">
66
{{ form.hidden_tag() }}
7-
<input id="csrf_token" name="csrf_token" type="hidden" value="{{ csrf_token }}">
87
<div class="login totp-setup">
98
<div class="login-screen">
109
<div class="app-title">

src/templates/verify.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
<h1>{{ i18n.t('auth.verify_page_title') }}</h1>
55
<form action="{{ url_for('verify') }}" method="post">
66
{{ form.hidden_tag() }}
7-
<input id="csrf_token" name="csrf_token" type="hidden" value="{{ csrf_token }}">
87
<div class="login verify">
98
<div class="login-screen">
109
<div class="app-title">

0 commit comments

Comments
 (0)