Google Authenticator

Tips submitted by PHPMaker users
Post Reply
EpicFart
User
Posts: 4

Google Authenticator

Post by EpicFart »

Hey all,
For anyone wanting to use Google Authenticator to log in (instead of a password), this is how to do it:

  1. 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);
  1. 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;
    }
  1. 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
    );
}

}


EpicFart
User
Posts: 4

Post by EpicFart »

Oops... forgot the following function (in your userfn12.php) :

//****************************************************************************************************
//Return the SALT from a USER Name
function get_user_salt($v) {
global $dbpdo;
try {
$stmt = $dbpdo->prepare("SELECT user_salt FROM dp_user WHERE user_login=:ul");
$stmt->bindValue(':ul', $v, PDO::PARAM_STR);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
} catch(PDOException $ex) {
PushPDOError(FUNCTION,$ex); //Pass this function name and error message to the error handler
}
$getresult = $row{'user_salt'};
//if (is_null($getresult)) {$getresult = "t0LSTSLQl89OPJxHrg4ciXXMGz1zlE3c";}
return @$getresult;
}
//****************************************************************************************************


Post Reply