Two Factor Authentication With PHP and Africa’s Talking SMS API

two factor authentication
Two factor authentication is an additional layer of security used to ensure only authenticated users gain access to an online account. because Passwords are historically weak, and can be easily stolen, it can’t alone be the way that users access their accounts.

Introduction

In this tutorial, we will be building a secure authentication system to log in and signup users with two-factor authentication using Africa’s talking SMS API.  

What You’ll Need?

  • Basic knowledge of PHP & MYSQL.
  • XAMPP server installed on your machine. (You can use whatever you want. I’m using XAMPP).
  • An Africa’s Talking account.
  • An Africa’s Talking API key & username & short code.

How does it work?

First, we will create the registration form and collect the user data and process it with PHP, here we’ll talk about error handling, form sanitization and form validation. If the data is valid we proceed to create new users in the database.

When users log in, if they entered the correct data, a random number will be sent to the Africa’s Talking API, this will then send that number to the phone number we specified, we will then redirect the users to another form where they have to enter the number they’ve received. If the numbers match, we’ll generate an access token and assign it to a cookie and create a session in the database then redirect to the home page.

Project structure

In XAMPP htdocs folder I have folder called auth and it’s structured as below:

htdocs
├── auth
│       ├── config.php
│       ├── functions.php
│       ├── index.php
│       ├── login.php
│       ├── checkpoint.php
│       └── register.php

Getting Started

First you need to create a database and two tables in it in MYSQL

1-users

2-sessions

Create a new Africa’s Talking account HERE if you don’t have one yet

We then generate an API Key

Head over to the Africa’s Talking dashboard

From the side menu click settings -> API Key

Type your password and generate your API key and remember to save it because you can’t access it again.

Now for the username, we’re going to use the Africa’s Talking sandbox because this is a test app so the username is always “sandbox”.

Add shortcode

From the side menu click SMS -> shortcodes

add your code and you’re good to go. remember you’ll use this code to appear to the recipients as the sender.

let’s dig in!

Let’s start with the config file to connect to the database you created.

I’m using the PDO class to connect to the MYSQL database, it takes three parameters when you construct it:

  • A string contains (“type of database: database name and host)
  • Your database username
  • Your database Password

If you are not familiar with PDO please follow this LINK to get more information about it.

This file will be imported into every page we interact with, in the database, and we’ll use this $con variable to do so.

Registration form

In the register.php add html form with inputs for first name, last name, email, confirm email, password, confirm password and phone number.

Add Css :

html, body, * {
    margin: 0;
    padding: 0;
    font-family: 'Roboto', sans-serif;
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
}
body {
    width: 100%;
    min-height: 100vh;
    background: #f1f1f1;
    display: flex;
    align-items: center;
    justify-content: center;
}
form {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.form-body {
    width: 500px;
    height: auto;
    background: #ccc;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 15px;
    border-radius: 8px;
}
.form-header {
    width: 100%;
    height: auto;
    display: flex;
    justify-content: flex-start;
}
.form-header h1 {
    font-size: 22px;
    font-weight: 500;
    color: #666666;
    margin-bottom: 15px;
}
.errors {
    width: 100%;
    height: auto;
    display: flex;
    flex-direction: column;
    padding: 5px;
    margin-bottom: 5px;
}
.input {
    width: 100%;
    height: 35px;
    background: #bdbdbd;
    margin: 5px 0px;
    padding: 0px 10px;
    border: 1px solid #b3b3b3;
    font-size: 16px;
    color: #606060;
}
.input:focus {
    border: 1px solid #b3b3b3;
    outline: none;
}
.btn {
    font-size: 18px;
    color: white;
    background-color: #4343ff;
    padding: 10px 30px;
    border: none;
    outline: none;
    border-radius: 3px;
    margin: 10px 0;
    cursor: pointer;
}
.btn:hover {
    background-color: #5f5ff9;
}
.form-text {
    font-size: 13px;
    font-weight: 400px;
    color: #404040;
    text-decoration: none;
}
.form-text:hover {
    text-decoration: underline;
}
.errors {
    width: 100%;
    height: auto;
    background: rgb(170, 13, 13);
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: flex-start;
    padding: 5px;
}
.error {
    font-size: 15px;
    color: #f1f1f1;
}

Don’t forget to add the name attribute to each input to be used in processing with php.

We will then submit the form to the same page (“register.php”) by adding an action attribute to the form element and specifying the link to the same page where the form is.

Above the html code, open <?PHP ?> tags and let’s start to process the form data:

<?php
require_once("config.php");

if(isset($_POST["register"])) {
    $errors = array();

    $firstName = $_POST["firstName"];
    $lastName = $_POST["lastName"];
    $email = $_POST["email"];
    $confirmEmail = $_POST["email2"];
    $password = $_POST["password"];
    $confirmPassword = $_POST["password2"];
    $phone = $_POST["phone"];
?>

First, check if the register button was clicked before we load the page (remember register is the name attribute we gave it to the input).

create errors variable of type array to store any potential error.

then collect form data by creating a variable for each input and get the input value by passing its name in the $_POST global array.

Form sanitization & validation

We can’t accept our form inputs like that, we can’t also trust that the user didn’t write code in these inputs that might corrupt our data. The data in the database should also have a valid form, for example, we shouldn’t let the user type his email like this (!#$$), so we have to do Form sanitization & validation.

For that open the functions.php file and let’s create some functions that will help us validate the form.

function clear($text) {
    $text = strip_tags($text);
    $text = trim($text);
    $text = htmlspecialchars($text);
    return $text;
}
function clearText($text) {
    $text = clear($text);
    $text = str_replace(" ", "", $text);
    $text = strtolower($text);
    $text = ucfirst($text);
    return $text;
}
function clearEmail($email) {
    $email = str_replace(" ", "", $email);
    $email = clear($email);
    return $email;
}

In the first function clear we should pass any input value to it to remove any white spaces or HTML code and tags.

For the second one, you’ll need it in inputs like first name or last name, so use the str_replace php function to replace spaces with nothing, to have your name without spaces in it. We also want the names to have the same structure, as the first character is an uppercase and the rest are lowercase characters. So we use strtolower method on the passed name then use ucfirst to make the first character uppercase and then return the text.

In the clear email function, we want to only remove spaces, for now, more validation on the email will be covered in another function later.

We now need to write more functions that take care of things such as how many characters the user is allowed to write or if the emails provided match or whether the email already exists in the database.

Validate First and Last name

Pass the first name and the last name to the functions that validate the first name and validates the last name and they need to be between 2 and 25 characters, we then create an array for errors and make sure that the function returns this array at the end.

Validate Email

The validateEmail function takes 3 parameters

  • $con the variable created in the config file and holds the connection to the database.
  • The email that the user entered.
  • The confirmation email that the user entered

Check if the two emails match, if not, push an error to the errors array as an html span element, because that will be the output in the html.

Use the filter_var method with FILTER_VALIDATE_EMAIL to make sure that what the user submits is a valid email, if not push another error to the errors array.

Use the $con variable to connect to the database and find an email with the same provided email, if it’s found that means the email already exists in the DB, so push another error and finally return the errors array.

Validate Password

In password validation you want to make sure that the two provided passwords are the same. if not push an error.

Use regular expressions to make sure that the password only contains numbers and letters.

You can find more on regular expressions here :

Finally make sure that the password is between 5 and 30 characters and returns the errors array.

Now let’s use our validation methods in the register.php :

For every function that returns an errors array, we check if that array is empty, which means no error came from this function. if not we loop through this array and push the errors in it to the errors array we created in register.php to have all errors in one place.

Now let’s output errors to the user right above the form element :

<?php 
   if(isset($_POST["register"])) {
      if(!empty($errors)) {
          echo "<div class='errors'>";
          foreach($errors as $error) {
             echo $error;
          }
          echo "</div>";
      }
   } 
?>

Check if the user submitted the form and if we have errors (errors array is not empty) echo div with class errors to hold all errors and loop through all the span elements we pushed to the errors array, then echo the div closing.

Sign Up Users

What if we have no errors ?

That means the form is valid and it’s time to sign the user up

if(empty($errors)) {
     $hashedPassword = password_hash($password, PASSWORD_DEFAULT);
}

After using the validation functions in register.php, check if the errors array is empty, if it’s empty we will then sign up the user.

The first thing we need to do is hash the password because you can’t save users passwords in the database as plain text, it’s not secure to do that.

PHP has a method called password_hash, it takes a password and hashes it, to find out more about password hashing and how to create secure login with PHP; Read this article

Pass to the second parameter PASSWORD Default.

$query = $con->prepare("INSERT INTO users 
(first_name, last_name, email, pw, phone)
VALUES(:fn, :ln, :em, :pw, :ph)");

Prepare our SQL statement to insert the user data to MYSQL.

fn, ln, em, pw, ph are placeholders that will be replaced by variables, because it’s not secure to concatenate PHP variables right in your SQL statement string.

$query->bindParam(":fn", $firstName);
$query->bindParam(":ln", $lastName);
$query->bindParam(":em", $email);
$query->bindParam(":pw", $hashedPassword);
$query->bindParam(":pp", $phone);
$query->execute();

Use PDO bindParam method to bind the placeholders to your variables then execute the query.

Now, the user data should be inserted in the database, let’s make sure:

if($query->rowCount() == 1) {
    $_SESSION["userLoggedIn"] = $con->lastInsertId();
}

If the rows returned from the query is 1 that means the data is successfully inserted.

Save the id of the user you just inserted by using the PDO lastInsertId method in the $_SESSION global variable to use it in the verification page so we know that the user has logged in.

Use Africa’s Talking SMS API

This is how it works again, the user submits data to the server, the server validates the data and if the data is valid sends a request to the Africa’s Talking API, then the Africa’s Talking API sends an SMS message to the phone number we sent to it, the SMS contains a code we generated on the server then the client will have to enter that code to log in.

Let’s start by creating a function in the function.php file to send a request to the Africa’s Talking API.

I’ll use PHP curl library to send http requests.

function sendMessage($msg, $phone) {
    $curl = curl_init();

    curl_setopt($curl, CURLOPT_URL,
"https://api.sandbox.africastalking.com/version1/messaging");
}

This function “sendMessage” takes two parameters, first the message we want to send and the phone number.

Start curl by using the curl_init method and save it in a variable, call it whatever you want, for me I am going to call it $curl.

Now we need to set some curl options by using curl_setopt method, one of these options is CURLOPT_URL and it is the URL we want to send the request to and we want to set it to the Africa’s talking SMS API endpoint which is “https://api.sandbox.africastalking.com/version1/messaging”.

Other curl options:

$data = 'username=sandbox&to='.$phone.'&message='.$msg.'&from=34020';

curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HTTPHEADER, 
array('Accept: application/json',
'Content-Type: application/x-www-form-urlencoded', 
'apiKey: 85c6794bf305950074a31a02d86b98401eaf3057a02dbec6330166535e923961'));
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);

First, create a variable called data and it is a string that will be sent as the request body and it has some URL variables :

  • username: always sandbox when testing
  • to : concatenate the $phone variable passed to the function with the string.
  • message: concatenate the $msg variable passed to the function with the string.
  • from: It is the short code we created before.

Note: you can send the message to more than one user just separate different phone numbers with “,” instead of $phone variable but in this case, we are sending it to a single user.

Curl outputs the response directly by default, and we want to store it in a variable for further processing not outputting it to the user, so that’s exactly what this CURLOPT_RETURNTRANSFER does.

We have to set some http headers. The Africa’s Talking API returns json response so we need to accept json (Accept: application/json).

Content-Type is the content we send and it is url encoded so it’s value = application/x-www-form-urlencoded.

apiKey: is the api key we generated before.

CURLOPT_POST is the method we are using to make this request and it should be POST so set it to 1.

CURLOPT_POSTFIELDS is the data we need to send to the Africa’s Talking API, set it to the data variable created before.

$apiResponse = curl_exec($curl);

Execute curl by using curl_exec method and save the response in a variable.

Now that response should look like this :

{
    "SMSMessageData": {
        "Message": "Sent to 1/1 Total Cost: KES 0.8000",
        "Recipients": [{
            "statusCode": 101,
            "number": "+254711XXXYYY",
            "status": "Success",
            "cost": "KES 0.8000",
            "messageId": "ATPid_SampleTxnId123"
        }]
    }
}

This is json response and it means that the message is sent and everything is ok now we need to process that response to know that the message is sent and redirect the user to the verification page.

$response = array();
$phpApiResponse = json_decode($apiResponse);

$response["apiStatus"]= $phpApiResponse->SMSMessageData->Recipients[0]->status;
$response["apiStatusCode"] = $phpApiResponse->SMSMessageData->Recipients[0]->statusCode;

Create an array and call it response to customize it and return it at the end of the function.

We need to convert that JSON response to a PHP array to process it with PHP and that’s what json_decode does.

Now $phpApiResponse holds a PHP array of the response came form the Africa’s talking API.

We need two important pieces of information from this array, search in the $phpApiResponse array for status which should be “Success” and save it to our response array with a key called apiStatus, the second is statusCode which should be 101, search in the $phpApiResponse array for statusCode and save it in our response array in a key called apiStatusCode.

$response["curlStatusCode"] = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close ($curl);

return $response;

All of the previous data will let us know if the message is sent from the Africa’s Talking API to the user’s phone number, we also need to know if our request is sent to the API or not by using curl_getinfo and pass our $curl variable and CURLINFO_HTTP_CODE and that should be 201 which means success.

Now the response array has 3 values :

  • response[“apiStatus”] (should be “Success”)
  • response[“apiStatusCode”] (should be 101)
  • response[“curlStatusCode”] (should be 201)

Close curl and return the response.

Send Message

Now the sendMessage function is finished let’s use it in the register.php :

if($query->rowCount() == 1) {
    $_SESSION["userLoggedIn"] = $con->lastInsertId();
    $_SESSION["confirmationCode"] = rand(111111,999999);
    $messageText =
"Your%20Account%20Verification%20Code%20is%20".$_SESSION["confirmationCode"];
$msg = sendMessage($messageText, $phone);

If the user data is inserted, we save the user id in a $_SESSION variable and generate a random number of 6-digits using php rand() function and save it in another $_SESSION variable.

Let’s build the message text, use %20 for spaces because remember our request body is URL encoded. and append the code generated by rand() to the message string.

Call sendMessage function and save it in a variable and pass the message text to it and also pass the phone number of the user, remember it’s saved in the $phone variable because we are still in the register.php page and we have all the data that the user submitted.

if($msg["curlStatusCode"] == 201) {
    if($msg["apiStatus"] === "Success" && $msg["apiStatusCode"] == 101) {
         header('Location: http://localhost/auth/checkpoint.php');
         die();
     }
}
else {
   $errors[] = "<span class='error'>Something Went Wrong Please Try Again</span>";
}

Check if the curl status code is 201, which means the request is sent to the Africa’s Talking API, inside that check if API status is success and the API status code is 101.

If it’s not push an error to the errors array with (“Something Went Wrong Please Try Again”).

Now everything is good and the message should be sent to the user.

creating a new user

Now if you hit the register button a message should be sent to the number you entered.

Open the Africa’s Talking simulator from the side menu under tools and type the number which will receive the message.

using africa’s talking simulator
the code is sent

Now we should redirect the user to the checkpoint.php page to enter the code.

Create a similar html form but with one input that will hold the verification number.

Make sure the form submission is on the same page, at the top of the page open PHP tags and let’s process the form.

<?php
require_once("config.php");
require_once("functions.php");

if(!isset($_SESSION["userLoggedIn"]) || !isset($_SESSION["confirmationCode"])) {
    exit();
}
else {
    if(isset($_POST["codeSubmit"])) {
        if(isset($_POST["code"])) {
            if($_POST["code"] == $_SESSION["confirmationCode"]) {
                  //processing
            }
        }
    }
}

At first we need to do multiple checks as follows:

  • We check if the userloggedin, and if the key in the $_SESSION global variable is set and that is the id we set before.
  • If not we exit() the page because we don’t want anyone to go to this page without trying to login with the correct data before.
  • We also check if the confirmation code in the global $_SESSION variable is set because that is everything this page about!.
  • If the userloggedin and the confirmation number is set we want to check if the code is submitted and if the code input is also not empty.

If so now we are going to log in the user.

Login

One last check that we need to do is check if the code that the user entered matches the code we saved in the global $_SESSION variable.

if($_POST["code"] == $_SESSION["confirmationCode"]) {
    try {
         $accessToken = base64_encode(bin2hex(openssl_random_pseudo_bytes(24)));
    }
}

Create a random access token by using these methods:

  • openssl_random_pseudo_bytes : Generates a string of pseudo-random bytes.
  • bin2hex: Converts binary data into hexadecimal representation.
  • base64_encode: Encodes data with MIME base64.

That will give us a long encoded string.

Now it’s time to create a session for that user in the database :

$query = $con->prepare("INSERT INTO sessions (user_id, access_token) VALUES (:id, :token)");
$query->bindParam(":id", $_SESSION["userLoggedIn"]);
$query->bindParam(":token", $accessToken);
$query->execute();

The values are the user id we already have as $_SESSION[“userloggedIn”] and the accessToken we just generated.

if($query->rowCount() == 1) {
     session_unset(); 
     setcookie("access_token", $accessToken, time() + (86400 * 30), "/");
     header('Location: http://localhost/auth/index.php');
     die();
}

Check if the session is created successfully and use session_unset() to delete all the $_SESSION variables you created because the user may log out in the same session, and enter the code form and that shouldn’t happen.

Set a cookie with the name “access token” and its values should be the $accessToken we generated with an expiry time of one day which will be sent with each request, and that’s how we know the user is logged in.

Redirect to the index.php

Check if the user logged in

At the index.php we should check if the user logged in or not before we render the page.

  • Check if the cookie is set and get the access token.
  • Find a session in the database with the same access token in the cookie.
  • Get the user id of this session and fetch its data.

Login Page

All of that was logging in after we register a new user, now we need to have a login page for the already created users.

In the login.php page create html form like we did before in register.php but with two inputs email and password.

login page

Submit the form to the same page and at the top of the page let’s start to process the form data.

Collect the email and password fields, use the validation functions and check if that email exists in the database.

If it exists use password_verify and pass it to the password that the user already entered and the hashed password in the database.

If email or password is wrong push an error to the errors array with (“your email or password was incorrect”) and don’t specify which one was.

If password_verify returned true save the id of the user in $_SESSION[“userLoggedIn”] and generate a confirmation code by rand() again and save it in $_SESSION[“confirmationCode”] just like before.

Send a message and check if the curl status code is 201 and the if API status code is 101 and the API status is “Success” then redirect the user to the checkpoint page.

Logout

To log the users out, we have to do two things :

First we remove the cookie we set “access token” from the user browser.

Then we need to remove the user session from the database.

Let’s dig in.

if (isset($_COOKIE['access_token'])) {
    unset($_COOKIE['access_token']); 
)
if (isset($_COOKIE['access_token'])) {
    $query = $con->prepare("DELETE FROM sessions WHERE access_token = :ac");
    $query->bindParam(":ac", $_COOKIE['access_token']);
    $query->execute();
}

Resend Code Functionality

One last thing we can add to this project to make it better is to let the users resend the code in case they haven’t received the message.

We need to do that with javascript because it’s not good for users experience to reload the page every time they try to resend the code.

But first we need to add parts to the code:

In the register.php add:

$_SESSION["phone"] = $phone;

In the login.php add:

$_SESSION["phone"] = $sqlData["phone];

To access the phone in the checkpoint.php page.

In the checkpoint.php add resent button right after the submit button :

<button id="resend">Resend</button>

In the same page add javascript after the opening body tag :

<script>
const phone = "<?php echo $_SESSION["phone"]; ?>";
const randomCode = Math.floor(100000 + Math.random() * 900000);
const msg = "Your%20Account%20Verification%20Code%20is%20"+randomCode;
const data = 'username=sandbox&to='+phone+'&message='+msg+'&from=34020';

fetch("https://api.sandbox.africastalking.com/version1/messaging", {
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/x-www-form-urlencoded',
      'apiKey: '85c6794bf305950074a31a02d86b98401eaf3057a02dbec6330166535e923961'
    },
    method: "POST",
    body: data
})
.then(res => {
     console.log(res) 
})
.catch(err => { 
    console.log(err) 
});

const newCode = new formData();
formData.append("newCode", randomCode);

fetch("/auth/checkpoint.php", {
    method: "POST",
    body: newCode
})
.then(res => {
     console.log(res) 
})
.catch(err => { 
    console.log(err) 
});

</script>

It’s the same logic as before but in javascript, we have to send two requests, one to the Africa’s Talking API to send a new message to the user and the other to the same page (checkpoint.php) to inform PHP that there is a new confirmation code.

Now we have to listen for that in PHP, in checkpoint.php before all the if statements, we check if the new code is set and if so, we change the session variable of the confirmation code to the new code.

if(isset($_POST["newCode"])) {
    $_SESSION["confirmationCode] = $_POST["newCode"];
}

Conclusion

We have covered a lot of topics in this article like Form sanitization and validation, web scraping using curl, token based authentication and more.

You could have this setup in any programming language or application, the idea is similar just use your style and make it fit your style and your case.

Africa’s Talking API has an SDK for PHP, it is also fine to use it. Read more about Africa’s talking API’s here.

If your website is a single page application and you use react or angular or any other javascript frameworks you can save the access token in the browser’s local storage and send it with every request you make to the server, but be careful and read about XSS and CRSF attacks and find a way to store your token safely.

You can also use JSON web tokens to verify users without creating session tables and query the database for every single request, have a look at it here

You can find the source code in this Github repository

0 Shares:
You May Also Like