├── LICENSE ├── README ├── example.php ├── lib ├── FixedByteNotation.php └── GoogleAuthenticator.php ├── tmpl ├── ask-for-otp.php ├── loggedin.php ├── login-error.php ├── login.php └── show-qr.php ├── users.dat └── web ├── Users.php └── index.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Liip AG 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Ported from http://code.google.com/p/google-authenticator/ 2 | 3 | You can use the Google Authenticator app from here 4 | http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=1066447 5 | to generate One Time Passwords/Tokens and check them with this little 6 | PHP app (Of course, you can also create them with this). 7 | 8 | There are many real world applications for that, but noone implemented it yet. 9 | 10 | See example.php for how to use it. 11 | 12 | There's a little web app showing how it works in web/, please make users.dat 13 | writeable for the webserver, doesn't really work otherwise (it can't save the 14 | secret). Try to login with chregu/foobar. 15 | 16 | 17 | What's missing in the example: 18 | *** 19 | 20 | * Prevent replay attacks. One token should only be used once 21 | * Show QR Code only when providing password again (or not at all) 22 | * Regenrate secret -------------------------------------------------------------------------------- /example.php: -------------------------------------------------------------------------------- 1 | getCode($secret); 12 | 13 | print "\n"; 14 | 15 | print "Check if $code is valid: "; 16 | 17 | if ($g->checkCode($secret,$code)) { 18 | print "YES \n"; 19 | } else { 20 | print "NO \n"; 21 | } 22 | 23 | $secret = $g->generateSecret(); 24 | print "Get a new Secret: $secret \n"; 25 | 26 | print "The QR Code for this secret (to scan with the Google Authenticator App: \n"; 27 | print $g->getURL('chregu','example.org',$secret); 28 | print "\n"; -------------------------------------------------------------------------------- /lib/FixedByteNotation.php: -------------------------------------------------------------------------------- 1 | = ($radix <<= 1) && $bitsPerCharacter < 8) { 62 | $bitsPerCharacter++; 63 | } 64 | 65 | $radix >>= 1; 66 | 67 | } elseif ($bitsPerCharacter > 8) { 68 | // $bitsPerCharacter must not be greater than 8 69 | $bitsPerCharacter = 8; 70 | $radix = 256; 71 | 72 | } else { 73 | $radix = 1 << $bitsPerCharacter; 74 | } 75 | 76 | $this->_chars = $chars; 77 | $this->_bitsPerCharacter = $bitsPerCharacter; 78 | $this->_radix = $radix; 79 | $this->_rightPadFinalBits = $rightPadFinalBits; 80 | $this->_padFinalGroup = $padFinalGroup; 81 | $this->_padCharacter = $padCharacter[0]; 82 | } 83 | 84 | /** 85 | * Encode a string 86 | * 87 | * @param string $rawString Binary data to encode 88 | * @return string 89 | */ 90 | public function encode($rawString) 91 | { 92 | // Unpack string into an array of bytes 93 | $bytes = unpack('C*', $rawString); 94 | $byteCount = count($bytes); 95 | 96 | $encodedString = ''; 97 | $byte = array_shift($bytes); 98 | $bitsRead = 0; 99 | 100 | $chars = $this->_chars; 101 | $bitsPerCharacter = $this->_bitsPerCharacter; 102 | $rightPadFinalBits = $this->_rightPadFinalBits; 103 | $padFinalGroup = $this->_padFinalGroup; 104 | $padCharacter = $this->_padCharacter; 105 | 106 | // Generate encoded output; 107 | // each loop produces one encoded character 108 | for ($c = 0; $c < $byteCount * 8 / $bitsPerCharacter; $c++) { 109 | 110 | // Get the bits needed for this encoded character 111 | if ($bitsRead + $bitsPerCharacter > 8) { 112 | // Not enough bits remain in this byte for the current 113 | // character 114 | // Save the remaining bits before getting the next byte 115 | $oldBitCount = 8 - $bitsRead; 116 | $oldBits = $byte ^ ($byte >> $oldBitCount << $oldBitCount); 117 | $newBitCount = $bitsPerCharacter - $oldBitCount; 118 | 119 | if (!$bytes) { 120 | // Last bits; match final character and exit loop 121 | if ($rightPadFinalBits) $oldBits <<= $newBitCount; 122 | $encodedString .= $chars[$oldBits]; 123 | 124 | if ($padFinalGroup) { 125 | // Array of the lowest common multiples of 126 | // $bitsPerCharacter and 8, divided by 8 127 | $lcmMap = array(1 => 1, 2 => 1, 3 => 3, 4 => 1, 128 | 5 => 5, 6 => 3, 7 => 7, 8 => 1); 129 | $bytesPerGroup = $lcmMap[$bitsPerCharacter]; 130 | $pads = $bytesPerGroup * 8 / $bitsPerCharacter 131 | - ceil((strlen($rawString) % $bytesPerGroup) 132 | * 8 / $bitsPerCharacter); 133 | $encodedString .= str_repeat($padCharacter[0], $pads); 134 | } 135 | 136 | break; 137 | } 138 | 139 | // Get next byte 140 | $byte = array_shift($bytes); 141 | $bitsRead = 0; 142 | 143 | } else { 144 | $oldBitCount = 0; 145 | $newBitCount = $bitsPerCharacter; 146 | } 147 | 148 | // Read only the needed bits from this byte 149 | $bits = $byte >> 8 - ($bitsRead + ($newBitCount)); 150 | $bits ^= $bits >> $newBitCount << $newBitCount; 151 | $bitsRead += $newBitCount; 152 | 153 | if ($oldBitCount) { 154 | // Bits come from seperate bytes, add $oldBits to $bits 155 | $bits = ($oldBits << $newBitCount) | $bits; 156 | } 157 | 158 | $encodedString .= $chars[$bits]; 159 | } 160 | 161 | return $encodedString; 162 | } 163 | 164 | /** 165 | * Decode a string 166 | * 167 | * @param string $encodedString Data to decode 168 | * @param boolean $caseSensitive 169 | * @param boolean $strict Returns NULL if $encodedString contains 170 | * an undecodable character 171 | * @return string|NULL 172 | */ 173 | public function decode($encodedString, $caseSensitive = TRUE, 174 | $strict = FALSE) 175 | { 176 | if (!$encodedString || !is_string($encodedString)) { 177 | // Empty string, nothing to decode 178 | return ''; 179 | } 180 | 181 | $chars = $this->_chars; 182 | $bitsPerCharacter = $this->_bitsPerCharacter; 183 | $radix = $this->_radix; 184 | $rightPadFinalBits = $this->_rightPadFinalBits; 185 | $padFinalGroup = $this->_padFinalGroup; 186 | $padCharacter = $this->_padCharacter; 187 | 188 | // Get index of encoded characters 189 | if ($this->_charmap) { 190 | $charmap = $this->_charmap; 191 | 192 | } else { 193 | $charmap = array(); 194 | 195 | for ($i = 0; $i < $radix; $i++) { 196 | $charmap[$chars[$i]] = $i; 197 | } 198 | 199 | $this->_charmap = $charmap; 200 | } 201 | 202 | // The last encoded character is $encodedString[$lastNotatedIndex] 203 | $lastNotatedIndex = strlen($encodedString) - 1; 204 | 205 | // Remove trailing padding characters 206 | while ($encodedString[$lastNotatedIndex] == $padCharacter[0]) { 207 | $encodedString = substr($encodedString, 0, $lastNotatedIndex); 208 | $lastNotatedIndex--; 209 | } 210 | 211 | $rawString = ''; 212 | $byte = 0; 213 | $bitsWritten = 0; 214 | 215 | // Convert each encoded character to a series of unencoded bits 216 | for ($c = 0; $c <= $lastNotatedIndex; $c++) { 217 | 218 | if (!isset($charmap[$encodedString[$c]]) && !$caseSensitive) { 219 | // Encoded character was not found; try other case 220 | if (isset($charmap[$cUpper 221 | = strtoupper($encodedString[$c])])) { 222 | $charmap[$encodedString[$c]] = $charmap[$cUpper]; 223 | 224 | } elseif (isset($charmap[$cLower 225 | = strtolower($encodedString[$c])])) { 226 | $charmap[$encodedString[$c]] = $charmap[$cLower]; 227 | } 228 | } 229 | 230 | if (isset($charmap[$encodedString[$c]])) { 231 | $bitsNeeded = 8 - $bitsWritten; 232 | $unusedBitCount = $bitsPerCharacter - $bitsNeeded; 233 | 234 | // Get the new bits ready 235 | if ($bitsNeeded > $bitsPerCharacter) { 236 | // New bits aren't enough to complete a byte; shift them 237 | // left into position 238 | $newBits = $charmap[$encodedString[$c]] << $bitsNeeded 239 | - $bitsPerCharacter; 240 | $bitsWritten += $bitsPerCharacter; 241 | 242 | } elseif ($c != $lastNotatedIndex || $rightPadFinalBits) { 243 | // Zero or more too many bits to complete a byte; 244 | // shift right 245 | $newBits = $charmap[$encodedString[$c]] >> $unusedBitCount; 246 | $bitsWritten = 8; //$bitsWritten += $bitsNeeded; 247 | 248 | } else { 249 | // Final bits don't need to be shifted 250 | $newBits = $charmap[$encodedString[$c]]; 251 | $bitsWritten = 8; 252 | } 253 | 254 | $byte |= $newBits; 255 | 256 | if ($bitsWritten == 8 || $c == $lastNotatedIndex) { 257 | // Byte is ready to be written 258 | $rawString .= pack('C', $byte); 259 | 260 | if ($c != $lastNotatedIndex) { 261 | // Start the next byte 262 | $bitsWritten = $unusedBitCount; 263 | $byte = ($charmap[$encodedString[$c]] 264 | ^ ($newBits << $unusedBitCount)) << 8 - $bitsWritten; 265 | } 266 | } 267 | 268 | } elseif ($strict) { 269 | // Unable to decode character; abort 270 | return NULL; 271 | } 272 | } 273 | 274 | return $rawString; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /lib/GoogleAuthenticator.php: -------------------------------------------------------------------------------- 1 | getCode($secret,$time + $i) == $code) { 32 | return true; 33 | } 34 | } 35 | 36 | return false; 37 | 38 | } 39 | 40 | public function getCode($secret,$time = null) { 41 | 42 | if (!$time) { 43 | $time = floor(time() / 30); 44 | } 45 | $base32 = new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', TRUE, TRUE); 46 | $secret = $base32->decode($secret); 47 | 48 | $time = pack("N", $time); 49 | $time = str_pad($time,8, chr(0), STR_PAD_LEFT); 50 | 51 | $hash = hash_hmac('sha1',$time,$secret,true); 52 | $offset = ord(substr($hash,-1)); 53 | $offset = $offset & 0xF; 54 | 55 | $truncatedHash = self::hashToInt($hash, $offset) & 0x7FFFFFFF; 56 | $pinValue = str_pad($truncatedHash % self::$PIN_MODULO,6,"0",STR_PAD_LEFT);; 57 | return $pinValue; 58 | } 59 | 60 | protected function hashToInt($bytes, $start) { 61 | $input = substr($bytes, $start, strlen($bytes) - $start); 62 | $val2 = unpack("N",substr($input,0,4)); 63 | return $val2[1]; 64 | } 65 | 66 | public function getUrl($user, $hostname, $secret) { 67 | $url = sprintf("otpauth://totp/%s@%s?secret=%s", $user, $hostname, $secret); 68 | $encoder = "https://api.qrserver.com/v1/create-qr-code/?size=200x200&data="; 69 | $encoderURL = sprintf( "%sotpauth://totp/%s@%s&secret=%s",$encoder, $user, $hostname, $secret); 70 | 71 | return $encoderURL; 72 | 73 | } 74 | 75 | public function generateSecret() { 76 | $secret = ""; 77 | for($i = 1; $i<= self::$SECRET_LENGTH;$i++) { 78 | $c = rand(0,255); 79 | $secret .= pack("c",$c); 80 | } 81 | $base32 = new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', TRUE, TRUE); 82 | return $base32->encode($secret); 83 | 84 | 85 | } 86 | 87 | } 88 | 89 | -------------------------------------------------------------------------------- /tmpl/ask-for-otp.php: -------------------------------------------------------------------------------- 1 | 2 |

please otp

3 |

4 |

5 | 6 |
7 | (Set $debug in index.php to false, if you don't want to have the OTP prefilled (for real life application, for example ;))
8 | 9 | 10 | otp:
17 |
18 | 19 | 20 |
-------------------------------------------------------------------------------- /tmpl/loggedin.php: -------------------------------------------------------------------------------- 1 | 2 |

3 | Hello getUsername(); ?> 4 |

5 | 8 | 9 |

10 | Show QR Code 11 |

12 | 13 | 16 | 17 |

18 | Logout 19 |

-------------------------------------------------------------------------------- /tmpl/login-error.php: -------------------------------------------------------------------------------- 1 |

2 | Wrong username or password or token. 3 |

4 |

5 | try again 6 |

-------------------------------------------------------------------------------- /tmpl/login.php: -------------------------------------------------------------------------------- 1 | 2 |

please login

3 |

4 |

5 | username:
6 | password:
7 | 8 |
-------------------------------------------------------------------------------- /tmpl/show-qr.php: -------------------------------------------------------------------------------- 1 |

Please scan this

2 | 3 |

with the Google Authenticator App

4 | 5 |

6 | getUrl($user->getUsername(),$_SERVER['HTTP_HOST'],$secret); 9 | ?> 10 | 11 | 12 |

-------------------------------------------------------------------------------- /users.dat: -------------------------------------------------------------------------------- 1 | {"chregu":{"password":"foobar"}} -------------------------------------------------------------------------------- /web/Users.php: -------------------------------------------------------------------------------- 1 | userFile = $file; 8 | 9 | $this->users = json_decode(file_get_contents($file),true); 10 | } 11 | function hasSession() { 12 | session_start(); 13 | if (isset($_SESSION['username'])) { 14 | return $_SESSION['username']; 15 | } 16 | return false; 17 | } 18 | 19 | 20 | function storeData(User $user) { 21 | $this->users[$user->getUsername()] = $user->getData(); 22 | file_put_contents($this->userFile,json_encode($this->users)); 23 | } 24 | 25 | function loadUser($name) { 26 | if (isset($this->users[$name])) { 27 | 28 | return new User($name,$this->users[$name]); 29 | } else { 30 | return false; 31 | } 32 | } 33 | 34 | 35 | 36 | } 37 | 38 | class User { 39 | 40 | function __construct($user,$data) { 41 | $this->data = $data; 42 | $this->user = $user; 43 | } 44 | 45 | function auth($pass) { 46 | if ($this->data['password'] === $pass) { 47 | return true; 48 | } 49 | 50 | return false; 51 | 52 | } 53 | 54 | function startSession() { 55 | 56 | $_SESSION['username'] = $this->user; 57 | } 58 | 59 | function doLogin() { 60 | session_regenerate_id(); 61 | $_SESSION['loggedin'] = true; 62 | $_SESSION['ua'] = $_SERVER['HTTP_USER_AGENT']; 63 | } 64 | 65 | function doOTP() { 66 | $_SESSION['OTP'] = true; 67 | } 68 | 69 | function isOTP() { 70 | if (isset($_SESSION['OTP']) && $_SESSION['OTP'] == true) { 71 | 72 | return true; 73 | } 74 | return false; 75 | 76 | } 77 | function isLoggedIn() { 78 | if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] == true && 79 | isset($_SESSION['ua']) && $_SESSION['ua'] == $_SERVER['HTTP_USER_AGENT'] 80 | ) { 81 | 82 | return $_SESSION['username']; 83 | } 84 | return false; 85 | 86 | } 87 | 88 | 89 | function getUsername() { 90 | return $this->user; 91 | } 92 | 93 | function getSecret() { 94 | if (isset($this->data['secret'])) { 95 | return $this->data['secret']; 96 | } 97 | return false; 98 | } 99 | 100 | function generateSecret() { 101 | $g = new GoogleAuthenticator(); 102 | $secret = $g->generateSecret(); 103 | $this->data['secret'] = $secret; 104 | return $secret; 105 | 106 | } 107 | 108 | function getData() { 109 | return $this->data; 110 | } 111 | 112 | function setOTPCookie() { 113 | $time = floor(time() / (3600 * 24) ); // get day number 114 | //about using the user agent: It's easy to fake it, but it increases the barrier for stealing and reusing cookies nevertheless 115 | // and it doesn't do any harm (except that it's invalid after a browser upgrade, but that may be even intented) 116 | $cookie = $time.":".hash_hmac("sha1",$this->getUsername().":".$time.":". $_SERVER['HTTP_USER_AGENT'],$this->getSecret()); 117 | setcookie ( "otp", $cookie, time() + (30 * 24 * 3600), null,null,null,true ); 118 | } 119 | 120 | function hasValidOTPCookie() { 121 | // 0 = tomorrow it is invalid 122 | $daysUntilInvalid = 0; 123 | $time = (string) floor((time() / (3600 * 24))) ; // get day number 124 | if (isset($_COOKIE['otp'])) { 125 | list( $otpday,$hash) = explode(":",$_COOKIE['otp']); 126 | 127 | if ( $otpday >= $time - $daysUntilInvalid && $hash == hash_hmac('sha1',$this->getUsername().":".$otpday .":". $_SERVER['HTTP_USER_AGENT'] , $this->getSecret()) 128 | ) { 129 | return true; 130 | } 131 | 132 | 133 | } 134 | return false; 135 | 136 | } 137 | 138 | } 139 | ?> 140 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | Google Authenticator in PHP demo 14 | 15 | 16 | hasSession()) { 24 | //load the user data from the json storage. 25 | $user = $users->loadUser($username); 26 | //if he clicked logout, destroy the session and redirect to the startscreen. 27 | if (isset($_GET['logout'])) { 28 | session_destroy(); 29 | header("Location: ./"); 30 | } 31 | // check if the user is logged in. 32 | if ($user->isLoggedIn()) { 33 | include("../tmpl/loggedin.php"); 34 | //show the QR code if whished so 35 | if (isset($_GET['showqr'])) { 36 | $secret = $user->getSecret(); 37 | include("../tmpl/show-qr.php"); 38 | } 39 | } 40 | //if the user is in the OTP phase and submit the OTP. 41 | else if ($user->isOTP() && isset($_POST['otp'])) { 42 | $g = new GoogleAuthenticator(); 43 | // check if the submitted token is the right one and log in 44 | if ($g->checkCode($user->getSecret(),$_POST['otp'])) { 45 | // do log-in the user 46 | $user->doLogin(); 47 | //if the user clicked the "remember the token" checkbox, set the cookie 48 | if (isset($_POST['remember']) && $_POST['remember']) { 49 | $user->setOTPCookie(); 50 | } 51 | include("../tmpl/loggedin.php"); 52 | } 53 | //if the OTP is wrong, destroy the session and tell the user to try again 54 | else { 55 | session_destroy(); 56 | include("../tmpl/login-error.php"); 57 | } 58 | 59 | } 60 | // if the user is neither logged in nor in the OTP phase, show the login form 61 | else { 62 | session_destroy(); 63 | include("../tmpl/login.php"); 64 | } 65 | die(); 66 | } 67 | //if the username is set in _POST, then we assume the user filled in the login form. 68 | else if (isset($_POST['username'])) { 69 | // check if we can load the user (ie. the user exists in our db) 70 | $user = $users->loadUser($_POST['username']); 71 | if ($user) { 72 | //try to authenticate the password and start the session if it's correct. 73 | if ($user->auth($_POST['password'])) { 74 | $user->startSession(); 75 | //check if the user has a valid OTP cookie, so we don't have to 76 | // ask for the current token and can directly log in 77 | if ($user->hasValidOTPCookie()) { 78 | include("../tmpl/loggedin.php"); 79 | $user->doLogin(); 80 | } 81 | // try to get the users' secret from the db, 82 | // if he doesn't have one, generate one, store it and show it. 83 | else if (!$user->getSecret()) { 84 | include("../tmpl/loggedin.php"); 85 | 86 | $secret = $user->generateSecret(); 87 | $users->storeData($user); 88 | $user->doLogin(); 89 | include("../tmpl/show-qr.php"); 90 | } 91 | // if the user neither has a valid OTP cookie nor it's the first login 92 | // ask for the OTP 93 | else { 94 | $user->doOTP(); 95 | include("../tmpl/ask-for-otp.php"); 96 | } 97 | 98 | 99 | die(); 100 | } 101 | } 102 | // if we're here, something went wrong, destroy the session and show a login error 103 | session_destroy(); 104 | 105 | include("../tmpl/login-error.php"); 106 | die(); 107 | } 108 | // if neither a session nor tried to submit the login credentials -> login screen 109 | include("../tmpl/login.php"); 110 | 111 | 112 | ?> 113 | 114 | --------------------------------------------------------------------------------