Passwords have had their day. At least, that is, as the sole layer of security when protecting important information systems. With all the high-profile hacks in recent times and the majority of users still using easily-guessable passwords which are the same for multiple accounts across multiple websites, it’s so surprise that Two-Factor Authentication has gained in popularity so much in recent years.
Two-Factor Authentication works on the concept of making you prove not one thing, but two. The first – something you know – is your password. The second – something you physically have – is your mobile phone. And you prove that you have your phone by letting the website text you a random code which you enter in. It’s beautifully simple yet extremely effective. It’s not 100% completely foolproof and hack-proof, but it’s pretty darn close.
In this tutorial I’ll show you how to build a simple website login form which presents an extra two-factor authentication step for accounts which have the option switched on. You can then take the concepts and adapt it to your own system, where you might either want to enforce Two-Factor Authentication or leave it as an optional extra for your more security-conscious users!
The key concept that two-factor authentication centres around is providing the user with a single-use token (ie. a code) that we give to them on a different medium to that which they’re using to log in. Some systems rely on giving a keyfob to the user for them to keep on their keyring and lose/break whilst others either send a text message or automated phone call to the user to give them their code. In this example we’ll be sending an SMS from our application using Telecoms Cloud and their REST API.
I’ll be using PHP in this example as it’s the language I’m most familiar with, but you can adapt the concepts to whatever platform you’re most comfortable working with, and the Telecoms Cloud API can be used easily from any programming language.
Set up SMS for Two-Factor Authentication
- Create a free Telecoms Cloud account at www.telecomscloud.com – you’ll get £5 free credit when you sign up which is plenty to send yourself lots of test text messages whilst you develop your app
- Under My Account select API Settings to generate your Client ID and Client Secret that we’ll need later on
- From your My Numbers page, make a note of your Telecoms Cloud number so that the SMS messages sent by your application can appear to come from this number. If you don’t have one, you can pass the default SMS sender number which you’ll see in your account settings.
- Since we’re using PHP in this example, we’ll use the PHP Client for the Telecoms Cloud API from github.com/TelecomsCloud to make our code a bit simpler. Install and configure the Client to use the code samples in this guide.
Set up User Database for Two-Factor Authentication
I’m using a MySQL database here but you can use whatever database you like – the concepts are the same!
- Create a database and database user with appropriate permissions. I’m using phpMyAdmin but you can of course use whatever database manager you like.
- Create a table with the statement below:
1 2 3 4 5 6 7 |
CREATE TABLE IF NOT EXISTS `users` ( `user_id` int(10) NOT NULL, `username` varchar(50) NOT NULL, `password_hash` varchar(255) NOT NULL, `mfa_status` tinyint(1) NOT NULL DEFAULT '0', `mobile_number` varchar(25) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ; |
To generate a secure password hash for our example, you can run the following code:
1 |
echo password_hash('my-ultra-secure-password', PASSWORD_DEFAULT); |
Paste the output from the code above into phpMyAdmin, being careful not to include any whitespace either side of the string of text.
Build Login Forms & Script for Two-Factor Authentication
Now we’ve got our SMS account set up with Telecoms Cloud, and our user database built and a test account created, we’re ready to build the rest of the system!
Step 1: Establish connections to our local database and the Telecoms Cloud API
The first thing we’ll want to do in our script is set up the connection to our database and the Telecoms Cloud API
The path to the autoload.php file will depend on where you installed the PHP Client, check the documentation for more details. Make sure you put your Client ID and Client Secret into lines 5 and 6, and then your database username & password into lines 10 and 11.
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php // Initialise Telecoms Cloud API require_once __DIR__ . '/../phapic/vendor/autoload.php'; $telecomsCloudApiUri = 'https://api.telecomscloud.com'; $clientId = 'my-id'; $clientSecret = 'my-secret'; // Connect to local MySQL database $dsn = 'mysql:host=localhost;dbname=tcApi'; $dbUsername = 'tcApiUser'; $dbPassword = 'my-db-passwd'; $pdo = new PDO($dsn, $dbUsername, $dbPassword); |
Step 2: Handle Login Attempts
First off, start a new session with the following line:
1 |
session_start(); |
We then want to take care of what we do if a user has submitted our login form (which we’ve not built yet – but don’t worry, that’s coming). The easiest way to do this in PHP is to check to see if the $_POST superglobal is empty or not, like this:
1 2 3 |
if(!empty($_POST)) { // User is attempting to login - we'll do stuff here } |
Since this login process is “two-factor”, it’s also “two-stage” as well. By that I mean that a user first enters their username and password then if they’re valid, the second stage is receiving the text message with a code they enter. At this point, they’re half logged in so we need some way of a) establishing that fact and b) remembering who they are between form submissions! We do this by saving their username as a session variable, but not yet storing the fact that they’re authenticated. So, the first thing we check for inside the curly braces for a POST submission is if a session variable called username exists yet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
if(empty($_SESSION['username'])) { // Username isn't yet set, but form has been submitted, // so we must be in the first stage $username = trim($_POST['username']); // Verify username and password $query = sprintf("SELECT username, password_hash, mfa_status, mobile_number FROM users WHERE username = %s", $pdo->quote($username)); $result = $pdo->query($query); $row = $result->fetch(PDO::FETCH_ASSOC); $result->closeCursor(); if(!password_verify(trim($_POST['password']), $row['password_hash'])) { session_unset(); echo 'Invalid Password. Please check and try again'; } else { // correct password entered - we'll do more here } } |
In the code above we grab the username they entered and use this to pull some account data from our database (using the connection we established earlier on). We grab more than just their hashed password, as if it’s a successful login attempt we’ll need their mobile number to send the SMS to, so it makes sense to grab that now rather than query the database twice.
We use the password_verify function to compare the hashed version of their password which we stored in the database with the password they just entered. If the password was wrong we abort mission and present a message telling them so. Otherwise, we can continue:
1 2 3 4 5 6 7 8 9 10 11 12 |
// Correct Password has been entered, save username in session $_SESSION["username"] = $row["username"]; // Do they require 2-factor authentication login? if($row['mfa_status'] == 1) { // Two-Factor Authentication happens here } else { // Authenticate without sending SMS $_SESSION["authenticated"] = true; } |
We now want to make them “half” logged in, by storing their username in the session, so we start off by doing that. However, for them to be fully logged in to this app, the “authenticated” session variable must also be set. We’ll not set that until we’ve established if they have Two-Factor Authentication turn on in their account or not.
In the snippet above, we check the mfa_status setting in their account which can either be 1 or 0, depending on if it’s turned on for that user’s account. That’s obviously a policy decision for you to implement in your own application; you might make it compulsory, or you might charge extra for that feature. Either way, you’ll need to build a simple interface to allow (or implore) users to turn the feature on and verify their mobile number before saving it as their MFA device. I won’t cover that in this article, but with the concepts discussed here you will have no problems building that!
If Two-Factor Authentication isn’t turned on in the user’s account, they go from being half-logged-in to fully-logged-in rather quickly! We simply set the authenticated session variable and have done with it. Inside the first set of curly braces, though, we have the juicy stuff:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// Make a 6 digit random number $code = ''; for($i = 0; $i < 6; $i++) { $code .= mt_rand(0, 9); } // Save one-time code in session $_SESSION["code"] = $code; // Send code as SMS by using Telecoms Cloud API $SMS_text = "Please use " . $code . " as your account security code."; // Create storage interface for retrieval/generation of OAuth access token $storage = new \Tc\Phapic\PdoStorageInterface( $clientId, $clientSecret, $pdo); $telecomsCloud = new \Tc\Phapic\Phapic( $telecomsCloudApiUri, $storage, $clientId, $clientSecret); // Send the SMS $send = $telecomsCloud->sendSMS( $row["mobile_number"], $myTelecomsCloudNumber, $SMS_text); |
What we’re doing above is first generating a random code to send to the user’s phone via SMS, and we also save that code in the session so that next time the form is submitted we can see if the code the enter is the one we sent them.
Important: Don’t use mt_rand()
in the real-world; I’ve used it here for simplicity as this is just an example (with no dependencies) to explain how Two-Factor Authentication works and how to send the code you generated as an SMS. For cryptographically secure alternatives if you’re using PHP, read the advice at http://php.net/mt_rand
We then instantiate an authenticated telecomsCloud object for the API and call the sendSMS method to send a text. There’s an optional 4th parameter which I’ve missed off here, which is the two-letter country code matching the location of the number you’re sending to. This defaults to GB so I’ve left it blank. For the 3rd parameter, from, I’ve passed a variable which is one of my Telecoms Cloud service numbers so that the SMS appears to come from that number. You’ll need to specify that somewhere in your application. If you don’t have a service number with Telecoms Cloud, you can specify the default SMS from number (which you’ll find in your Telecoms Cloud account settings) although that’s not ideal as the identity of your messages won’t be consistent with your company/brand so get your own service number if you can!
The sendSMS method returns a unique reference ID for that message which can be used to check the dispatch/delivery status (along with the time the message was delivered) if you wish, but I’ve left that out to keep the example simple.
Next, we need to turn our attention to what to do if the form is submitted and the username is set in a session variable. The only time this will happen is when a user is submitting the code they’ve received via SMS, so we’ll handle it like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
} else if(isset($_POST['code'])) { // Customer's entered in the 2-factor code, check it if(trim($_POST['code']) == $_SESSION["code"]) { $_SESSION["authenticated"] = true; unset($_SESSION["code"]); } else { session_unset(); echo 'Invalid code. Please check and try logging in again'; } } |
Quite simply, we check to see if the entered code matches the one we previously saved in the session before texting it to the user. If it does – great! – we’ll set authenticated to true in the session and they’re logged in.
If the code is incorrect, we’ll output a message advising the customer, and they can start the whole login process again. (Here, you might also want to add in a mechanism that blocks their account for, say, 30 minutes if they enter an incorrect code more than 3 times, in order to frustrate anyone trying to hack their account by guessing codes.)
Step 3:Â Create Two-Factor Authentication Login Forms
There are three possible screens we can display to our user depending on which stage of the login process they’re at:
- Nothing set in session, ie. the user has just landed on the login page and hasn’t yet started logging in
- If they have Two-Factor Authentication enabled, the screen asking them to enter the code we sent to them via SMS
- On successful login (with or without Two-Factor Authentication) we’ll welcome them and then direct them to their account area
The first state we’ll check for, though, is if they’re logged in. You’ll recall above that in this application we’re considering users to be logged in if the authenticated session variable is set, and is set to true. Checking this is dead easy:
1 2 3 4 5 6 7 8 |
if(isset($_SESSION["authenticated"])) { ?> <h1>Welcome to the Very Secret Private Area</h1> <a href="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>?logout">Logout</a> <?php } else { // user needs to login } |
In the real world, you’d probably use a header redirect here to send them to the my account page here, but to keep the example simple I’ve simply put a welcome message and a logout button. More on that logout button later!
In the else section above we handle the logic for if the user is at the first or second stage of logging in:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
if(!isset($_SESSION["username"])) { ?> <h1>Login</h1> <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post"> <input type="text" name="username" /> <input type="password" name="password" /> <input type="submit" value="Login" /> </form> <?php } else { // Display mobile number to customer but star out all but last 4 digits $mobile_number = $row["mobile_number"]; $starred = str_repeat('*',strlen($mobile_number) - 4); $starred .= substr($mobile_number, -4); ?> <h2>Please Enter the Code</h2> <p>We've just sent a one-time code via SMS to <?php echo $starred; ?> - please enter it below to login.</p> <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post"> <input type="text" name="code" /> <input type="submit" value="Confirm" /> </form> <?php } |
If username isn’t set yet, then they must be at the very first stage of logging in so we display the login form asking for their username and password:
Otherwise, if we didn’t stop at the first hurdle (saying they’re authenticated) then we must be requiring a Two-Factor Authentication code to be entered, so we ask them to enter that code. We know the number from the code earlier on, so we can helpfully output the last 4 digits of the phone number to which we’ve sent the code, to assist the customer without giving the entire number away to any would-be hacker:
When that form is submitted we’ll jump to the section of PHP code validating the code entered by the user against the code stored in the session, because username is set in the session, and code is set as POST variable.
The logout button
Lastly, we’ll go right up to the top of our script immediately after the session_start(); section and add these lines:
1 2 3 4 |
if(isset($_GET['logout'])) { session_destroy(); session_start(); } |
That destroys our session and all data stored in it (eg. authenticated and username) then starts us a brand new session so the user can log in again if they need to. Any page in your application can now link users to your login form with “?logout” in the query string, and the user will be logged out.
Complete Two-Factor Authentication Code Sample with SMS and PHP:
Bringing it all together, we have the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
<?php // Initialise Telecoms Cloud API require_once __DIR__ . '/../phapic/vendor/autoload.php'; $telecomsCloudApiUri = 'https://api.telecomscloud.com'; $clientId = 'my-id'; $clientSecret = 'my-secret'; // Connect to local MySQL database $dsn = 'mysql:host=localhost;dbname=tcApi'; $dbUsername = 'tcApiUser'; $dbPassword = 'my-db-passwd'; $pdo = new PDO($dsn, $dbUsername, $dbPassword); session_start(); if(isset($_GET['logout'])) { session_destroy(); session_start(); } if(!empty($_POST)) { // User is attempting to login if(empty($_SESSION['username'])) { // Username not yet set; first stage of login $username = trim($_POST['username']); // Verify username and password $query = sprintf("SELECT username, password_hash, mfa_status, mobile_number FROM users WHERE username = %s", $pdo->quote($username)); $result = $pdo->query($query); $row = $result->fetch(PDO::FETCH_ASSOC); $result->closeCursor(); if(!password_verify(trim($_POST['password']), $row['password_hash'])) { session_unset(); echo 'Invalid Password. Please check and try again'; } else { // Correct Password has been entered, save username in session $_SESSION["username"] = $row["username"]; // Do they require 2-factor authentication login? if($row['mfa_status'] == 1) { // Make a 6 digit random number $code = ''; for($i = 0; $i < 6; $i++) { $code .= mt_rand(0, 9); } // Save one-time code in session $_SESSION["code"] = $code; // Send code as SMS by using Telecoms Cloud API $SMS_text = "Please use " . $code . " as your account security code."; // Create storage interface for retrieval/generation of OAuth access token $storage = new \Tc\Phapic\PdoStorageInterface( $clientId, $clientSecret, $pdo); $telecomsCloud = new \Tc\Phapic\Phapic( $telecomsCloudApiUri, $storage, $clientId, $clientSecret); // Send the SMS $send = $telecomsCloud->sendSMS( $row["mobile_number"], $myTelecomsCloudNumber, $SMS_text); } else { // Authenticate without sending SMS $_SESSION["authenticated"] = true; } } } else if(isset($_POST['code'])) { // Customer's entered in the 2-factor code, check it if(trim($_POST['code']) == $_SESSION["code"]) { $_SESSION["authenticated"] = true; unset($_SESSION["code"]); } else { session_unset(); echo 'Invalid code. Please check and try logging in again'; } } } if(isset($_SESSION["authenticated"])) { ?> <h1>Welcome to the Very Secret Private Area</h1> <a href="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>?logout">Logout</a> <?php } else { if(!isset($_SESSION["username"])) { ?> <h1>Login</h1> <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post"> <input type="text" name="username" /> <input type="password" name="password" /> <input type="submit" value="Login" /> </form> <?php } else { // Display mobile number to customer but star out all but last 4 digits $mobile_number = $row["mobile_number"]; $starred = str_repeat('*',strlen($mobile_number) - 4); $starred .= substr($mobile_number, -4); ?> <h2>Please Enter the Code</h2> <p>We've just sent a one-time code via SMS to <?php echo $starred; ?> - please enter it below to login.</p> <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post"> <input type="text" name="code" /> <input type="submit" value="Confirm" /> </form> <?php } } |
Summary
This tutorial should have given you a basic understanding of how Two-Factor Authentication works and how it adds a really good extra layer of security to users’ accounts rather than just using passwords on their own.
There’s plenty of further expansion you can do to this basic example, for example you could give users the option of having their code read to them down the phone by an automated phone call, for example.
Hello Admin,
Please where can i get the autoload.php
Hi Oni, I don’t think the service is operational any more – you could try someone like Twilio instead.
Thanks!