Verifying Sensitive Actions with Python, Flask and Authy

Verifying Sensitive Actions with Python, Flask and Authy

Verifying Sensitive Actions with Python, Flask and Authy. Adding two-factor authentication (2FA) to your login process increases the security of your user's data.

Verifying Sensitive Actions with Python, Flask and Authy. Adding two-factor authentication (2FA) to your login process increases the security of your user's data.

Adding two-factor authentication (2FA) to your login process increases the security of your user’s data. We can extend that to validate sensitive actions like sending money from your account, changing your shipping address, or confirming a medical appointment. Even though the user should be already logged in with a username and password, we want to make sure that they authorize every payment. This blog post will show you how to secure payment actions using Python, Flask, a bit of Javascript, and the Authy API.

PSD2 & SCA

The European Payment Services Directive (PSD2) regulation requires Strong Customer Authentication (SCA) for all transactions over €30 by September 2019. This post will show you how to implement a compliant solution for your application. For more detail on PSD2, SCA, and dynamic linking, check out this post.

The solution in this post is useful regardless of regulatory requirements. For example, Gemini uses push authorizations to validate cryptocurrency withdrawals.

What you’ll need

To code along with this post, you’ll need:

Start by downloading or cloning the starter application from Github.

git clone [email protected]:robinske/payfriend-starter.git && cd payfriend-starter

If you haven’t already, now is the time to sign up for Twilio and create an Authy Application. Navigate to the Twilio Console and grab your Authy App API Key under Settings.

Copy .env.example to .env. Once we have an Authy API key, we can store it in our .env file, which helps us set the environment variables for our app. Update your .env file:

# Secret key
SECRET_KEY=replace-me-in-production

# Authy API Key
# (create an app here https://www.twilio.com/console/authy)
AUTHY_API_KEY=FLc***************************

Installing dependencies

Next, install the necessary dependencies.

On Mac/Linux:

python3 -m venv venv

. venv/bin/activate

Or on Windows cmd:

py -3 -m venv venv

venv\Scripts\activate.bat

Install Requirements:

pip install -r requirements.txt

Now we’re ready to run and test our starter application.

Run the application

On Mac OS or Linux operating systems run:

export FLASK_APP=payfriend

export FLASK_ENV=development

flask run

Or on Windows cmd:

set FLASK_APP=payfriend

set FLASK_ENV=development

flask run

Navigate to http://127.0.0.1:5000/auth/register and register yourself as a new user with your real phone number. The application already has phone verification with the Twilio Verify API, which allows us to use the user’s trusted phone number for subsequent authorizations.

Phone verification is an important part of PSD2 compliance; we need to trust the device before we can start using it for payment authorizations.

After you’ve registered you’ll end up on a page like this:

At this point, you can send a payment of any amount to one of your friends. So if I send $20 to my friend Neville, I’ll see that reflected in My Payments:

What if we wanted additional safeguards to make sure I approved sending that money to Neville?

Registering a User with Authy

When a new user signs up for our website we store them in the database and register the user with Authy. This code is already in the starter project and you can learn more about that process in the Authy API documentation.

Once we register the user with Authy we get the user’s Authy ID from the response. This is very important — it’s how we will verify the identity of our user with Authy and send subsequent authorizations from our application.

How to Add PSD2 compliant 2FA for Payment Transactions

For this transaction, we will validate that the user has their mobile phone by either:

When a user attempts to “Send a Payment” on our website, we will ask them for an additional authorization. Let’s take a look at push authorization first.

When our user sends a payment we immediately attempt to verify their identity with a push authorization. We can fall back gracefully if they don’t have a smartphone with the Authy app, but we don’t know until we try.

Authy lets us pass extra details with our push request including a message, a logo, and any other details we want to send. We could easily send any number of details by adding additional key-value pairs to our details dict. For our scenario this could look like:

details = {
    'Sending to': 'Hermione',
    'Transaction amount': '1,000,000',
    'Currency': 'Galleons'
}

Details will show up in the app. Hidden details can also be included for tracking information about the request (like origin IP address) that you may not need or want to display in the app.

Let’s write the code for sending a push authorization. Head over to payfriend/utils.py and add the following function.

def send_push_auth(authy_id_str, send_to, amount):
    """
    Sends a push authorization with payment details to the user's Authy app

    :returns (push_id, errors): tuple of push_id (if successful)
                                and errors dict (if unsuccessful)
    """
    details = {
        "Sending to": send_to,
        "Transaction amount": str('${:,.2f}'.format(amount))
    }

    hidden_details = {
        "user_ip_address": request.environ.get('REMOTE_ADDR', request.remote_addr),
        "requester_user_id": str(g.user.id)
    }

    logos = [dict(res = 'default', url = 'https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/120/apple/155/money-bag_1f4b0.png')]

    api = get_authy_client()
    resp = api.one_touch.send_request(
        user_id=int(authy_id_str),
        message="Please authorize payment to {}".format(send_to),
        seconds_to_expire=1200,
        details=details,
        hidden_details={},
        logos=logos
    )

    if resp.ok():
        push_id = resp.content['approval_request']['uuid']
        return (push_id, {})
    else:
        flash(resp.errors()['message'])
        return (None, resp.errors())

This function constructs our push authorization request. We add necessary details about the request like the transaction amount and the payee. We also define the logo that will show up in the request. If the request is successful, resp.ok() will return True and we can grab the authorization uuid from the response. Otherwise we’ll return the relevant errors.

Next, head over to payfriend/payment.py and add the code to call our new function. In def send() we only want to add the Payment to the database once we have sent the push authorization. To do that, we’ll add a conditional statement after we try to send the push authorization. Replace lines 76-79 (starting with payment =) in the starter project with the highlighted code below.

def send():
    form = PaymentForm(request.form)
    if form.validate_on_submit():
        send_to = form.send_to.data
        amount = form.amount.data
        authy_id = g.user.authy_id

        # create a unique ID we can use to track payment status
        payment_id = str(uuid.uuid4())

        (push_id, errors) = utils.send_push_auth(authy_id, send_to, amount)
        if push_id:
            payment = Payment(payment_id, authy_id, send_to, amount, push_id)
            db.session.add(payment)
            db.session.commit()
            return jsonify({
                "success": True,
                "payment_id": payment_id
            })
        else:
            flash("Error sending authorization. {}".format(errors))
            return jsonify({"success": False})

    return render_template("payments/send.html", form=form)

Once we send the request we update the payment status based on the response. Let’s update our payments view to show the status. Open payfriend/templates/payments/list.html and add a column for the status:

<table style="width:100%">
  <tr>
    <th>Your Email</th>
    <th>Payment ID</th>
    <th>Sent to</th>
    <th>Amount</th>
    <th>Status</th>
  </tr>
{% for payment in payments %}
<tr>
  <td>{{ payment.email }}</td>
  <td>{{ payment.id }}</td>
  <td>{{ payment.send_to }}</td>
  <td>{{ "${:,.2f}".format(payment.amount) }}</td>
  <td>{{ payment.status }}</td>
</tr>
{% endfor %}
</table>

Make sure you have the Authy app installed on your phone and that you’re registered for Authy with the same phone number that you used to register for Payfriend. Restart the Flask application and send a payment. We’re not updating the status yet but this time you’ll get an authorization request in the Authy app and see a ‘pending’ status attached to that new payment ID.

Configure the push authorization callback

In order for our app to know what the user did after we sent the authorization request, we need to register a callback endpoint with Authy.

In payment.py add a new route for the callback. Here we look up the payment using the uuid sent with the Authy POST request.

def update_payment_status(payment, status):
    # once a payment status has been set, don't allow that to change
    # this requires a new transaction in order to be PSD2 compliant
    if payment.status != 'pending':
        flash("Error: payment request was already {}. Please start a new transaction.".format(
            payment.status))
        return redirect(url_for('payments.list_payments'))

    payment.status = status
    db.session.commit()

@bp.route('/callback', methods=["POST"])
@verify_authy_request
def callback():
    """
    Used by Twilio to send a notification when the user
    approves or denies a push authorization in the Authy app
    """
    push_id = request.json.get('uuid')
    status = request.json.get('status')
    payment = Payment.query.filter_by(push_id=push_id).first()

    update_payment_status(payment, status)
    return ('', 200)

We need one more endpoint that our client side code can query to check the payment status and update our view accordingly. Add this to payment.py:

@bp.route('/status', methods=["GET", "POST"])
@login_required
def status():
    """
    Used by AJAX requests to check the OneTouch verification status of a payment
    """
    payment_id = request.args.get('payment_id')
    payment = Payment.query.get(payment_id)
    return payment.status

Let’s take a look at that client-side code now.

Handle Two-Factor Asynchronously

We want to handle our authorization asynchronously so the user doesn’t even know it’s happening.

We’ve already taken a look at what’s happening on the server side, so let’s step in front of the cameras now and see how our JavaScript is interacting with those server endpoints.

First, we hijack the payment form submit and pass the data to our controller using Ajax. If we expect a push authorization response, we then begin polling /payment/status every 3 seconds until we see the request was either approved or denied. Our callback will update /payment/status so we will know when an authorization has been approved or denied.

In auth.js update the sendPayment function to check for the payment authorization status before redirecting the user.

var sendPayment = function(form) {
    $.post("/payments/send", form, function(data) {
      if (data.success) {
        $(".auth-ot").fadeIn();
        checkPaymentStatus(data.payment_id);
      }
    });
  };

var checkPaymentStatus = function(payment_id) {
    $.get("/payments/status?payment_id=" + payment_id, function(data) {
      if (data == "approved") {
        redirectWithMessage('/payments/', 'Your payment has been approved!')
      } else if (data == "denied") {
        redirectWithMessage('/payments/send', 'Your payment request has been denied.');
      } else {
        setTimeout(checkPaymentStatus(payment_id), 3000);
      }
    });
  };

You’ll need a publicly accessible route that Authy can access in order to handle the callback and for that we will use ngrok. Note that in this tutorial only the HTTP address from ngrok will work, so you should start it using this command:

ngrok http -bind-tls=false 5000

Copy the Forwarding url:

Head back to Authy Console and update your application’s Push Authentication callback URL with /payments/callback appended.

Leave ngrok running in the background and try sending a new payment. Now when you approve or deny the request your application will update the payment status and you can see it reflected in your list of payments.

Now let’s see how to handle the case where a user doesn’t have the Authy app installed.

Fallback to SMS

There are reasons that you could include SMS as a verification option: like being able to reach users without smartphones and onboard users seamlessly (no app install required).

In utils.py add the code to send and validate an authorization token via SMS. One important feature we’re taking advantage of here is the action and action_message parameters. The action will tie the SMS authorization to the specific transaction, a requirement for PSD2. The action message will add important details to the SMS message body about the payee and amount of the transaction, other requirements for PSD2 and a helpful message to the user regardless of regulation.

def send_sms_auth(payment):
    """
    Sends an SMS one time password (OTP) to the user's phone_number

    :returns (sms_id, errors): tuple of sms_id (if successful)
                               and errors dict (if unsuccessful)
    """
    api = get_authy_client()
    session['payment_id'] = payment.id
    options = {
        'force': True,
        'action': payment.id,
        'action_message': 'Verify Payment to {} for {}'.format(
            payment.send_to,
            str('${:,.2f}'.format(payment.amount)))
    }
    resp = api.users.request_sms(payment.authy_id, options)
    if resp.ok():
        flash(resp.content['message'])
        return True
    else:
        flash(resp.errors()['message'])
        return False


def check_sms_auth(authy_id, payment_id, code):
    """
    Validates an one time password (OTP)
    """
    api = get_authy_client()
    try:
        options = {
            'force': True,
            'action': payment_id,
        }
        resp = api.tokens.verify(authy_id, code, options)
        if resp.ok():
            return True
        else:
            flash(resp.errors()['message'])
    except Exception as e:
        flash("Error validating code: {}".format(e))

    return False

Note that you must include the same action parameter when sending and checking the token. We’re using the payment_id for that.

Next, add a new route for starting the SMS authorization in payments.py and a wrapper function for our validation utility:

@bp.route('/auth/sms', methods=["POST"])
@login_required
def sms_auth():
    if not g.user.authy_id:
        return(redirect(url_for('auth.verify')))

    payment_id = request.form['payment_id']
    session['payment_id'] = payment_id
    payment = Payment.query.get(payment_id)

    if utils.send_sms_auth(payment):
        return redirect(url_for('auth.verify'))
    else:
        return redirect(url_for('payments.send'))

def check_sms_auth(authy_id, payment_id, code):
    """
    Validates an SMS OTP.
    """
    if utils.check_sms_auth(g.user.authy_id, payment_id, code):
        payment = Payment.query.get(payment_id)
        update_payment_status(payment, 'approved')
        return redirect(url_for('payments.list_payments'))
    else:
        abort(400)

Finally we can take advantage of the existing token verification route we used for phone verification in auth.py with a few small changes.

Add an import for our new check_sms_auth function in auth.py:

from payfriend.payment import check_sms_auth

Then update the /verify route to check based on the type of verification. Right now this route only handles phone verification on signup before we’ve created the Authy user. Therefore we can assume that if the global user has an Authy ID then we can check the SMS authorization instead of the phone verification.

The new /verify route will look like this:

@bp.route('/verify', methods=('GET', 'POST'))
def verify():
    """
    Generic endpoint to verify a code entered by the user.
    """
    form = VerifyForm(request.form)
    validated = form.validate_on_submit()

    if form.validate_on_submit():
        email = g.user.email
        (country_code, phone) = utils.parse_phone_number(g.user.phone_number)
        code = form.verification_code.data

        # route based on the type of verification
        if not g.user.authy_id:
            if utils.check_verification(country_code, phone, code):
                return handle_verified_user(email, country_code, phone, code)
        else:
            payment_id = session['payment_id']
            return check_sms_auth(g.user.authy_id, payment_id, code)

    return render_template('auth/verify.html', form=form)

Finally, we need to update the client code to allow for SMS authorization. You can handle this many ways, but let’s make the Send SMS button appear after 15 seconds if the user hasn’t approved the push authorization in auth.js:

var sendPayment = function(form) {
  $.post("/payments/send", form, function(data) {
    if (data.success) {
      $(".auth-ot").fadeIn();
      checkPaymentStatus(data.payment_id);

      // display SMS option after 15 seconds
      setTimeout(function() {
        $("#payment_id").val(data.payment_id);
        $(".auth-sms").fadeIn();
      }, 15000);
    }
  });
};

Nice! Now you have a PSD2 compliant application with two different options for authorization.

Where to next?

The full code is available on my Github. Take a look at this diff to compare the finished solution with the starter solution.

Learn More

Complete Python Bootcamp: Go from zero to hero in Python 3

Complete Python Masterclass

Learn Python by Building a Blockchain & Cryptocurrency

Python and Django Full Stack Web Developer Bootcamp

The Python Bible™ | Everything You Need to Program in Python

Learning Python for Data Analysis and Visualization

Python for Financial Analysis and Algorithmic Trading

The Modern Python 3 Bootcamp

python flask

What's new in Bootstrap 5 and when Bootstrap 5 release date?

How to Build Progressive Web Apps (PWA) using Angular 9

What is new features in Javascript ES2020 ECMAScript 2020

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

Random Password Generator Online

HTML Color Picker online | HEX Color Picker | RGB Color Picker

Basic Data Types in Python | Python Web Development For Beginners

In the programming world, Data types play an important role. Each Variable is stored in different data types and responsible for various functions. Python had two different objects, and They are mutable and immutable objects.

Python Flask - Introduction to Flask Templates

This is our second tutorial in Python Flask, in this tutorial we are going to have Introduction to Flask Templates, so for this Flask looks for the template

Python Flask-Mail Library to Send Emails in Browser Using Flask Full Project For Beginners

Python Flask-Mail Library to Send Emails in Browser Using Flask Full Project For Beginners #python #flask #flaskmail Welcome Folks My name is Gautam

Python Flask for Beginners: Build a CRUD Web App with Python and Flask

In this Python Flask tutorial, you'll learn to build CRUD web applications using Python and Flask. Python and Flask can make building a CRUD app super easy.

Flask How to Create Routes with Flask-Classy - Code Loop

In this Flask article we are going to learn How to Create Routes with Flask-Classy, Flask-Classy is an extension that adds class-based views to Flask. so