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.
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.
To code along with this post, you’ll need:
Start by downloading or cloning the starter application from Github.
git clone git@github.com: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***************************
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.
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?
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.
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.
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.
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.
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.
The full code is available on my Github. Take a look at this diff to compare the finished solution with the starter solution.
☞ Complete Python Bootcamp: Go from zero to hero in Python 3
☞ 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