How to login to Flask App when using Locust

蹲街弑〆低调 提交于 2019-12-24 23:16:20

问题


First time using Locust. I have a Flask App that requires user to login to access most routes. I cant get Locust to successfully login to my Flask App.

Here is my Locust.py file:

from locust import HttpLocust, TaskSet, task
import re


class UserBehavior(TaskSet):
    def on_start(self):
        """ on_start is called when a Locust start before any task is scheduled """
        self.client.verify = False
        self.get_token()
        self.login()

    def on_stop(self):
        """ on_stop is called when the TaskSet is stopping """
        self.logout()

    def get_token(self):
        response = self.client.get("/login")
        # Sample string from response:
        # <input id="csrf_token" name="csrf_token" type="hidden" value="REDACTED">
        self.csrftoken = re.search(' name="csrf_token" .* value="(.+?)"', response.text).group(1)
        print(f"DEBUG: self.csrftoken = {self.csrftoken}")

    def login(self):
        response = self.client.post("/login",
                                    {"email": "REDACTED", "password": "REDACTED"},
                                    headers={"X-CSRFToken": self.csrftoken})
        print(f"DEBUG: login response.status_code = {response.status_code}")

    def logout(self):
        self.client.get("/logout")

    @task(5)
    def list_domains(self):
        response = self.client.get("/domains", headers={"X-CSRFToken": self.csrftoken})
        print(f"DEBUG list: response.status_code = {response.status_code}")


class WebsiteUser(HttpLocust):
    task_set = UserBehavior
    min_wait = 5000
    max_wait = 9000

Here is the login function of my Flask App: (with a few debug statements added)

@users.route('/login', methods=['GET', 'POST'])
def login():

    if request.method == 'POST':                                                        ##DEBUG
        logging.debug(f"debug0: inside login func with method == POST")                 ##DEBUG

    if current_user.is_authenticated:
        return redirect(url_for('main.home'))
    form = LoginForm()
    if form.validate_on_submit():
        logging.debug(f"debug0.1: inside validate_on_submit")                          ##DEBUG
        user = Users.query.filter_by(email=form.email.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember.data)
            next_page = request.args.get('next')
            if not next_page or url_parse(next_page).netloc != '':
                next_page = url_for('main.home')
            logging.debug(f"debug1: Login was successful")                              ##DEBUG
            return redirect(next_page)
        else:
            logging.debug(f"debug2: Login failed")                                      ##DEBUG
            flash(f'Login unsuccessful. Please check email and password!', 'danger')
    logging.debug(f"debug3: the end of login func")                                     ##DEBUG
    return render_template('login.html', title='Login', form=form)

When i run Locust, I get this output:

[2019-09-16 18:03:06,598] Mac-mini-3.local/INFO/locust.main: Starting web monitor at *:8089
[2019-09-16 18:03:06,598] Mac-mini-3.local/INFO/locust.main: Starting Locust 0.11.0
[2019-09-16 18:03:14,069] Mac-mini-3.local/INFO/locust.runners: Hatching and swarming 2 clients at the rate 1 clients/s...
[2019-09-16 18:03:14,138] Mac-mini-3.local/ERROR/stderr: /Users/myuser/.local/share/virtualenvs/locustio-gB1-mbqd/lib/python3.7/site-packages/urllib3/connectionpool.py:851: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings InsecureRequestWarning)
[2019-09-16 18:03:14,162] Mac-mini-3.local/INFO/stdout: DEBUG: self.csrftoken = REDACTED
[2019-09-16 18:03:14,183] Mac-mini-3.local/INFO/stdout: DEBUG: login response.status_code = 200
[2019-09-16 18:03:14,213] Mac-mini-3.local/INFO/stdout: DEBUG list: response.status_code = 200
[2019-09-16 18:03:15,112] Mac-mini-3.local/INFO/stdout: DEBUG: self.csrftoken = REDACTED
[2019-09-16 18:03:15,137] Mac-mini-3.local/INFO/stdout: DEBUG: login response.status_code = 200

I'm not concerned about the 'InsecureRequestWarning' as this is because I am using a self signed cert and i have disabled verification with 'self.client.verify = False' The csrftoken looks correct.

From the Flask App itself, I get this output:

DEBUG:user:debug0: inside login func with method == POST
INFO:flask_wtf.csrf:The CSRF token is missing.
DEBUG:user:debug3: the end of login func
DEBUG:user:debug3: the end of login func
DEBUG:user:debug3: the end of login func
DEBUG:user:debug3: the end of login func

So, it's hitting the login function (proven by debug0) but it's not getting into the 'form.validate_on_submit()' conditional.

So far I have spent all day on this, reading articles and trying a lot of things, ie adding the X-CSRFToken headers. I feel I am missing something fundamental, and would really appreciate some help.

thanks, WJ


回答1:


This is probably not an issue with Locust. Have you tried logging in and getting the token using something like Postman or cURL?




回答2:


Ok, I solved it, so thought I would share the anwser for anyone else that comes across this. As suggested by @user10788336 its is not a Locust issue.

The issue was that when POSTing to the flask route, the form was not being validated (ie form.validate() was not getting set).

So, I made two changes.

1) changed the POST to have an additional form item which i called "test-mode" and I set the value to "locust-test"

here is the new Locust.py file:

from locust import HttpLocust, TaskSet, task
import re

class UserBehavior(TaskSet):
    def on_start(self):
        """ on_start is called when a Locust start before any task is scheduled """
        self.client.verify = False
        self.get_token()
        self.login()

    def on_stop(self):
        """ on_stop is called when the TaskSet is stopping """
        self.logout()

    def get_token(self):
        response = self.client.get("/login")
        # Sample string from response:
        # <input id="csrf_token" name="csrf_token" type="hidden" value="REDACTED">
        self.csrftoken = re.search(' name="csrf_token" .* value="(.+?)"', response.text).group(1)
        print(f"DEBUG: self.csrftoken = {self.csrftoken}")

    def login(self):
        response = self.client.post("/login",
                                    {"email": "REDACTED",
                                     "password": "REDACTED",
                                     "test-mode": "locust-test"
                                     },
                                    headers={"X-CSRFToken": self.csrftoken})
        print(f"DEBUG: login response.status_code = {response.status_code}")

    def logout(self):
        self.client.get("/logout")

    @task(5)
    def list_domains(self):
        response = self.client.get("/domains", headers={"X-CSRFToken": self.csrftoken})
        print(f"DEBUG list: response.status_code = {response.status_code}")


class WebsiteUser(HttpLocust):
    task_set = UserBehavior
    min_wait = 5000
    max_wait = 9000

The diff between old and new is:

<                                     {"email": "REDACTED", "password": "REDACTED"},
---
>                                     {"email": "REDACTED",
>                                      "password": "REDACTED",
>                                      "test-mode": "locust-test"
>                                      },

2) I changed my login function the Flask app: The change is that I don't need the form to be validated, so I skip that ONLY when I detect that I am running in test-mode.

here is the new login function:

@users.route('/login', methods=['GET', 'POST'])
def login():

    if current_user.is_authenticated:
        return redirect(url_for('main.home'))

    form = LoginForm()

    # shortcut for Locust testing - need to avoid form.validate() (which is within form.validate_on_submit())
    form_is_ok = False
    if request.method == 'POST':
        if request.form.get('test-mode') == 'locust-test':
            form_is_ok = True
        else:
            form_is_ok = form.validate_on_submit()

    if form_is_ok:
        logging.debug(f"debug0.1: inside validate_on_submit")  # DEBUG
        user = Users.query.filter_by(email=form.email.data).first()
        if user and user.check_password(form.password.data):
            login_user(user, remember=form.remember.data)
            next_page = request.args.get('next')
            if not next_page or url_parse(next_page).netloc != '':
                next_page = url_for('main.home')
            logging.debug(f"debug1: Login was successful")  # DEBUG
            return redirect(next_page)
        else:
            logging.debug(f"debug2: Login failed")  # DEBUG
            flash(f'Login unsuccessful. Please check email and password!', 'danger')
    logging.debug(f"debug3: the end of login func")  # DEBUG
    return render_template('login.html', title='Login', form=form)

The diff between old and new is:

<     if form.validate_on_submit():
---
>
>     # shortcut for Locust testing - need to avoid form.validate() (which is within form.validate_on_submit())
>     form_is_ok = False
>     if request.method == 'POST':
>         if request.form.get('test-mode') == 'locust-test':
>             form_is_ok = True
>         else:
>             form_is_ok = form.validate_on_submit()
>
>     if form_is_ok:

I think this is still secure ... thoughts on that? I might add a config variable, that disables/enables this functionality.

And it works!!!!

BTW, Locust is awesome!

Hope this helps.

cheers, WJ




回答3:


Can you share the code for what the form.validate_on_submit method is expecting/validating? Chances are there is a hidden field in the login form that is acting as a dynamic token and/or you're missing a required field. Can you also share the HTML source of the login form?

I'd also be curious to see you add an additional debug statement in your login method that outputs the value of the CSRF token, to make sure it is valid, e.g.

    def login(self):
    print(f"DEBUG: login csrftoken = {self.csrftoken}")
    response = self.client.post("/login",
                                {"email": "REDACTED",
                                 "password": "REDACTED",
                                 "test-mode": "locust-test"
                                 },
                                headers={"X-CSRFToken": self.csrftoken})
    print(f"DEBUG: login response.status_code = {response.status_code}")

I'd also be curious to see if there are any error messages when you submit the form without a valid CSRF token. Does it return a 200 status code and report the invalid token?

My guess is that it's something to do with the CSRF token not being valid or handled correctly.

Is this an older or customized version of Flask login? I'm not seeing an X-CSRFToken header method in the latest version of Flask login docs at https://flask-login.readthedocs.io/en/latest/



来源:https://stackoverflow.com/questions/57953212/how-to-login-to-flask-app-when-using-locust

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!