6

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
BBALEY
  • 183
  • 2
  • 2
  • 7
  • 3
    Did you solved this problem? I have encountered it also right now. – Bruce Yang Jun 25 '19 at 06:30
  • 1
    Bruce - no, I gave up. the time using Flask was supposed to save me was lost on things like this – BBALEY Jun 26 '19 at 16:01
  • 1
    Where/how were you deploying the app? I'm having the same issue when I deploy on a free heroku deployment, but it only happens from time to time. I'm somewhat convinced the issue is that heroku spins up the application container on a different server/dyno from the one it was initiated on. When it does this, the CSRF token doesn't match the session. Again this is only my speculation but it seems to match the symptoms I was seeing. – Jesse Reich Apr 01 '20 at 18:14
  • i have the same problem , someone found solution? – user63898 May 11 '20 at 09:30
  • I abandoned flask before finding a solution - however the comment below by Brian DeRocher might be worth trying... – bbaley May 12 '20 at 15:35
  • This question might be related: https://stackoverflow.com/questions/61572178/flask-multiple-session-cookies-with-the-same-name – azmeuk Dec 29 '20 at 10:02

6 Answers6

6

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

Quang Le
  • 61
  • 1
  • 2
  • If you are not using Gunicorn, study your container/server carefully since it may be the cause. – Quang Le Oct 01 '20 at 03:54
  • Do you have any idea how it may be related? – azmeuk Dec 29 '20 at 09:17
  • 1
    I'm not sure but I think it's because the web server (Gunicorn) creates separated OS processes for corresponding FLASK instances. These instances are not synchronized or communicated each other as the FLASK doesn't support that(As I know). – Quang Le Jan 11 '21 at 03:44
  • Changing the number of workers from 3 to 1 worked for me. – scrollout Feb 12 '23 at 20:59
2

{{ 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.)

Dave W. Smith
  • 24,318
  • 4
  • 40
  • 46
  • Yes - BEFORE submitting the form and the POST, I see In the code you can see the options I set, which include WTF_CSRF_SECRET_KEY specifically which I tried even though it is supposed to use the default secret_key, and WTF_CSRF_ENABLED to True Also as mentioned I see the csrf_token value in the FORM DATA - but wasn't sure if that was correct – BBALEY Jan 03 '19 at 22:40
  • The only other thing that stands out is that `csrf.init_app(app)` happens before you change config settings. That seems dangerous. (I don't explicitly `init_app` `django_wtf.csrf` in my apps, since it'll happen lazily.) – Dave W. Smith Jan 03 '19 at 22:52
  • At what point would you csrf.init_app considering my code above ? It's very hard to find explicit docs/info regarding when to do what in what order... – BBALEY Jan 04 '19 at 03:22
  • None of my flask apps that use `flask_wtf` initialize csrf explicitly. – Dave W. Smith Jan 04 '19 at 03:24
  • I tried moving the csrf.init_app(app) to every order I could think of with same result. When I removed it - I now get a different error "{'csrf_token': ['The CSRF tokens do not match.']} " – BBALEY Jan 04 '19 at 03:25
  • I was trying those settings based on various posts to try and figure it out – BBALEY Jan 04 '19 at 03:25
  • Try removing it. – Dave W. Smith Jan 04 '19 at 03:25
  • Alternatively, try, in a separate app, to use flask_wtf in the simplest possible way (e.g., the "Flask Mega Tutorial"), then proceed in the direction you suspect to be correct until it breaks. – Dave W. Smith Jan 04 '19 at 03:27
  • I did as mentioned - I now get "{'csrf_token': ['The CSRF tokens do not match.']} " when I flash(form.errors) – BBALEY Jan 04 '19 at 03:28
  • it is coming from vailidate_csrf() in csrf.py. if not safe_str_cmp(session[field_name], token): raise ValidationError('The CSRF tokens do not match.'). but I do not know what that is referring to with regard to settings or values – BBALEY Jan 04 '19 at 03:30
  • In your position, I'd try to work forward from a small, working example. – Dave W. Smith Jan 04 '19 at 03:37
  • @user1289330 did you find fix to your problem? i facing this exact problem The CSRF tokens do not match. – user63898 May 11 '20 at 09:28
2

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'])
1

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.

0

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

pksec
  • 63
  • 7
0

I was getting this error while trying to deploy a Flask application on Heroku, built on Mac with PyCharm and remote repository in Github.What I understood by going through various posts is that for Flask forms csrf protection is inbuilt . We don't have to initialise additional function. I was using Wtf quick_forms and still I was getting this error. I also understood that csrf key is same as FLASK key. I had set up flask key as environment variable in PyCharm and Heroku was not detecting it. When I disclosed the key in the main app this error disappeared.

  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Aug 16 '23 at 07:42