Search code examples
pythonflaskcookiesjwtflask-jwt-extended

Flask JWT Extended missing csrf access token


My flask application uses JWT as means of authentication. Those tokens are stored in cookies and flask-jwt-extended is configured to use them. For regular GET requests authentication works fine and @jwt_required decorator is able to read tokens from cookies and authenticate the user. But when making AJAX POST request using fetch() the extension is not able to read them and returns Missing CSRF token error. Strangely when accessing request object in the POST route all required cookies are present as well as in all other routes when authenticated, which means that fetch() sets all required cookies correctly:

ImmutableMultiDict([
('csrftoken', 'valid_csrf_token'),
('session','valid_session_cookie'), 
('access_token_cookie', 'valid_access_token'), 
('csrf_access_token', 'valid_csrf_access_token')
])

Flask POST route:

@main.route("/sendmail", methods=["POST"])
@jwt_required()
async def send_mail():
    data = json.loads(request.data)
    
    mail_template = render_template("mail-view.html", data=data)

    pdf_report = pdfkit.from_string(mail_template, False)

    message = Message(
        subject="Flask-Mailing module",
        recipients=["[email protected]"],
        body="Message body",
        subtype="html",
    )
    message.attach("report.pdf", pdf_report)
    await mail.send_message(message)
    return jsonify({"message": "success"}), 200

Fetch request:

fetch(window.location.origin + "/sendmail", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    credentials: "same-origin",
    body: JSON.stringify(mail),
})

My app config object:

class DevConfig:
    SECRET_KEY = os.environ.get("SECRET_KEY")
    JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
    JWT_COOKIE_SECURE = False
    SESSION_COOKIE_SECURE = False
    JWT_TOKEN_LOCATION = ["cookies"]
    JWT_ACCESS_TOKEN_EXPIRES = datetime.timedelta(hours=1)

    MAIL_SERVER = "smtp.googlemail.com"
    MAIL_PORT = 587
    MAIL_USE_TLS = True
    MAIL_USE_SSL = False
    MAIL_USERNAME = os.environ.get("MAIL_USERNAME")
    MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")
    MAIL_FROM = os.environ.get("MAIL_USERNAME")

Solution

  • With flask-jwt-extended default settings the CSRF-Token needs to be send only for state changing request methods (everything except 'GET'). Thats why the 'GET'-method is authorized even without a CSRF-Token. Secondly, the whole idea of a CSRF-Token is to not send it automatically by the web browser, so it is not accepted within a cookie:

    By default, we accomplish this by setting two cookies when someone logging in. The first cookie contains the JWT, and encoded in that JWT is the double submit token. This cookie is set as http-only, so that it cannot be access via javascript (this is what prevents XSS attacks from being able to steal the JWT). The second cookie we set contains only the same double submit token, but this time in a cookie that is readable by javascript. Whenever a request is made, it needs to include an X-CSRF-TOKEN header, with the value of the double submit token. If the value in this header does not match the value stored in the JWT, the request is kicked out as invalid.

    https://flask-jwt-extended.readthedocs.io/en/stable/token_locations/

    So whenever you sent a request you should add the CSRF-Token to the headers:

    fetch(window.location.origin + "/sendmail", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-CSRF-TOKEN": getCookie("csrf_access_token"),
        },
        credentials: "same-origin",
        body: JSON.stringify(mail),
    })