I have researched every post I can find regarding “CSRF session token missing” in a Flask WTF app, but so far I cannot find the solution in any that have a solution or I am missing it and not seeing it.
In this case I am creating a login page, and the error is generated on POST/submit of the login form.
In Browser Dev tools I can see “csrf_token” in the Form Data but no token in the headers.
The form data is coming from;
<form method="POST" action="">
{{ form.hidden_tag() }}
{{ form.csrf_token() }}
In the login.html, but I don’t know if this is the expected result – it does not seem to be working.
I was thinking I should see a X-CSRFToken In the request Headers ? But I do NOT.
Here is what I “think” I am doing correctly based on what I have researched and read on the topic for this error and configuration:
- Am Using WTF FlaskForm
- Am using WTF CSRFProtect
- I DO have a SECRET_KEY set (I have tried the default and specifically for WTF)
- I am NOT excluding any views from CSRF
- I am using Flask-Login Login Manager
- Neither FireFox or Chome are blocking the “session” cookie and I can verify it is there in both browsers
- running on localhost:5000 and I also tried a specific domain like local.flask:5000
- I am only storing small strings (user_id) in the session
Should it be a different cookie ? (e.g. named “csrf_token” not the “session” named cookie ?)
While debugging in the WTF csrf.py
in the validate_csrf() function, I find;
secret_key = _get_config(
secret_key, 'WTF_CSRF_SECRET_KEY', current_app.secret_key,
message='A secret key is required to use CSRF.'
)
returns the expected secret value:
secret_key = {bytes} b'abc123ced456'
field_name = _get_config(
token_key, 'WTF_CSRF_FIELD_NAME', 'csrf_token',
message='A field name is required to use CSRF.'
)
returns
field_name = {str} ‘csrf_token’
and _data seems ok:
data = {str} 'IjZiNWY5ZDdiNTZjMTVkM2U0Mzg3MjU1NGMxYzc3Yjg1MTMzYTlhYzEi.XC447w.cmc1INq6u8qVuq0EOL9ARcPwB6k'
However it fails because “field_name” is not IN session
if field_name not in session:
raise ValidationError('The CSRF session token is missing.')
So the question is WHY ?
I also get an error checking for the key/value from the login form method;
@app.route("/login", methods=['GET', 'POST'])
def login():
test = session['secret_key']
KeyError: ‘secret_key’
How does the app.secret_key get to the session ‘secret_key’ ?
This appears to NOT be happening.
app.py
from flask import Flask, render_template, url_for, flash, redirect, Response, jsonify, abort, session
from flask_session import Session
from flask_wtf.csrf import CSRFProtect
from flask_cors import CORS
from flask_login import LoginManager,UserMixin,current_user,login_required,login_user,logout_user
from forms import RegistrationForm, LoginForm, TimecardForm
from employees import employees
csrf = CSRFProtect()
app = Flask(__name__)
csrf.init_app(app)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') or
'abc123ced456'
app.config['SESSION_TYPE'] = 'memcached'
app.config['WTF_CSRF_ENABLED'] = True
app.config['WTF_CSRF_SECRET_KEY'] = os.getenv('SECRET_KEY') or
'abc123ced456'
app.config['SESSION_COOKIE_SECURE'] = True
app.config['REMEMBER_COOKIE_SECURE'] = True
CORS(app)
sess = Session()
sess.init_app(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.session_protection = "strong"
login_manager.login_view = 'login'
@login_manager.user_loader
def load_user(userid):
result = None
emp_collection = employees.oEmployeeCollection()
emp_collection.getAllEmployees(None, None)
result = emp_collection.getEmployee(userid)
return result
@app.route("/login", methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
emp_collection = employees.oEmployeeCollection()
emp_collection.getAllEmployees(None, None)
current_user = emp_collection.getEmployee(form.user_init.data.upper())
if current_user is not None:
if current_user.password == form.password.data:
login_user(current_user, remember=True)
sess['current_user'] = current_user.toJSON()
flash('You have been logged in!', 'success')
#next = flask.request.args.get('next')
## is_safe_url should check if the url is safe for redirects.
#if not is_safe_url(next):
# return flask.abort(400)
#return flask.redirect(next or flask.url_for('index'))
return redirect(url_for('home'))
else:
flash('Login Unsuccessful. Please check username and password', 'danger')
else:
flash('Login Unsuccessful. Please check username and password', 'danger')
flash(form.errors)
return render_template('login.html', title='Login', form=form)
@app.before_first_request
def execute_this():
# emp_collection.getAllEmployees(None, None)
test = None
if __name__ == '__main__':
app.run(host='flask.local', port=5000, debug=False)
login.html
{% extends "template.html" %}
{% block content %}
<div class="content-section">
<form method="POST" action="">
{{ form.hidden_tag() }}
{{ form.csrf_token() }}
<fieldset class="form-group">
<legend class="border-bottom mb-4">Log In</legend>
<div class="form-group">
{{ form.user_init.label(class="form-control-label")}}
{% if form.user_init.errors %}
{{ form.user_init(class="form-control form-control-lg is-invalid") }}
<div class="invalid-feedback">
{% for error in form.user_init.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.user_init(class="form-control form-control-lg") }}
{% endif %}
</div>
<div class="form-group">
{{ form.password.label(class="form-control-label") }}
{% if form.password.errors %}
{{ form.password(class="form-control form-control-lg is-invalid") }}
<div class="invalid-feedback">
{% for error in form.password.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.password(class="form-control form-control-lg") }}
{% endif %}
</div>
<div class="form-check">
{{ form.remember(class="form-check-input") }}
{{ form.remember.label(class="form-check-label") }}
</div>
</fieldset>
<div class="form-group">
{{ form.submit(class="btn btn-outline-info") }}
</div>
<small class="text-muted ml-2">
<a href="#">Forgot Password?</a>
</small>
</form>
</div>
<div class="border-top pt-3">
<small class="text-muted">
Need An Account? <a class="ml-2" href="{{ url_for('register') }}">Sign Up Now</a>
</small>
</div>
{% endblock content %}
Forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField, DateField, DecimalField
from wtforms.validators import DataRequired, Length, Email, EqualTo
class LoginForm(FlaskForm):
user_init = StringField('User', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember = BooleanField('Remember Me')
submit = SubmitField('Login')
Request results
Response
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>The CSRF session token is missing.</p>
Session cookie
Content-Type →text/html
Content-Length →142
Access-Control-Allow-Origin →*
Set-Cookie →session=ad0a88f2-4048-4a3b-9934-c2cd5957e9ff; Expires=Sun, 03-Feb-2019 14:55:27 GMT; HttpOnly; Path=/
Server →Werkzeug/0.14.1 Python/3.7.1
Date →Thu, 03 Jan 2019 14:55:27 GMT
Request General
Request URL: http://localhost:5000/login
Request Method: POST
Status Code: 400 BAD REQUEST
Remote Address: 127.0.0.1:5000
Referrer Policy: no-referrer-when-downgrade
Response Headers
Access-Control-Allow-Origin: http://localhost:5000
Content-Length: 150
Content-Type: text/html
Date: Thu, 03 Jan 2019 14:47:18 GMT
Server: Werkzeug/0.14.1 Python/3.7.1
Set-Cookie: session=62e6139c-332b-4811-ad3a-de5c29c878aa; Expires=Sun, 03-Feb-2019 14:47:18 GMT; HttpOnly; Path=/
Vary: Origin
Request Headers
POST /login HTTP/1.1
Host: localhost:5000
Connection: keep-alive
Content-Length: 258
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Origin: http://localhost:5000
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://localhost:5000/login
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: Webstorm-655f3561=d5da8892-b9fc-4680-8fe8-17baf5fd6f8d;session=62e6139c-332b-4811-ad3a-de5c29c878aa
Form Data
csrf_token=ImI5ZDlkYjZmNjkxMDZlZDczZjdlY2VjMTM2NTQzOWZlMDBkYTY1ZWMi.XC4gZQ.DVyKZ07nrQN6WZn0jmoHyKrf_YI&
csrf_token=ImI5ZDlkYjZmNjkxMDZlZDczZjdlY2VjMTM2NTQzOWZlMDBkYTY1ZWMi.XC4gZQ.DVyKZ07nrQN6WZn0jmoHyKrf_YI&user_init=ABC&password=changeme&remember=y&submit=Login
5
Answers
{{ form.hidden_tag() }}
should expand into something likeIf you’re not seeing that, double-check how you’ve set up the configuration parts of your app. Aside from
SECRET_KEY
, are you setting any of theWTF_
options?You’ll probably want to remove
{{ form.csrf_token() }}
No
X-
headers are involved. (I did a quick check on one of my apps, in case I’d forgotten something.)See if the “secure” attribute of the cookie is being set. If that’s true, and you’re calling a non-secure website, the cookie will not be sent. I’ve seen this be the cause of the CSRF token missing issue.
I met "The CSRF token is missing" issue yesterday and fortunately, I’ve found the cause for my case. I’ve deployed my Flask app on Gunicorn + Nginx using sync workers configuration following by this instruction and that’s the problem. Flask is NOT working with Gunicorn’s sync workers, so moving to threads has resolved my issue.
The answer from @brian worked for me. The problem was I was in a test environment with localhost setup and without HTTPS serving.
See more: https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_SECURE
Setting the following config to False enables the session cookies to load in a non-production (test environment)
I’ll just leave it here.
I faced with similar problem with actual packages versions (Flask==2.0.1, Flask-WTF==0.15.1, Flask-Login==0.5.0).
Analyzing the code of the Flask-WTF, I noticed that the csrf token cookie will not be set in the session if it already in globals (flask.g variable). It may be also when session cookie cleared or another reasons.
The solution is remove csrf token key from g if it absent in session variable in before_request section: