skip to Main Content

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:

  1. Am Using WTF FlaskForm
  2. Am using WTF CSRFProtect
  3. I DO have a SECRET_KEY set (I have tried the default and specifically for WTF)
  4. I am NOT excluding any views from CSRF
  5. I am using Flask-Login Login Manager
  6. Neither FireFox or Chome are blocking the “session” cookie and I can verify it is there in both browsers
  7. running on localhost:5000 and I also tried a specific domain like local.flask:5000
  8. 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


  1. {{ form.hidden_tag() }} should expand into something like

    <input id="csrf_token" name="csrf_token" type="hidden" value="... long string ...">
    

    If 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 the WTF_ 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.)

    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
  3. 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.

    gunicorn –workers 1 –threads 3 -b 0.0.0.0:5000 wsgi:app

    Login or Signup to reply.
  4. 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)

    app.config[‘SESSION_COOKIE_SECURE’] = False

    Login or Signup to reply.
  5. 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:

    from flask import g, session
    from app import app
    
    @app.before_request
    def fix_missing_csrf_token():
        if app.config['WTF_CSRF_FIELD_NAME'] not in session:
            if app.config['WTF_CSRF_FIELD_NAME'] in g:
                g.pop(app.config['WTF_CSRF_FIELD_NAME'])
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search