Hey all,
For anyone wanting to use Google Authenticator to log in (instead of a password), this is how to do it:
- GENERATING A UNIQUE SECRET (uses base-32)
When you are adding a user (register.php) and writing to the DB (you need a varchar(16) field called user_salt in your user table), add this code:
//Used for Google Authenticator date_default_timezone_set('America/Edmonton'); //or whatever timezone you are in - VERY IMPORTANT, or the codes won't sync require_once('GoogleAuthenticator.php'); $authenticator = new GoogleAuthenticator(); $this->user_salt->SetDbValueDef($rsnew, $authenticator->createSecret(), NULL, FALSE);
DISPLAYING A QR-CODE TO SET UP THE AUTHENTICATION
I have a user profile page (they have to log in at least once using a password or email account validation) that contains the following:if (IsLoggedIn())
{
try {
$stmt = $dbpdo->prepare("SELECT user_login,user_salt FROM dp_user WHERE user_login=:ul");
$stmt->bindValue(':ul', $_SESSION[EW_SESSION_USER_NAME], PDO::PARAM_STR);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
} catch(PDOException $ex) {
PushPDOError("Get User Details",$ex); //error handler (not posted in this example)
}//Google Authenticator date_default_timezone_set('America/Edmonton'); require_once('GoogleAuthenticator.php'); $authenticator = new GoogleAuthenticator(); $secret = $row[user_salt]; $name = $row[user_login]; $title = 'Your website or company name here'; $qrCodeUrl = $authenticator->getQRCodeGoogleUrl($name, $secret, $title); $qrCodeImage = "<img src='".$qrCodeUrl."' />"; echo "Google Authenticator ID:<p><small>(Feel free to use Google Authenticator (available in the APP store on all mobile platforms) to log you into this site - vs having to remember your password. Just type the response code as your password when logging in.)</small></p>".$qrCodeImage; }
- VALIDATING THE USER
In your login.php (near the top):
//Check Google Authenticator (instead of password) - uses function User_CustomValidate in phpfn12.php
date_default_timezone_set('America/Edmonton'); //or whatever timezone you are in
require_once('GoogleAuthenticator.php');
In phpfn12.php (User Custom Validate event):
// User Custom Validate event
function User_CustomValidate(&$usr, &$pwd) {
$authenticator = new GoogleAuthenticator();
$secret = get_user_salt($usr);
$tolerance = 1; //check current & previous OTP.
$checkResult = $authenticator->verifyCode($secret, $pwd, $tolerance);
//ie: Check the validity of the user (using the unique user salt) if they entered the authenticator result into the password field
//Return TRUE if valid.
return $checkResult;
}
===============================
Done... easy as pie!
I tweaked the below code to make it work (can't remember what I changed as it was a few months ago, however leaving the author credits - as it was his work!)
Enjoy
=======================================================
GoogleAuthenticator.php (create in your website main folder)
<?php
/**
- PHP Class for handling Google Authenticator 2-factor authentication
* - @author Michael Kliewe
- @copyright 2012 Michael Kliewe
- @license www. opensource. org/licenses/bsd-license.php BSD License
- @link www. phpgangsta. de/
*/
class GoogleAuthenticator
{
protected $_codeLength = 6;
/**
* Create new secret.
* 16 characters, randomly chosen from the allowed base32 characters.
*
* @param int $secretLength
* @return string
*/
public function createSecret($secretLength = 16)
{
$validChars = $this->_getBase32LookupTable();
unset($validChars[32]);
$secret = '';
for ($i = 0; $i < $secretLength; $i++) {
$secret .= $validChars[array_rand($validChars)];
}
return $secret;
}
/**
* Calculate the code, with given secret and point in time
*
* @param string $secret
* @param int|null $timeSlice
* @return string
*/
public function getCode($secret, $timeSlice = null)
{
if ($timeSlice === null) {
$timeSlice = floor(time() / 30);
}
$secretkey = $this->_base32Decode($secret);
// Pack time into binary string
$time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
// Hash it with users secret key
$hm = hash_hmac('SHA1', $time, $secretkey, true);
// Use last nipple of result as index/offset
$offset = ord(substr($hm, -1)) & 0x0F;
// grab 4 bytes of the result
$hashpart = substr($hm, $offset, 4);
// Unpak binary value
$value = unpack('N', $hashpart);
$value = $value[1];
// Only 32 bits
$value = $value & 0x7FFFFFFF;
$modulo = pow(10, $this->_codeLength);
return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
}
/**
* Get QR-Code URL for image, from google charts
*
* @param string $name
* @param string $secret
* @param string $title
* @return string
*/
public function getQRCodeGoogleUrl($name, $secret, $title = null) {
$urlencoded = urlencode('otpauth://totp/'.$title.':'.$name.'?secret='.$secret.'&issuer='.$title);
return 'htt ps://chart. googleapis. com/chart?chs=150x150&chld=M|0&cht=qr&chl='.$urlencoded.''; //REMOVE SPACES ON THIS LINE (as this forum does not allow URLs)
}
/**
* Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now
*
* @param string $secret
* @param string $code
* @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
* @param int|null $currentTimeSlice time slice if we want use other that time()
* @return bool
*/
public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null)
{
if ($currentTimeSlice === null) {
$currentTimeSlice = floor(time() / 30);
}
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
$calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
if ($calculatedCode == $code ) {
return true;
}
}
return false;
}
/**
* Set the code length, should be >=6
*
* @param int $length
* @return PHPGangsta_GoogleAuthenticator
*/
public function setCodeLength($length)
{
$this->_codeLength = $length;
return $this;
}
/**
* Helper class to decode base32
*
* @param $secret
* @return bool|string
*/
protected function _base32Decode($secret)
{
if (empty($secret)) return '';
$base32chars = $this->_getBase32LookupTable();
$base32charsFlipped = array_flip($base32chars);
$paddingCharCount = substr_count($secret, $base32chars[32]);
$allowedValues = array(6, 4, 3, 1, 0);
if (!in_array($paddingCharCount, $allowedValues)) return false;
for ($i = 0; $i < 4; $i++){
if ($paddingCharCount == $allowedValues[$i] &&
substr($secret, -($allowedValues[$i])) != str_repeat($base32chars[32], $allowedValues[$i])) return false;
}
$secret = str_replace('=','', $secret);
$secret = str_split($secret);
$binaryString = "";
for ($i = 0; $i < count($secret); $i = $i+8) {
$x = "";
if (!in_array($secret[$i], $base32chars)) return false;
for ($j = 0; $j < 8; $j++) {
$x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
}
$eightBits = str_split($x, 8);
for ($z = 0; $z < count($eightBits); $z++) {
$binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:"";
}
}
return $binaryString;
}
/**
* Helper class to encode base32
*
* @param string $secret
* @param bool $padding
* @return string
*/
protected function _base32Encode($secret, $padding = true)
{
if (empty($secret)) return '';
$base32chars = $this->_getBase32LookupTable();
$secret = str_split($secret);
$binaryString = "";
for ($i = 0; $i < count($secret); $i++) {
$binaryString .= str_pad(base_convert(ord($secret[$i]), 10, 2), 8, '0', STR_PAD_LEFT);
}
$fiveBitBinaryArray = str_split($binaryString, 5);
$base32 = "";
$i = 0;
while ($i < count($fiveBitBinaryArray)) {
$base32 .= $base32chars[base_convert(str_pad($fiveBitBinaryArray[$i], 5, '0'), 2, 10)];
$i++;
}
if ($padding && ($x = strlen($binaryString) % 40) != 0) {
if ($x == 8) $base32 .= str_repeat($base32chars[32], 6);
elseif ($x == 16) $base32 .= str_repeat($base32chars[32], 4);
elseif ($x == 24) $base32 .= str_repeat($base32chars[32], 3);
elseif ($x == 32) $base32 .= $base32chars[32];
}
return $base32;
}
/**
* Get array with all 32 characters for decoding from/encoding to base32
*
* @return array
*/
protected function _getBase32LookupTable()
{
return array(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
'=' // padding char
);
}
}