Most systems currently use static passwords for authentication login,But because static passwords are easy to steal,Its security cannot meet the security requirements.

Dynamic passwords use a one-time password and invalid passwords to prevent security problems caused by theft of passwords.

Dynamic password is divided into hotp (dynamic password based on event count,rfc4226), totp (time-based dynamic password,rfc6238), ocra (challenge dynamic password,rfc6287) and so on.

This article introduces a dynamic password authentication scheme that integrates TOTP.The php framework uses thinkphp3.2.3, and the dynamic password generator uses google authtication.

1.Add oath algorithm class to thinkphp framework

The oath algorithm encapsulates the oath.php code as follows:

 * this program is free software:you can redistribute it and/or modify
 * it under the terms of the gnu general public license as published by
 * the free software foundation, either version 3 of the license, or
 * (at your option) any later version.
 * this program is distributed in the hope that it will be useful, * but without any warranty;without even the implied warranty of
 * merchantability or fitness for a particular purpose. see the
 * gnu general public license for more details.
 * you should have received a copy of the gnu general public license
 * along with this program. if not, see<http://www.gnu.org/licenses/> ;.
 * php google two-factor authentication module.
 * see http://www.idontplaydarts.com/2011/07/google-totp-two-factor-authentication-for-php/
 * for more details
 * @author phil
 ** /
class google2fa {
 const keyregeneration=30;//interval between key regeneration
 const otplength=6;//length of the token generated
 private static $lut=array (//lookup needed for base32 encoding
  "a" =>0, "b" =>1,  "c" =>2, "d" =>3,  "e" =>4, "f" =>5,  "g" =>6, "h" =>7,  "i" =>8, "j" =>9,  "k" =>10, "l" =>11,  "m" =>12, "n" =>13,  "o" =>14, "p" =>15,  "q" =>16, "r" =>17,  "s" =>18, "t" =>19,  "u" =>20, "v" =>21,  "w" =>22, "x" =>23,  "y" =>24, "z" =>25,  "2" =>26, "3" =>27,  "4" =>28, "5" =>29,  "6" =>30, "7" =>31
  * generates a 16 digit secret key in base32 format
  * @return string
  ** /
 public static function generate_secret_key ($length=16) {
  for ($i=0;$i<$length;$i ++)
   $s.=$b32 [rand (0,31)];
  return $s;
  * returns the current unix timestamp devided by the keyregeneration
  * period.
  * @return integer
  ** /
 public static function get_timestamp () {
  return floor (microtime (true)/self ::keyregeneration);
  * decodes a base32 string into a binary string.
  ** /
 public static function base32_decode ($b32) {
  $b32=strtoupper ($b32);
  if (! preg_match ("/^ [abcdefghijklmnopqrstuvwxyz234567] + $/", $b32, $match))
   throw new exception ("invalid characters in the base32 string.");
  $l=strlen ($b32);
  for ($i=0;$i<$l;$i ++) {
   $n=$n<<5;//move buffer left by 5 to make room
   $n=$n + self ::$lut [$b32 [$i]];//add value into buffer
   $j=$j + 5;//keep track of number of bits in buffer
   if ($j>= 8) {
    $binary.=chr (($n&(0xff<<$j))>>$j);
  return $binary;
 /* by tang * /
 public static function base32_encode ($data, $length) {
  if ($length>0) {
   $buffer=$data [0];
   while (($bitsleft>0 || $next&$length)) {
    if ($bitsleft<5) {
    if ($next<$length) {
     $buffer<<= 8;
     $buffer |=$data [$next ++]&0xff;
     $bitsleft +=8;
    } else {
     $buffer<<= $pad;
     $bitsleft +=$pad;
    $bitsleft-= 5;
    $result.=$basestr [$index];
    $count ++;
   return $result;
  * takes the secret key and the timestamp and returns the one time
  * password.
  * @param binary $key-secret key in binary form.
  * @param integer $counter-timestamp as returned by get_timestamp.
  * @return string
  ** /
 public static function oath_hotp ($key, $counter)
  if (strlen ($key)<8)
  throw new exception ("secret key is too short. must be at least 16 base 32 characters");
  $bin_counter=pack ("n *", 0). pack ("n *", $counter);//counter must be 64-bit int
  $hash=hash_hmac ("sha1", $bin_counter, $key, true);
  return str_pad (self ::oath_truncate ($hash), self ::otplength, "0", str_pad_left);
  * verifys a user inputted key against the current timestamp. checks $window
  * keys either side of the timestamp.
  * @param string $b32seed
  * @param string $key-user specified key
  * @param integer $window
  * @param boolean $usetimestamp
  * @return boolean
  ** /
 public static function verify_key ($b32seed, $key, $window=5, $usetimestamp=true) {
  $timestamp=self ::get_timestamp ();
  if ($usetimestamp! == true) $timestamp=(int) $usetimestamp;
  $binaryseed=self ::base32_decode ($b32seed);
  for ($ts=$timestamp-$window;$ts<= $timestamp + $window;$ts ++)
   if (self ::oath_hotp ($binaryseed, $ts) == $key)
    return true;
  return false;
  * extracts the otp from the sha1 hash.
  * @param binary $hash
  * @return integer
  ** /
 public static function oath_truncate ($hash)
  $offset=ord ($hash [19])&0xf;
  return (
   ((ord ($hash [$offset + 0])&0x7f)<<24) |
   ((ord ($hash [$offset + 1])&0xff)<<16) |
   ((ord ($hash [$offset + 2])&0xff)<<8) |
   (ord ($hash [$offset + 3])&0xff)
  )%pow (10, self ::otplength);
$initalizationkey="lflfmu2sgvcuiuczkbmekrkliq";//set the inital key
$timestamp=google2fa ::get_timestamp ();
$secretkey=google2fa ::base32_decode ($initalizationkey);//decode it into binary
$otp=google2fa ::oath_hotp ($secretkey, $timestamp);//get current token
echo ("init key:$initalizationkey \ n");
echo ("timestamp:$timestamp \ n");
echo ("one time password:$otp \ n");
//use this to verify a key as it allows for some time drift.
$result=google2fa ::verify_key ($initalizationkey, "123456");
var_dump ($result);
* /

Because the seed key of Google ’s dynamic password algorithm uses base32 encoding, the base32 algorithm is required. The content of base32.php is as follows:

//namespace base32;
 * base32 encoder and decoder
 * last update:2012-06-20
 * rfc 4648 compliant
 * @link http://www.ietf.org/rfc/rfc4648.txt
 * some groundwork based on this class
 * https://github.com/nticompass/php-base32
 * @author christian riesen<[email protected]>
 * @link http://christianriesen.com
 * @license mit license see license file
 * /
class base32
  * alphabet for encoding and decoding base32
  * @var array
  * /
 private static $alphabet="abcdefghijklmnopqrstuvwxyz234567 =";
  * creates an array from a binary string into a given chunk size
  * @param string $binarystring string to chunk
  * @param integer $bits number of bits per chunk
  * @return array
  * /
 private static function chunk ($binarystring, $bits)
  $binarystring=chunk_split ($binarystring, $bits, "");
  if (substr ($binarystring, (strlen ($binarystring))-1) == "") {
   $binarystring=substr ($binarystring, 0, strlen ($binarystring) -1);
  return explode ("", $binarystring);
  * encodes into base32
  * @param string $string clear text string
  * @return string base32 encoded string
  * /
 public static function encode ($string)
  if (strlen ($string) == 0) {
   //gives an empty string
   return "";
  //convert string to binary
  foreach (str_split ($string) as $s) {
   //return each character as an 8-bit binary string
   $binarystring.=sprintf ("%08b", ord ($s));
  //break into 5-bit chunks, then break that into an array
  $binaryarray=self ::chunk ($binarystring, 5);
  //pad array to be divisible by 8
  while (count ($binaryarray)%8! == 0) {
   $binaryarray []=null;
  //encode in base32
  foreach ($binaryarray as $bin) {
   if (! is_null ($bin)) {
    //pad the binary strings
    $bin=str_pad ($bin, 5, 0, str_pad_right);
    $char=bindec ($bin);
   //base32 character
   $base32string.=self ::$alphabet [$char];
  return $base32string;
  * decodes base32
  * @param string $base32string base32 encoded string
  * @return string clear text string
  * /
 public static function decode ($base32string)
  //only work in upper cases
  $base32string=strtoupper ($base32string);
  //remove anything that is not base32 alphabet
  $pattern="/[^ a-z2-7] /";
  $base32string=preg_replace ($pattern, "", $base32string);
  if (strlen ($base32string) == 0) {
   //gives an empty string
   return "";
  $base32array=str_split ($base32string);
  foreach ($base32array as $str) {
   $char=strpos (self ::$alphabet, $str);
   //ignore the padding character
   if ($char! == 32) {
    $string.=sprintf ("%05b", $char);
  while (strlen ($string)%8! == 0) {
   $string=substr ($string, 0, strlen ($string) -1);
  $binaryarray=self ::chunk ($string, 8);
  foreach ($binaryarray as $bin) {
   //pad each value to 8 bits
   $bin=str_pad ($bin, 8, 0, str_pad_right);
   //convert binary strings to ascii
   $realstring.=chr (bindec ($bin));
  return $realstring;

Put these two files in the thinkphp \ library \ vendor \ oath directory of the thinkphp framework. The oath directory is created by yourself.

2.Add database fields

The user table adds the following fields:

auth_type (0-static password,1-Dynamic password)


temp_seed (temporary seed key)

last_logintime (last successful login time)

last_otp (last password used)

The auth_type is to indicate which authentication method the user uses,seed is the user's seed key,temp_seed is a seed key temporarily saved before the user opens,If the user has successfully opened the dynamic password authentication,The content of this field will be filled into the seed field. last_logintime and last_otp are the time of the last successful authentication and the dynamic password,Used to prevent users from using the same password repeatedly.

3.Code integration

1), enable dynamic password

On the password change page of the original system,With the choice of authentication method,E.g:

If the user chooses the dynamic password method,Will generate a QR code to display on the page,Used for users to activate dynamic passwords.In order to be compatible with google authtication, its QR code format is the same as that of Google.For the method of generating QR code, see my other article"Thinkphp3.2.3 integrated phpqrcode to generate a QR code with a logo".

The code for generating the key QR code is as follows:

public function qrcode ()
  vendor ("oath.base32");
  $base32=new \ base32 ();
  $rand=random (16);//Generate a random seed
  $rand=$base32->encode ($rand);
  $rand=str_replace ("=", "", $rand);//Remove the filled "="
  $errorcorrectionlevel=intval (3);//Fault tolerance level
  $matrixpointsize=intval (8);//Generate image size
  //Generate a QR code picture
  vendor ("phpqrcode.phpqrcode");
  $object=new \ qrcode ();
  $text=sprintf ("otpauth://totp /%s?secret =%s", $user, $rand);
  $object->png ($text, false, $errorcorrectionlevel, $matrixpointsize, 2);
  The generated seed $rand is saved to the temp_seed field of the database

random is a function that generates a random string.The code of $rand=str_replace ("=", "", $rand) is because the base32 decoding algorithm in the Google mobile token is not filled with "=".

The code to verify the user's dynamic password is as follows:

Read temp_seed from the database
vendor ("oath.oath");
$object=new \ google2fa ();
if ($object->verify_key ($temp_seed, $otp)) {
 Verification was successful.
Update the database seed to temp_seed, auth_type to 1, and last_otp to otp

2), dynamic password login

User dynamic password login verification code:

Read the auth_type, seed, last_otp fields from the database.

if ($auth_type == 1) {//dynamic password
 //Prevent duplicate authentication
 if ($lat_otp == $otp) {
  Dynamic password reuse returns
 vendor ("oath.oath");
 $object=new \ google2fa ();
 if (! $object->verify_key ($seed, $otp))
  Dynamic password is incorrect
  login successful,Update the database last_otp to $otp and last_logintime to time ()

4, test verification

Download google authtication, log in to the system with a static password,Go to the password change page.

Open google authtication, scan the QR code,A dynamic password is displayed.

Save the content,Successfully opened dynamic password!

Then you can log in to the system with a tall and dynamic password!

  • Previous Android viewpager example code to jump to main page after last page slide
  • Next EasyUI is displayed after loading HTML content style rendering