Python: Flask “400 Bad Request: The CSRF session token is missing.”
I am describing the condition under which exception “400 Bad Request: The CSRF session token is missing.” occurs in my application, and how I use the Flask application global exception handler to deal with this exception in a generic manner (or so I hope).
Python: Flask “400 Bad Request: The CSRF session token is missing.” |
To start off, I’d like to note that, based on what I’ve
gathered during my research, “400 Bad Request: The CSRF session token is missing.”
seems to happen under a variety of different conditions. In this post, I’m only
describing a specific instance related to my application – it might not be
relevant to similar problems you might have.
I’m using Flask-WTF, and I’ve CSRF Protection in place:
import flask
...
from flask_wtf.csrf import CSRFProtect
...
csrf = CSRFProtect()
...
def create_app(config=None):
"""Construct the core application."""
app = flask.Flask(__name__, instance_relative_config=True)
...
csrf.init_app(app)
...
return app
This exception occurs when I just stop at the login page, wait until the web session expires, and then log in.
My login form is pretty much a
Flask-WTF
recommended one, please note :
<form method="post" action="" id="loginForm">
...
<h1 class="h3 mb-3 fw-normal">Please login</h1>
<div class="form-floating">
<input type="email" class="form-control" id="email" name="email" placeholder="name@example.com" data-parsley-trigger="change" required>
<label for="email">Email address</label>
</div>
...
<div class="form-floating">
<input type="password" class="form-control" id="password" name="password" placeholder="Password" data-parsley-trigger="change" required>
<label for="password">Password</label>
</div>
...
<button class="w-100 btn btn-lg btn-primary" type="submit">Login</button>
</form>
A log in when the web session is still valid sees the following
form data submitted: csrf_token
email
and
password
, as per the screen capture below:
Flask-WTF’s
def validate_csrf(data, secret_key=None, time_limit=None, token_key=None)
in
module csrf.py
gets run before our code:
102
103
if field_name not in session:
raise ValidationError("The CSRF session token is missing.")
The value for field_name
is 'csrf_token'
, and the
session
instance is:
<SqlAlchemySession {'_permanent': True, 'csrf_token': '616c3b20d4b1760fb04a6988befd05102b7e6422', '_fresh': False}>
And so the exception will not be raised, validate_csrf(...)
exits
successfully. Have we noticed that the value for the submitted
csrf_token
is different to the value shows in server code?
There must be a conversion / translation takes place prior?
A log in when the web session has already been expired,
still has csrf_token
submitted.
But the session
instance shows:
<SqlAlchemySession {'_permanent': True}>
validate_csrf(...)
will raise
ValidationError("The CSRF session token is missing.")
.
Since this exception is outside of the application code, it
is handled by the application global exception handler
def handle_error(e)
method defined in the
application module, app.py
:
"""
App entry point.
"""
...
from bh_apistatus.result_status import make_500_status
...
from XXX import create_app
...
app = create_app()
@app.errorhandler(Exception)
def handle_error(e):
"""
Global error handler for uncaught exceptions.
Exceptions raised by @requires_access_role( ... ) will go to this handler.
"""
return make_500_status(str(e)).as_dict()
make_500_status(...)
is a method from the
bh-apistatus
library which I’ve written to manage data which would be returned
as JSON
. Browsers will then just display the exception
as JSON
– it makes absolutely no sense to the users.
def handle_error(e)
is to catch exceptions which the
application code have not a chance to handle.
Now that I understand the condition under which
“400 Bad Request: The CSRF session token is missing.”
would occur, I’d like to redirect users to the login page again with a
friendly message. Furthermore, I’d like to be able to configure how
def handle_error(e)
handles exceptions in a generic manner,
I’d attempt to postulate that there are only a handful of exception which
goes through this code path.
I came up with the following JSON
configuration,
which stores in a file named exception_config.json
, under
the application
Instance Folders:
[
{
"typeName": "CSRFError",
"code": 400,
"name": "Bad Request",
"description": "The CSRF session token is missing.",
"action": {
"name": "redirect",
"data1": "auths.login",
"data2": "Oops... Too much idle time has passed... Please try again."
}
}
]
We’ve seen that the exception being raised is ValidationError
,
but its type(e).__name__
is actually 'CSRFError'
,
and it has three (3) attributes: code
, name
and
description
: when all four (4) pieces of information match,
I’ll take the action
, otherwise default to make_500_status(...)
.
Accordingly def handle_error(e)
gets updated as follows:
@app.errorhandler(Exception)
def handle_error(e):
"""Global error handler for uncaught exceptions.
Exceptions raised by @requires_access_role( ... ), \Lib\site-packages\flask_wtf\csrf.py's
def validate_csrf(...) etc. will go to this handler.
"""
import os
import simplejson as json
filename = os.path.join(app.instance_path, '', 'exception_config.json')
with open(filename) as file:
json_str = file.read()
file.close()
exception_config = json.loads(json_str)
found = False
action = None
for itm in exception_config:
if (itm['typeName'] != type(e).__name__):
continue
if (itm['code'] == e.code and itm['name'] == e.name and
itm['description'] == e.description):
action = itm['action']
found = True
break
if found:
if (action['name'] == 'redirect'):
flash(action['data2'], 'danger')
return redirect(url_for(action['data1']))
return make_500_status(str(e)).as_dict()
It should be self-explanatory. I don’t yet know what other exceptions might come through this code path, I’ll leave it at this implementation for the time being. I’m sure this would not be its final shape.
With this update in place, the response is much friendlier:
I hope this post is useful. Thank you for reading and stay safe as always.
✿✿✿
Feature image sources: