Click here to Skip to main content
15,868,139 members
Articles / Programming Languages / PHP

Two Factor Authentication with JavaScript and HTML5

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
4 Jan 2013CC (ASA 3U)4 min read 54.1K   1K   32  
Compact One Time Password Generator (RFC6238) written in javascript

Introduction

In one of previous articles I've briefly demonstrated how OTP value is calculated

(http://www.codeproject.com/Articles/502240/Mysterious-google-two-step-authentication-in-debug) as well as shared compact set of PHP classes and libraries that allow OTP calculation in your server side code (https://github.com/Voronenko/PHPOTP). This approach assumed that you want your customer to use google authenticator tool in order to get the OTP value. For example, such approach is used by LastPass password service. But what if you want some customized UI for your OTP token generator? You might need want this UI to be branded with your application/service skin, etc...

This article will show how to deal with it.

Background

The OTP token generator program is usually an application on some mobile device: IOS or Android based. Both platforms support HTML5 very well, and this allows us to implement our OTP generator in pure HTML/Javascript, as a single page application.

Challenges to solve

  • Implement OTP token generation in javascript
  • Implement UI and logic to change the code each 30 seconds
  • Ensure that implemented solution is capable working in offline.

Let's go step by step.

OTP token generation in javascript.

As you might recall from by previous article we need following ingredients for our algorithm:

  • base32 conversion library,
  • sha1 cryptho algorithm implementation
  • HMAC & OTP algorithm implementation if present.

We prefer libraries with MIT or LGPL licenses in order to be able to license our solution as free for commercial use. For base32 implementation I definitely recommend nibbler library: http://www.tumuski.com/2010/04/nibbler/. It has minor glitches with padding, but the issue is rare and community patch is present at project page.

For Sha1 algorithm and other cryptho algorithms in javascript I recommend CryptoJS library by google http://code.google.com/p/crypto-js/. CryptoJS is a growing collection of standard and secure cryptographic algorithms implemented in JavaScript using best practices and patterns. They are fast, and they have a consistent and simple interface. Library is still supported and developed at the present moment. We can find there both sha1 and hmac implementations - brilliant!

OTP algorithm: Javascript becomes quite popular now: for example we can use this NodeJS module as a basis https://github.com/guyht/notp/ . The issue is that module designed specifically for NodeJS environment, thus all not important dependencies need to be eliminated in order to allow this module work in a browser environment. MIT license allows us to do such modifications.

In this case I had to do a port of the Buffer object, use nibbler implementation for base32 and emulate NodeJS crypto module (http://nodejs.org/api/crypto.html) for HMAC calculation like this:

JavaScript
var cryptoFAKE = {
<pre>   createHmac:function(algorithm, key) {
      var _key = key.value();
      return new HMacBasicImpl(_key);
   }
};

In a result we have adopted NOTP class that provides way to calculate one time password:

JavaScript
Notp.getTOTP (args, err, cb) 

Arguments: object with required field K - private key string

UI

For UI we have to answer on following questions:

  • Where will we store the key (referred as a CLUE in this section)
  • How will we program the UI.

Fortunately HTML5 allows web pages to persist their data on the client device -
DOM Storage https://developer.mozilla.org/en-US/docs/DOM/Storage.

JavaScript
var CLUE= localStorage.getItem('CLUE');
  if (typeof(CLUE)=="undefined") {
     CLUE=null;
  }

For Single Page Applications my favorite library is KnockoutJS. It allows to concentrate on developing logic,
and outsource binding to html elements on Knockout markup.
Model: has three properties: clue (the key), current token and boolean property that informs whenever clue is present or not. Only one method - UpdateToken - calculates OTP and updates model properties.

JavaScript
var Model = {
     existsclue:ko.observable((CLUE!=null)),
     clue:  ko.observable(CLUE),
     token: ko.observable('XXXXXXX'),
     notp: new Notp(),
     UpdateTokenCallback: function(code) {
       this.token(code);
     },
     UpdateToken: function(){
        var args = {
       K : CLUE
      };
          this.notp.getTOTP(args,
      function(err) { alert(err); },
              Model.UpdateTokenCallback.bind(Model)
          );
     }
  }

View:

The good thing is that you are not limited in design. You can change look and feel of yours OTP application with images, HTML and CSS : add your company logo, corporate fonts, etc...

HTML
<header aria="company logo">
   <div class="center"><img src="im/logo.gif"/></div>

</header>
<div id="main" role="main" class="center">

   <p data-bind="text:token" id="code">LOADING...</p>

   <p data-bind="text:clue" id="clue">CLUE</p>(<span data-bind="text:existsclue"></span>)
   <p data-bind="visible:(!existsclue())" id="syncro">
      <a href="setup.php">Please navigate to this link to setup your device!</a>
   </p>


   <p>
      <a href="#" onclick="window.applicationCache.update()">Debug: cache.swapCache()</a>
   </p>

</div>

we are detecting whenever CLUE is present in localstorage, and if not - propose our customer to setup ("Please navigate to this link to setup your device"). In a real scenario we might want person to login using some secure method, but for demo purposes we use simple approach: put clue in the session and display QR code that can be grabbed by client device - i.e. customer needs just to scan QR code in order to get your OTP application configured.

PHP
<?php

require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR .'rfc6238/base32static.php');
session_start();
$secretcode = '12345678901234567890';
$_SESSION['secretcode'] = $secretcode;
;


$url = "http://".$_SERVER["HTTP_HOST"].str_replace(basename($_SERVER["SCRIPT_NAME"]),"",$_SERVER["SCRIPT_NAME"])."setupinitdevice.php?PHPSESSID=".$_COOKIE["PHPSESSID"];


?>
<h1> Please navigate by link below to setup 2 factor auth </h1>
<img src="setupqrcodeimage.php?PHPSESSID=<?php print $_COOKIE["PHPSESSID"]?>" />
<br/>

<a href="<?php print $url?>">This is the same link for debug</a>

Once link is opened on device using QR Code or in a different way, - application on a device is ready to use.

<?php
  session_start();
  $secretcode = $_SESSION['secretcode'];
  if (empty($secretcode)) {
    die('Sorry, device is not supported /'.$_COOKIE["PHPSESSID"].'/ while'.session_id(). '  AND #'.$_SESSION['secretcode'].'#');
  }


  $url = "http://".$_SERVER["HTTP_HOST"].str_replace(basename($_SERVER["SCRIPT_NAME"]),"",$_SERVER["SCRIPT_NAME"])."index.html";
?>
<html>
  <head>
    <meta http-equiv="refresh" content="2;url=<?php print $url?>">
    <script type="text/javascript">
        if (!window.localStorage) {
           alert('Sorry! this device is not supported');
        }

        localStorage.setItem('CLUE', '<?php print $secretcode?>');
        alert(localStorage.getItem('CLUE'));
    </script>
  </head>
  <body>
    <a href="<?php print $url?>">If this page did not redirect you, press here</a>
  </body>
</html>

Offline mode

Our customer shouldn't required to get internet access each time when he needs OTP value. Here is the place
when we make use of another html5 technology: offline cache https://developer.mozilla.org/en-US/docs/HTML/Using_the_application_cache. Let's enable our application for offline by declaring manifest:

HTML
<html class="no-js" lang="en" manifest="appcache.php"> 

In real scenario you might want manifest file to be compact, but again for purposes of demo let's include all project scripts into offline mode

PHP
<?php
  header('Content-Type: text/cache-manifest');
  echo "CACHE MANIFEST\n";

 $hashes = "";

  $dir = new RecursiveDirectoryIterator(".");
  foreach(new RecursiveIteratorIterator($dir) as $file) {
    if ($file->IsFile() &&
       ($file != "./appcache.php") &&
       (pathinfo($file, PATHINFO_EXTENSION)!='appcache') &&
       (substr($file->getFilename(), 0, 1) != ".")
       )
    {
      echo $file . "\n";
      $hashes .= md5_file($file);
    }
  }
  echo "# Hash: " . md5($hashes) . "\n";

?>

Code in action

I will demonstrate the code in series of the screenshots.

Clue is not defined yet

CLUE setup process

CLUE setup process

CLUE is stored, OTP working

CLUE is stored, OTP is generated

Can be added to home screen of the mobile device

Can be added to home screen of the mobile device

And used in offline mode

And used in offline mode

Important note on running demo in offline mode

Please ensure that cache manifest is served with correct mime type:

AddType text/cache-manifest appcache
AddType text/cache-manifest .appcache

If you cloned repository - neither adjust appcache.php code, or remove .git folder with subfolders

Code can be downloaded from GitHub: https://github.com/Voronenko/JSOTP

Summary

I really hope that more secure two factor authorization will be widely used across web sites. Ideas shared in this article could allow developers to have more control on look and feel of customer OTP application as well as target more devices capable running HTML5 scenarios.

License

This article, along with any associated source code and files, is licensed under The Creative Commons Attribution-Share Alike 3.0 Unported License


Written By
Web Developer
Ukraine Ukraine
Web Developer, interested in bleeding age web technologies and projects.

Experienced and interested in:
- High load web projects, bespoke software development
- DevOps: Chef, Ansible, Vagrant
- NoSQL (mongodb)
- Client stack (javascript core, jquery, AngularJS, HTML5 apis)
- *AAS (Amazon beanstalk, Redhat openshift)
- MEAN & Pure JS stack (Javascript, AngularJS, Node.JS, MongoDB)


-> DevOps inquiries
-> Other inquiries
-> Follow me on Github

Comments and Discussions

 
-- There are no messages in this forum --