Securing Your Site with 2FA

Today we’re going to take a quick tour of two-factor authentication (2FA) and how to better secure your site—one of the top use cases for 2FA. This service is easy to implement and provides a good barrier against many types of intrusion attempts, as well as increasing your customers’ trust in the security of your application.
The most common use case for 2FA is as a second layer of authentication when someone logs into a website. After providing a password you send a code to a known phone number and they reply with that code, providing a second proof point, or factor, of authentication—hence the name of two-factor authentication.
There are actually several other useful use cases for 2FA. The most common is confirming ownership of a number in the first place. When a customer enables 2FA they will need to provide you with a phone number, at that time you can send them their first 2FA code. When they provide it in return, you will know that they have access to this phone number.
Note: It’s important to recognize that messaging and voice ownership on a number can be split. For the highest level of security, you may want to validate both by sending a 2FA code first by SMS and then by voice delivery after.
Protecting your login
In order to protect your site you’ll need to add one additional step to the login process—a challenge for a 2FA code. This step will be inserted after you have validated their password, but before you grant them access to the site, as shown below.

As you’ll note above, after sending the code to their phone we then present the page allowing them to provide it. You can choose to prompt them for their delivery preferences, voice or SMS, as well their preferred number if they have validated more than one, either before sending the code or when they initially enable the service. Asking at the time of login does provide for more flexibility on your users’ part.
Turning to our code, we’ll get the basics of our 2FA interaction setup, starting with the sending of the 2FA code, then displaying the page where they can enter it in.
# Having already verified the user’s username and password
# obtain user info
user = get_user()
# send out 2FA code
send2FA(config['bandwidth']['account_id'], user, scope)
# then show 2FA request
# we'll pass the scope in the html for simplification of the demo, but it should be somewhere non-user accessible
message = "We just sent you a 2FA code for '" + scope + "', please enter it here"
return render_template('2fa_form.html', username=user.username, scope=scope, message=message)
And now we get to the meat of sending the 2FA code to the user, our send2FA
function. You’ll see that this function supports sending 2FA messages for either voice or SMS and with a scope defined by the calling function—we’ll explain that scope field in a little bit.
def send2FA(account_id, user, scope):
'''
Send out a 2FA Code
:param account_id your BAND account id
:param user who is receiving this code, object has their number and username
:param scope the scope for this request, e.g. login, secure action, etc
:return None
'''
# determine if the user has a preference for voice or sms, for use below
if(user.delivery_pref == "sms"):
application_id = config['bandwidth']['messaging_application_id']
else:
application_id = config['bandwidth']['voice_application_id']
# These three variables are available for expansion by our 2FA service
# {NAME} (optional) is the name of your Application within the Bandwidth dashboard
# {SCOPE} (optional) is the scope defined by this call, e.g. login, admin, verify
# {CODE} (required) is the code created by the system
message = user.username + ", your {NAME} {SCOPE} code is {CODE}"
try:
body = {
# from is any number in the location referenced by your Bandwidth Messaging Application
"from": config['numbers']['from_number'],
"to": user.number, # the recipient of the message
"applicationId": application_id,
"scope": scope,
"digits": 6, # 4-8
"message": message
}
auth_client = bandwidth_client.two_factor_auth_client.client
if(user.delivery_pref == "sms"):
auth_client.create_messaging_two_factor(account_id, body)
else:
auth_client.create_voice_two_factor(account_id, body)
return None
except APIException as e:
print("send2FA> Failed to send 2FA: %s" %
e.response.text)
return None
This is pretty straightforward, you’ll see that you can control the message that is provided to the user. You could vary this for voice and messaging if you want. You can also vary the length of the code that is provided here.
One parameter of the body that is worth calling out is the from
parameter, this is where you set the number you want your code to come from. With Bandwidth your 2FA codes come from a number that you own and control—you aren’t sharing it with other customers. This provides additional security and identity control for you, and a more consistent and reliable experience for your customers.
Our function to verify the code supplied back by the user is similarly flexible, it can be used to validate codes sent via voice or SMS. This function returns True
or False
, based on the response from our 2FA validation service. If False
, you’ll want to challenge them again for the proper code. You may choose to give them the option to resend as well.
def validate2FA(account_id, user, scope, code):
'''
Validate the 2FA Code you sent out before
:param account_id your BAND account id
:param user who is receiving this code
:param scope the scope for this request, e.g. login, secure action, etc
:param code The code that was given back to you by the End User
:return True or False
'''
# determine if the user has a preference for voice or sms, for use below
if(user.delivery_pref == "sms"):
application_id = config['bandwidth']['messaging_application_id']
else:
application_id = config['bandwidth']['voice_application_id']
try:
body = {
# Should be the same as your request
"from": config['numbers']['from_number'],
"to": user.number,
"applicationId": application_id,
"scope": scope,
"code": code,
"digits": 6,
"expirationTimeInMinutes": 3
}
auth_client = bandwidth_client.two_factor_auth_client.client
response = auth_client.create_verify_two_factor(account_id, body)
return response.body.valid
except APIException as e:
print("validate2FA> Failed to validate 2FA: %s" %
e.response.text)
return None
If you were paying attention, you’ll notice that you were never in possession of the code that Bandwidth sent your user. This provides additional protection, should your storage or session data be compromised.
Should you want to secure another area of your application, for example the ability to add users, delete an account, or maybe transfer large sums of money, you can secure that action with 2FA as well. In order to provide a higher level of security, we suggest that you do so with a different Scope
. This will ensure that no one can use a code meant for logging in for this purpose and it is why the initial code block we showed you utilized a variable for the scope
field.
In our example we built a reusable code flow and template page for collecting 2FA codes. If you’d like to see how this is done, please feel free to peruse the git repository. If you’d like to see a more adventurous 2FA implementation, you can check out how a 2FA developer secured his front door with 2FA!