Search code examples
pythonpython-3.xflaskservertwo-factor-authentication

Get two-factor authentication code from user without losing session (Flask server)


I have an API request to a third-party website that works great in the command line (from https://github.com/haochi/personalcapital):

pc = PersonalCapital()

try:
    pc.login(email, password)
except RequireTwoFactorException:
    pc.two_factor_challenge(TwoFactorVerificationModeEnum.SMS)
    pc.two_factor_authenticate(TwoFactorVerificationModeEnum.SMS, input('code: '))
    pc.authenticate_password(password)

accounts_response = pc.fetch('/newaccount/getAccounts')
accounts = accounts_response.json()['spData']

When I run the above in the command line, I get back a JSON just as intended.

However, I'd like to use it in a web app on a Flask server. So, I need to remove the command line input('code: ') for SMS confirmation. I'm thinking I'll use a form via 'POST' to get the user input.

However, if I redirect() or render_template() to send the user to the form, it interrupts my API session, and I get back a "session not authenticated" response from the API.

Server logic. Routes in question are /update (email and password first) and /authenticate (SMS confirmation form):

@app.route("/update", methods=["GET", "POST"])
@login_required
def update():

    # Via post:
    if request.method == "POST":

        # Ensure userentered email
        if not request.form.get("pc_email"):
            return apology("Please enter username", 400)

        # Ensure user entered password
        elif not request.form.get("pc_password"):
            return apology("Please enter password", 400)

        # Save email & password
        email = request.form.get("pc_email")
        password = request.form.get("pc_password")

        # Try to log in
        try:
            pc.login(email, password)

        # If 2-factor is required, send sms & redirect
        except RequireTwoFactorException:
            pc.two_factor_challenge(TwoFactorVerificationModeEnum.SMS)
            return redirect("/authenticate")

        # Get data:
        else:
            # Get accounts data
            accounts_response = pc.fetch('/newaccount/getAccounts')
            accounts = accounts_response.json()['spData']

            # TODO - update database w/ data from accounts & transactions

            return redirect("/")


@app.route("/authenticate", methods=["GET","POST"])
@login_required
def authenticate():

        # Via POST:
        if request.method == "POST":

            # SMS authentication
            pc.two_factor_authenticate(TwoFactorVerificationModeEnum.SMS, \
                request.form.get(sms))
            pc.authenticate_password(password)

            # Get accounts data
            accounts_response = pc.fetch('/newaccount/getAccounts')
            accounts = accounts_response.json()

            # TODO - update database w/ data from accounts & transactions

            # Redirect to "/"
            return render_template("test.html", accounts=accounts)

        # Via GET:
        else:
            return render_template("authenticate.html")

Source code for project is here: https://github.com/bennett39/budget/blob/stackoverflow/01/application.py

How do I block the code from executing while waiting for the user to respond with their SMS code? Or, should I be going about this problem a different way?


Solution

  • The error you are experiencing is actually due to the way you try to use global variables to persist state between requests. You initially define password as a module level variable and then set password = request.form.get("pc_password") within your update function. Due to pythons rules regarding global and local variables https://docs.python.org/3/faq/programming.html#id9 this creates a new local variable containing the password value and leaves the module level variable untouched. You then access the original global password variable within your authenticate function which fails as this password variable is still set to its original value of ''. The quick fix would be to add global password at the start of your update function but this ignores the other problems with this method of persisting state. All of your global variables are shared between everyone using your site, so that if multiple people are logged in then they will all be logged into the same personal capital account. It would be preferable to use the session object to persist this data as each user will then only be able to access their own session object and there will be no risk of people accessing each others accounts. Your use of the PersonalCapital object complicates things a little as this uses instance variables to persist state, which is appropriate for a command line application but less so for a web application. It is a very simple object however, with only 2 instance variables. It should therefore be fairly straightforward to extract these and store them in the session at the end of your update function and use these values to rebuild the object at the start of your authenticate function.