With the recent string of high profile break-ins (hacks) at Sony and other companies, it's time you took a second look at the security of your website. Two-factor authentication is a step in the right direction to securing your website from attackers. In this tutorial, we'll take a look at implementing this in our CodeIgniter application.
What is Two-Factor Authentication?
Two-factor authentication requires users to use something they know, such as a username and password, and something they have, like a phone, to log in.
Lately, companies like Google and Facebook have been rolling out two-factor authentication for their users. Other services, like MailChimp, are using alternate forms of two-factor authentication to help thwart attackers. But still, what specifically is two-factor authentication?
Two-factor authentication is a way of proving your identity based on your username and password as well as a physical device that you can carry with you.
Duo's mobile application supports push notifications for authentication!
This makes it much harder for crooks to steal your identity, since they will need access to your phone or hardware token - not just your login credentials.
Lucky for you, Duo Security offers a free two-factor service ideal for anybody looking to protect their website.
Not only is Duo free, but it's full of features. They let you authenticate in a variety of ways, including:
- Phonecall authentication
- SMS-based tokens
- Mobile app token generator
- Push-based authentication
- Hardware tokens available for purchase
Step 1: Setup
Setup CodeIgniter
If you haven't worked with CodeIgniter before, I highly recommend that you check out theCodeIgniter From Scratch series first.
This tutorial will build on the Easy Authentication with CodeIgniter tutorial. This tutorial will be much easier for you to understand if you complete the previous tutorial before continuing. We will be using the files from that tutorial as our starting point.
Please verify that your config/autoload.php
has the following helpers being loaded.
$autoload['helper'] = array('url', 'form');
Create an Account
Head over to Duo Security, and sign up for an account.
They offer a free plan for open source projects, and for sites with less than 10 Duo users (A Duo user is someone who will be using the two-factor authentication to log in).
Create an Integration
After registering, you need to log in to Duo and create an integration. Once logged in, click on integrations on the side panel to pull up the integrations page. From there, click the "New Integration" button.
Make sure the integration you create is a Web SDK integration. This will allow you to use their PHP API with CodeIgniter.
The integration name is only used on Duo's website. This is just a way for you to identify your integration. Duo has a getting started guide that explains how to set up an integration.
Download the Web SDK
In addition to setting up an integration, you'll need to download the Web SDK.
There are two pieces of the SDK that we'll need: A PHP file (duo_web.php) and a JavaScript file. Please note that the JavaScript has a jQuery dependency and the bundled JavaScript comes with jQuery.
We will be using the bundled JavaScript, but note that if you aren't, jQuery must be loaded before the JavaScript provided by Duo. For more information on the Web SDK and how it works, view the documentation at http://www.duosecurity.com/docs/duoweb
Step 2: Modifications for Security
After finishing the Easy Authentication with CodeIgniter tutorial, you should have a basic login system in place.
Better Hashing
As a first step, we'll add a strong hashing function to the database. Openwall has a nice PHP hashing library that implements bcrypt
. The latest version of phpass is 0.3 at the time of this article.
Go ahead and download phpass from their website: http://openwall.com/phpass/. After downloading and unarchiving the folder, you'll need to place that in your libraries folder.
We'll now need to make our own library file as an interface to phpass. Create a new library file, named password.php.
Our library will have two functions:
- a hash function to rehash the old passwords
- a check_password function to compare hashes with plaintext passwords.
require_once('phpass-0.3/PasswordHash.php'); class Password { var $hasher; function __construct() { // 8 is the hash strength. A larger value can be used for extra security. // TRUE makes the passwords portable. FALSE is much more secure. $this->hasher = new PasswordHash(8, TRUE); } function hash($pass) { return $this->hasher->HashPassword($pass); } function check_password($pass, $hash){ return $this->hasher->CheckPassword($pass, $hash); } }
The require_once()
statement ensures that we'll be able to use the PasswordHash
class from phpass.
PasswordHash
takes two arguments in its constructor:
- a number indicating hash strength
- a boolean as to whether or not the passwords should be portable.
In this case, we're going to make our passwords portable.
This essentially means that the hash isn't as strong, but if we ever need to switch servers or move the database, we can make a copy. If we don't use a portable hashing scheme, we run the risk of having all of our users create new passwords if the database is moved.
Note: Even though we're implementing a stronger hashing function, you should still require users to have a strong password.
Altering the Admin Model
public function verify_user($email, $password) { $q = $this ->db ->where('email_address', $email) ->limit(1) ->get('users'); if ( $q->num_rows > 0 ) { $result = $q->row(); $this->load->library('password'); //Make sure the hashes match. if($this->password->check_password($password, $result->password)){ return $result; } } return false; }
Previously, we were selecting the user by the email address and the hashed password. Now we're pulling the user from the database based on the email address. This means we have to validate the password before we can return the user.
After we've pulled the user from the database, we'll load the password library we just created and verify that the entered password matches the hashed password.
If the two passwords match, we proceed to return the user, otherwise, we return false
.
Be sure to use the password library to hash a new password for yourself. The passwords in your database will be invalid now!
Altering the Users Table
We're going to add a basic permission field to the database. This permission will determine whether or not the user will log in with two-factor authentication.
We need to add a column to the users table for two-factor permissions. You can do this via phpMyAdmin, or by running the following SQL.
ALTER TABLE users ADD two_factor_permission BOOLEAN NOT NULL;
A value of 1 in the permission column will make the user use two-factor authentication.
The SQL will add a boolean column to the users table. We'll use this to require users to use two-factor authentication, or to bypass it.
If you did this right, you should see a new column in your users table. You'll then need to update a current record, or insert a new record that sets two_factor_permission
to true
(1).
If this column is set to false
(0), the user will be able to bypass two-factor authentication. This is ideal for users who don't need the same level of security as an administrator.
Step 3: Using the Permission Setting
We'll need a way to bypass secondary authentication, as well as a way to insert a secondary authentication step to the login process.
Bypassing Secondary Authentication
First off, we'll need a way to bypass secondary authentication. This means that we'll need to inspect the user in the admin controller.
if ( $res !== FALSE ) { $_SESSION['username'] = $res->email_address; if ( $res->two_factor_permission ) { $this->_second_auth($res->email_address); return; } else { $_SESSION['logged_in'] = TRUE; redirect('welcome'); } }
This checks to see if the user should be logged in with our two-factor system.
If the user should be using two-factor authentication, we want them to go to the secondary authentication page without logging them in.
Instead of redirecting the user, we can call the _second_auth()
function and have that load the page. The "return
" statement avoids loading the login form.
We've created a new session variable logged_in
which we will use to verify that the user has been logged in. This means we need to make some changes to the redirects.
Fixing the Redirects
There are two redirects that need to be changed: the first is in the index function of the admin controller.
if ( isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === TRUE ) { redirect('welcome'); }
The other is in the welcome
controller. We need to make sure that the user isn't logged in before redirecting.
if ( !isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== TRUE ) { redirect('admin'); }
Inserting Secondary Authentication
Now we need to handle the secondary authentication.
In the admin/index
function, we call _second_auth()
, so let's write a basic function.
public function _second_auth ($username) { echo "Welcome $username, you are looking at a secondary authentication page."; }
Step 4: Constructing the View
Traditional authentication systems treat logins as a single step process.
Duo gives us a bit of JavaScript and HTML to inject between the two steps. This means we'll need to create a view with the required code.
Let's create a new view, called second_auth.php
in the views
folder. We'll need to insert the the iframe and JavaScript provided by Duo to make it work.
You should create a page with the basic HTML structure. The following can be placed in the body:
<iframe id="duo_iframe" width="100%" height="500" frameborder="0"></iframe> <script type="text/javascript" src="/path/to/Duo-Web-v1.js" ></script>
In a typical setup, you would keep all your JavaScript in a resource folder. Here, we've put a resources
folder in the root of our site, with a 'js
' sub-folder that contains the Web SDK JavaScript file.
Our src
will look like:
src="<? echo base_url(); ?>resources/js/Duo-Web-v1.js"
We also need to add this second bit of JavaScript.
<script type="text/javascript" > Duo.init({ 'host':'<?php echo $host; ?>', 'post_action':'<?php echo $post_action; ?>', 'sig_request':'<?php echo $sig_request; ?>' }); </script>
We'll generate this data from the controller shortly.
Inserting a Form
If you followed the previous tutorial, you should have configured CodeIgniter to protect against CSRF.
Because the JavaScript will post data to our controller, CodeIgniter will be looking for the CSRF token. If we don't have this token, we'll get an error.
The JavaScript we're using will submit a form with the id "duo_form
". All we need to do is create it.
echo form_open('admin', array('id'=> "duo_form")); echo form_close();
By using the form class, CodeIgniter will automatically inject the token. When the form gets posted, CodeIgniter will find the token and let us continue.
Step 5: Preparing the Data
Back in the admin
controller, we need to generate some data in our _second_auth()
function.
The host is the API URL that you were provided with when you signed up with Duo. This URL should look something like api-xxxxxxxx.duosecurity.com
(where 'xxxxxxxx' is a unique string tied to your Duo account).
$data['host'] = "api-xxxxxxxx.duosecurity.com";
Remember to replace the host with your specific URL. The above URL will not work.
The post action is the URL that will be handling the response once the user has attempted to authenticate with Duo.
We'll create another function in the admin controller to handle the post-back. For now, we'll name the function process_second_auth()
.
$data['post_action'] = base_URL() . "admin/process_second_auth";
Loading the PHP Web SDK
Make sure you rename 'duo_web.php' to 'duo.php' to avoid CodeIgniter errors.
If you haven't downloaded the latest copy of duo_web.php, you can get it from Duo's Web SDK GitHub page.
Because the Web SDK comes as a PHP class, we can rename it to "duo.php
" and place it in our "application/libraries" folder.
After you've placed the file into the libraries
folder, we can load it in our controller.
public function _second_auth($username) { $this->load->library('duo'); $data['host'] = "api-xxxxxxxx.duosecurity.com"; $data['post_action'] = base_URL() . "admin/process_second_auth"; echo "Welcome $username, you are looking at a secondary authentication page."; }
Generating the Signed Request
To understand how to generate sig_request
, you must understand what we're generating.
The
$akey
variable needs to be at least 40 characters long, otherwise the Duo library will return an error!
The Duo Web SDK creates two signed tokens, one with the secret key they give you, another with an application key that you make up.
is a combination of the two tokens.sig_request
By creating your own application key you will have a second layer of security. An attacker will need both the secret key from Duo and your personal application key to spoof a token.
Now we'll generate the 'sig_request'. Duo will provide you with an integration key and secret key when you create an integration.
Make sure to replace the below text with the integration key and secret key given to you. You need to make up your own secret key. It needs to be at least 40 characters long, and should be as random as possible.
public function _second_auth($username) { $this->load->library('duo'); // Duo Integration Key $ikey = "REPLACE WITH YOUR DUO INTEGRATION KEY"; // Duo Secret Key $skey = "REPLACE WITH YOU DUO SECRET KEY"; // Personal Application Key $akey = "CREATE AN APPLICATION KEY"; $data['host'] = "api-xxxxxxxx.duosecurity.com"; $data['post_action'] = base_URL() . "admin/process_second_auth"; $data['sig_request'] = $this->duo->signRequest($ikey, $skey, $akey, $username); echo "Welcome $username, you are looking at a secondary authentication page."; }
Duo's signRequest()
will generate the tokens and return them as a string to pass to sig_request
.
Now we need to load the data into the view we created earlier.
public function _second_auth($username) { $this->load->library('duo'); // Duo Integration Key $ikey = "REPLACE WITH YOUR DUO INTEGRATION KEY"; // Duo Secret Key $skey = "REPLACE WITH YOUR DUO SECRET KEY"; // Personal Application Key $akey = "CREATE AN APPLICATION KEY"; $data['host'] = "api-xxxxxxxx.duosecurity.com"; $data['post_action'] = base_URL() . "admin/process_second_auth"; $data['sig_request'] = $this->duo->signRequest($ikey, $skey, $akey, $username); $this->load->view('second_auth', $data); }
If you attempt to login now, you should see this page:
This is the enrollment form. You can enroll your cell phone here, but we don't have anything to process the secondary authentication so it won't log you in.
If you don't see anything at all, view the page source for error messages. Any errors with the data will be displayed in the <script>
tag.
If it says, "Access Denied", make sure that you have entered the integration and secret key from Duo Security's website.
Step 6: Processing Secondary Authentication
We've set up our post action to be admin/process_second_auth
, so we need to create a process_second_auth()
function.
public function process_second_auth() { if ( isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === TRUE ) { redirect('welcome'); } }
Since this function will have its own URL, we need to redirect logged in users.
We have to load the Duo library again to validate the data.
$this->load->library('duo'); // Same keys used in _second_auth() $ikey = "REPLACE WITH YOUR DUO INTEGRATION KEY"; $skey = "REPLACE WITH YOUR DUO SECRET KEY"; $akey = "REPLACE WITH YOUR APPLICATION KEY";
We'll be needing the same $ikey, $skey
and $akey
from the _second_auth()
function to validate the posted data.
The JavaScript posts back a sig_response
from the Duo servers.
$sig_response = $this->input->post('sig_response'); $username = $this->duo->verifyResponse($ikey, $skey, $akey, $sig_response);
Once we've pulled sig_response
from the posted data, we'll run it through the verifyResponse()
function. This will return NULL if the tokens don't match, or a username if they are valid.
if ( $username ) { $_SESSION['logged_in'] = TRUE; redirect('welcome'); } else{ redirect('admin'); }
Lastly, we'll verify that a username was returned, and finish logging them in by setting the value of $_SESSION['logged_in']
to true.
All together the function should look like this:
public function process_second_auth() { if ( isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === TRUE ) { redirect('welcome'); } $this->load->library('duo'); // Same keys used in _second_auth() $ikey = "REPLACE WITH DUO'S INTEGRATION KEY"; $skey = "REPLACE WITH DUO'S SECRET KEY"; $akey = "REPLACE WITH YOUR APPLICATION KEY"; $sig_response = $this->input->post('sig_response'); $username = $this->duo->verifyResponse($ikey, $skey, $akey, $sig_response); if ( $username ) { $_SESSION['logged_in'] = TRUE; redirect('welcome'); } else{ redirect('admin'); } }
Now you should be able to login with two-factor authentication, go ahead and try it out!
Conclusion
Hopefully you've set up your own two-factor authentication system for CodeIgniter!
What else could you do? There is plenty to do in terms of security, but the largest improvement would be tracking user actions.
A good security system isn't only secure: it will help you identify where a vulnerability came from. You should keep track of login attempts, and other actions to make identifying attackers easier.
Thanks for reading! If you're having trouble, leave a post in the comments.
Comments