forked from irhosseinz/User-Manager
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Support for Google Authenticator (Two Factor Verification)
- Loading branch information
1 parent
c55771e
commit 90fb67d
Showing
7 changed files
with
454 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
<?php | ||
include_once('includes/config.php'); | ||
include_once('includes/umf.php'); | ||
include_once('includes/authenticator.php'); | ||
if(!isset($_SESSION['UM_DATA'])){ | ||
header('Location: login.php'); | ||
exit; | ||
} | ||
$SUCCESS=false; | ||
$ERROR=false; | ||
if(isset($_POST['code'])){ | ||
$ga = new PHPGangsta_GoogleAuthenticator(); | ||
|
||
$ok=$ga->verifyCode($_POST['secret'], $_POST['code'], UM_AUTHENTICATOR_TOL); | ||
|
||
if($ok){ | ||
$st=$DB->prepare("update users set authen_secret=? where _id=?"); | ||
$st->bind_param('si',$_POST['secret'],$_SESSION['UM_DATA']['_id']); | ||
$st->execute(); | ||
$SUCCESS="Setup Completed. Your Secret Is <b>{$_POST['secret']}</b>, Please write this down SomeWhere Safe"; | ||
}else{ | ||
$ERROR="Wrong Code!"; | ||
} | ||
} | ||
?> | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||
<meta name="description" content=""> | ||
<meta name="author" content=""> | ||
<link rel="icon" href="/img/favicon.png"> | ||
<title>2-Step Verification</title> | ||
<link href="css/bootstrap.min.css" rel="stylesheet"> | ||
<link href="css/dashboard.css" rel="stylesheet"> | ||
<script src="js/jquery-3.4.1.min.js"></script> | ||
<script src="js/bootstrap.bundle.js"></script> | ||
<script src="/js/jquery.validate.min.js"></script> | ||
<script type="text/javascript"> | ||
$( document ).ready(function(){ | ||
|
||
}); | ||
</script> | ||
</head> | ||
|
||
<body> | ||
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0"> | ||
<a class="navbar-brand col-sm-3 col-md-2 col-4 mr-0" href="#"><?php echo $UM_CONFIG['TITLE'];?></a> | ||
<ul class="navbar-nav px-3"> | ||
<li class="nav-item text-nowrap"> | ||
<a class="nav-link" href="login.php?logout">Sign out</a> | ||
</li> | ||
</ul> | ||
</nav> | ||
|
||
<div class="container-fluid"> | ||
<div class="row"> | ||
<nav class="col-md-2 col-sm-1 col-1 d-inline d-md-block bg-light sidebar" style="top:48px"> | ||
<div class="sidebar-sticky"> | ||
<ul class="nav flex-column"> | ||
<?php | ||
include('includes/dashboard_sidebar.php'); | ||
?> | ||
</ul> | ||
</div> | ||
</nav> | ||
|
||
<main role="main" class="col-md-9 offset-md-2 col-sm-11 offset-sm-1 col-11 offset-1 pt-3 px-4"> | ||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom"> | ||
<div class="container"> | ||
<?php | ||
if(@$ERROR){ | ||
echo '<div class="alert alert-danger" role="alert">'.$ERROR.'</div>'; | ||
}else if(@$SUCCESS){ | ||
echo '<div class="alert alert-success" role="alert">'.$SUCCESS.'</div>'; | ||
} | ||
|
||
$DATA=$DB->query("select * from users where _id={$_SESSION['UM_DATA']['_id']}")->fetch_assoc(); | ||
if($DATA['authen_secret']){ | ||
echo '<div class="alert alert-primary" role="alert">2FAuthentication is Activated</div>'; | ||
}else{ | ||
$ga = new PHPGangsta_GoogleAuthenticator(); | ||
$secret = $_POST['secret']?$_POST['secret']:$ga->createSecret(); | ||
|
||
$qrCodeUrl = $ga->getQRCodeGoogleUrl($UM_CONFIG['TITLE']."({$DATA['email']})", $secret); | ||
?> | ||
<form id="regForm1" action="authenticator.php" method="post"> | ||
<input type="hidden" value="<?php echo $secret;?>" name="secret"/> | ||
<p> | ||
<h4>Setup 2 Factor Verification</h4> | ||
<ul> | ||
<li>Get the Authenticator from The <a href="https://apps.apple.com/app/google-authenticator/id388497605" target="_blank">App store (IOS)</a> or <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank">Google Play (Android)</a></li> | ||
<li>After opening App Click on <b>Plus or Add button</b> And Then Select <b>Scan a QR Code</b></li> | ||
<li>Scan Below QRCode</li> | ||
<img src="<?php echo $qrCodeUrl;?>"/> | ||
<li>Enter Generated Code in Below Field</li> | ||
</ul> | ||
</p> | ||
<div class="form-group"> | ||
<input type="text" class="form-control" name="code" placeholder="000000" autocomplete="off"/> | ||
</div> | ||
<br/><input type="submit" class="btn btn-primary my-1" value="Verify" name="verify"/> | ||
</form> | ||
<?php | ||
} | ||
?> | ||
</div> | ||
</div> | ||
</main> | ||
</div> | ||
</div> | ||
<script src="js/feather.min.js"></script> | ||
<script> | ||
feather.replace() | ||
</script> | ||
<hr> | ||
<footer> | ||
<p class="text-center text-ltr engFont">© <a href="http://h.sandbad.biz/User-Manager/" target="_blank">UserManager</a></p> | ||
</footer> | ||
</body> | ||
</html> | ||
<?php | ||
$DB->close(); | ||
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
<?php | ||
|
||
/** | ||
* PHP Class for handling Google Authenticator 2-factor authentication. | ||
* | ||
* @author Michael Kliewe | ||
* @copyright 2012 Michael Kliewe | ||
* @license http://www.opensource.org/licenses/bsd-license.php BSD License | ||
* | ||
* @link http://www.phpgangsta.de/ | ||
*/ | ||
class PHPGangsta_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(); | ||
|
||
// Valid secret lengths are 80 to 640 bits | ||
if ($secretLength < 16 || $secretLength > 128) { | ||
throw new Exception('Bad secret length'); | ||
} | ||
$secret = ''; | ||
$rnd = false; | ||
if (function_exists('random_bytes')) { | ||
$rnd = random_bytes($secretLength); | ||
} elseif (function_exists('mcrypt_create_iv')) { | ||
$rnd = mcrypt_create_iv($secretLength, MCRYPT_DEV_URANDOM); | ||
} elseif (function_exists('openssl_random_pseudo_bytes')) { | ||
$rnd = openssl_random_pseudo_bytes($secretLength, $cryptoStrong); | ||
if (!$cryptoStrong) { | ||
$rnd = false; | ||
} | ||
} | ||
if ($rnd !== false) { | ||
for ($i = 0; $i < $secretLength; ++$i) { | ||
$secret .= $validChars[ord($rnd[$i]) & 31]; | ||
} | ||
} else { | ||
throw new Exception('No source of secure random'); | ||
} | ||
|
||
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 | ||
* @param array $params | ||
* | ||
* @return string | ||
*/ | ||
public function getQRCodeGoogleUrl($name, $secret, $title = null, $params = array()) | ||
{ | ||
$width = !empty($params['width']) && (int) $params['width'] > 0 ? (int) $params['width'] : 200; | ||
$height = !empty($params['height']) && (int) $params['height'] > 0 ? (int) $params['height'] : 200; | ||
$level = !empty($params['level']) && array_search($params['level'], array('L', 'M', 'Q', 'H')) !== false ? $params['level'] : 'M'; | ||
|
||
$urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.''); | ||
if (isset($title)) { | ||
$urlencoded .= urlencode('&issuer='.urlencode($title)); | ||
} | ||
|
||
return "https://api.qrserver.com/v1/create-qr-code/?data=$urlencoded&size=${width}x${height}&ecc=$level"; | ||
} | ||
|
||
/** | ||
* 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); | ||
} | ||
|
||
if (strlen($code) != 6) { | ||
return false; | ||
} | ||
|
||
for ($i = -$discrepancy; $i <= $discrepancy; ++$i) { | ||
$calculatedCode = $this->getCode($secret, $currentTimeSlice + $i); | ||
if ($this->timingSafeEquals($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; | ||
} | ||
|
||
/** | ||
* 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 | ||
); | ||
} | ||
|
||
/** | ||
* A timing safe equals comparison | ||
* more info here: http://blog.ircmaxell.com/2014/11/its-all-about-time.html. | ||
* | ||
* @param string $safeString The internal (safe) value to be checked | ||
* @param string $userString The user submitted (unsafe) value | ||
* | ||
* @return bool True if the two strings are identical | ||
*/ | ||
private function timingSafeEquals($safeString, $userString) | ||
{ | ||
if (function_exists('hash_equals')) { | ||
return hash_equals($safeString, $userString); | ||
} | ||
$safeLen = strlen($safeString); | ||
$userLen = strlen($userString); | ||
|
||
if ($userLen != $safeLen) { | ||
return false; | ||
} | ||
|
||
$result = 0; | ||
|
||
for ($i = 0; $i < $userLen; ++$i) { | ||
$result |= (ord($safeString[$i]) ^ ord($userString[$i])); | ||
} | ||
|
||
// They are only identical strings if $result is exactly 0... | ||
return $result === 0; | ||
} | ||
} |
Oops, something went wrong.