Proper Oauth for Google Home
This commit is contained in:
parent
1af11f3f58
commit
fef3c1e57f
@ -13,6 +13,7 @@ $router->any('/logout', 'Logout');
|
|||||||
$router->any('/automation', 'Automation');
|
$router->any('/automation', 'Automation');
|
||||||
$router->any('/setting', 'Setting');
|
$router->any('/setting', 'Setting');
|
||||||
$router->any('/ajax', 'Ajax');
|
$router->any('/ajax', 'Ajax');
|
||||||
|
$router->any('/oauth', 'Oauth');
|
||||||
|
|
||||||
$router->post('/api/login', 'AuthApi@login');
|
$router->post('/api/login', 'AuthApi@login');
|
||||||
$router->post('/api/logout', 'AuthApi@logout');
|
$router->post('/api/logout', 'AuthApi@logout');
|
||||||
@ -20,7 +21,7 @@ $router->post('/api/logout', 'AuthApi@logout');
|
|||||||
$router->get('/api/devices', 'DevicesApi@default');
|
$router->get('/api/devices', 'DevicesApi@default');
|
||||||
$router->get('/api/rooms', 'RoomsApi@default');
|
$router->get('/api/rooms', 'RoomsApi@default');
|
||||||
|
|
||||||
$router->get('/api/HA/auth', 'GoogleHomeApi@autorize');
|
$router->any('/api/HA/auth', 'Oauth');
|
||||||
$router->any('/api/HA', 'GoogleHomeApi@response');
|
$router->any('/api/HA', 'GoogleHomeApi@response');
|
||||||
|
|
||||||
// examples
|
// examples
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
class GoogleHomeApi{
|
class GoogleHomeApi{
|
||||||
static function response(){
|
static function response(){
|
||||||
|
//$this->requireAuth();
|
||||||
$json = file_get_contents('php://input');
|
$json = file_get_contents('php://input');
|
||||||
$obj = json_decode($json, true);
|
$obj = json_decode($json, true);
|
||||||
|
|
||||||
|
26
app/api/RecordApi.php
Normal file
26
app/api/RecordApi.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class RecordApi extends ApiController{
|
||||||
|
|
||||||
|
public function default(){
|
||||||
|
//$this->requireAuth();
|
||||||
|
$response = [];
|
||||||
|
$roomIds = [];
|
||||||
|
$roomsData = RoomManager::getRoomsDefault();
|
||||||
|
|
||||||
|
foreach ($roomsData as $roomKey => $room) {
|
||||||
|
$roomIds[] = $room['room_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$subDevicesData = SubDeviceManager::getSubdevicesByRoomIds($roomIds);
|
||||||
|
|
||||||
|
foreach ($roomsData as $roomKey => $roomData) {
|
||||||
|
$response[] = [
|
||||||
|
'room_id' => $roomData['room_id'],
|
||||||
|
'name' => $roomData['name'],
|
||||||
|
'widgets' => isset($subDevicesData[$roomData['room_id']]) ? $subDevicesData[$roomData['room_id']] : [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$this->response($response);
|
||||||
|
}
|
||||||
|
}
|
75
app/controllers/oauthController.php
Normal file
75
app/controllers/oauthController.php
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
global $userManager;
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
isset($_POST['username']) &&
|
||||||
|
$_POST['username'] != '' &&
|
||||||
|
isset($_POST['password']) &&
|
||||||
|
$_POST['password'] != ''
|
||||||
|
){
|
||||||
|
$ota = false;
|
||||||
|
$userName = $_POST['username'];
|
||||||
|
$userPassword = $_POST['password'];
|
||||||
|
$state = $_POST["state"];
|
||||||
|
$clientId = $_POST["clientId"];
|
||||||
|
$ota = $userManager->haveOtaEnabled($userName);
|
||||||
|
if ($ota == "") {
|
||||||
|
$token = (new AuthManager)->getToken($userName,$userPassword, $clientId);
|
||||||
|
if (!$token) {
|
||||||
|
throw new Exception("Auth failed", 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$get = [
|
||||||
|
"access_token"=>$token,
|
||||||
|
"token_type"=>"Bearer",
|
||||||
|
"state"=>$state,
|
||||||
|
];
|
||||||
|
|
||||||
|
header('Location: ' . $_POST["redirectUrl"] . '#' . http_build_query($get));
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['USERNAME'] = $userName;
|
||||||
|
$_SESSION['PASSWORD'] = $userPassword;
|
||||||
|
$_SESSION['OTA'] = $ota;
|
||||||
|
$_SESSION['STATE'] = $state;
|
||||||
|
$_SESSION['REDIRECT'] = $_POST["redirectUrl"];
|
||||||
|
$_SESSION['CLIENT'] = $clientId;
|
||||||
|
|
||||||
|
|
||||||
|
} else if (
|
||||||
|
isset($_POST['otaCode']) &&
|
||||||
|
$_POST['otaCode'] != ''
|
||||||
|
) {
|
||||||
|
$otaCode = $_POST['otaCode'];
|
||||||
|
$otaSecret = $_POST['otaSecret'];
|
||||||
|
|
||||||
|
$userName = $_SESSION['USERNAME'];
|
||||||
|
$userPassword = $_SESSION['PASSWORD'];
|
||||||
|
$ota = $_SESSION['OTA'];
|
||||||
|
$oauthState = $_SESSION['STATE'];
|
||||||
|
$oauthRedirect = $_SESSION['REDIRECT'];
|
||||||
|
$oauthClientId = $_SESSION['CLIENT'];
|
||||||
|
|
||||||
|
$ga = new PHPGangsta_GoogleAuthenticator();
|
||||||
|
$checkResult = $ga->verifyCode($otaSecret, $otaCode, 2); // 2 = 2*30sec clock tolerance
|
||||||
|
if ($checkResult) {
|
||||||
|
$token = (new AuthManager)->getToken($userName,$userPassword, $oauthClientId);
|
||||||
|
if (!$token) {
|
||||||
|
throw new Exception("Auth failed", 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$get = [
|
||||||
|
"access_token"=>$token,
|
||||||
|
"token_type"=>"Bearer",
|
||||||
|
"state"=>$oauthState,
|
||||||
|
];
|
||||||
|
|
||||||
|
header('Location: ' . $oauthRedirect . '#' . http_build_query($get));
|
||||||
|
echo 'OK';
|
||||||
|
} else {
|
||||||
|
echo 'FAILED';
|
||||||
|
}
|
||||||
|
die();
|
||||||
|
}
|
@ -14,9 +14,8 @@ if (isset($_POST) && !empty($_POST)){
|
|||||||
header('Location: ' . BASEURL . 'setting');
|
header('Location: ' . BASEURL . 'setting');
|
||||||
die();
|
die();
|
||||||
} else if (isset($_POST['submitEnableOta']) && $_POST['submitEnableOta'] != "") {
|
} else if (isset($_POST['submitEnableOta']) && $_POST['submitEnableOta'] != "") {
|
||||||
echo $otaCode = $_POST['otaCode'];
|
$otaCode = $_POST['otaCode'];
|
||||||
echo $otaSecret = $_POST['otaSecret'];
|
$otaSecret = $_POST['otaSecret'];
|
||||||
|
|
||||||
|
|
||||||
$ga = new PHPGangsta_GoogleAuthenticator();
|
$ga = new PHPGangsta_GoogleAuthenticator();
|
||||||
$checkResult = $ga->verifyCode($otaSecret, $otaCode, 2); // 2 = 2*30sec clock tolerance
|
$checkResult = $ga->verifyCode($otaSecret, $otaCode, 2); // 2 = 2*30sec clock tolerance
|
||||||
|
@ -12,6 +12,11 @@ class GoogleHome {
|
|||||||
|
|
||||||
//Google Compatibile Action Type
|
//Google Compatibile Action Type
|
||||||
$actionType = GoogleHomeDeviceTypes::getAction($subDeviceData['type']);
|
$actionType = GoogleHomeDeviceTypes::getAction($subDeviceData['type']);
|
||||||
|
|
||||||
|
if (strpos($deviceData['name'], 'Světlo') !== false || strpos($deviceData['name'], 'světlo') !== false) {
|
||||||
|
$actionType = 'action.devices.types.LIGHT';
|
||||||
|
}
|
||||||
|
|
||||||
$tempDevice = [
|
$tempDevice = [
|
||||||
'id' => (string) $subDeviceData['subdevice_id'],
|
'id' => (string) $subDeviceData['subdevice_id'],
|
||||||
'type' => $actionType,
|
'type' => $actionType,
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class AuthManager {
|
class AuthManager {
|
||||||
public function getToken($username, $password){
|
public function getToken($username, $password, $userAgent = null){
|
||||||
|
if ($userAgent == null) {
|
||||||
|
$userAgent = $this->headers['HTTP_USER_AGENT'];
|
||||||
|
}
|
||||||
|
|
||||||
$userManager = new UserManager();
|
$userManager = new UserManager();
|
||||||
if ($username != '' || $password != ''){
|
if ($username != '' || $password != ''){
|
||||||
$userLogedIn = $userManager->loginNew($username, $password);
|
$userLogedIn = $userManager->loginNew($username, $password);
|
||||||
@ -10,7 +14,11 @@ class AuthManager {
|
|||||||
// Create token header as a JSON string
|
// Create token header as a JSON string
|
||||||
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
|
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
|
||||||
// Create token payload as a JSON string
|
// Create token payload as a JSON string
|
||||||
$payload = json_encode(['user_id' => $userLogedIn]);
|
$payload = json_encode([
|
||||||
|
'user_id' => $userLogedIn,
|
||||||
|
'exp' => date('Y-m-d H:i:s',strtotime("+90 Days")),
|
||||||
|
'iat' => date('Y-m-d H:i:s',time()),
|
||||||
|
]);
|
||||||
// Encode Header to Base64Url String
|
// Encode Header to Base64Url String
|
||||||
$base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header));
|
$base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header));
|
||||||
// Encode Payload to Base64Url String
|
// Encode Payload to Base64Url String
|
||||||
@ -22,9 +30,19 @@ class AuthManager {
|
|||||||
// Create JWT
|
// Create JWT
|
||||||
$jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
|
$jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
|
||||||
|
|
||||||
|
|
||||||
|
$token = [
|
||||||
|
'user_id' => $userLogedIn,
|
||||||
|
'user_agent' => $userAgent,
|
||||||
|
'token' => $jwt,
|
||||||
|
'expire' => date('Y-m-d H:i:s',strtotime("+90 Days")),
|
||||||
|
'issued' => date('Y-m-d H:i:s',time()),
|
||||||
|
];
|
||||||
|
if (Db::add ('tokens', $token)){
|
||||||
return $jwt;
|
return $jwt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,5 +206,15 @@ class UserManager
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function setOta($otaCode, $otaSecret){
|
||||||
|
$ga = new PHPGangsta_GoogleAuthenticator();
|
||||||
|
$checkResult = $ga->verifyCode($otaSecret, $otaCode, 2); // 2 = 2*30sec clock tolerance
|
||||||
|
if ($checkResult) {
|
||||||
|
self::setUserData('ota', $otaSecret);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
30
app/views/Oauth.php
Normal file
30
app/views/Oauth.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
class Oauth extends Template
|
||||||
|
{
|
||||||
|
function __construct()
|
||||||
|
{
|
||||||
|
global $userManager;
|
||||||
|
global $lang;
|
||||||
|
|
||||||
|
$template = new Template('oauth');
|
||||||
|
$template->prepare('baseDir', BASEDIR);
|
||||||
|
$template->prepare('title', 'Home');
|
||||||
|
|
||||||
|
$template->prepare('lang', $lang);
|
||||||
|
|
||||||
|
if (isset($_GET['redirect_uri'])) {
|
||||||
|
$template->prepare('responseType', $_GET['response_type']);
|
||||||
|
$template->prepare('redirectUrl', $_GET['redirect_uri']);
|
||||||
|
$template->prepare('clientId', $_GET['client_id']);
|
||||||
|
$template->prepare('state', $_GET['state']);
|
||||||
|
} else {
|
||||||
|
$template->prepare('responseType', $_POST['responseType']);
|
||||||
|
$template->prepare('redirectUrl', $_POST['redirectUrl']);
|
||||||
|
$template->prepare('clientId', $_POST['clientId']);
|
||||||
|
$template->prepare('state', $_POST['state']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$template->render();
|
||||||
|
}
|
||||||
|
}
|
33
app/views/templates/oauth.phtml
Normal file
33
app/views/templates/oauth.phtml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<?php
|
||||||
|
$partial = new Partial('head');
|
||||||
|
$partial->prepare('baseDir',$BASEDIR);
|
||||||
|
$partial->render();
|
||||||
|
?>
|
||||||
|
<title><?php echo $TITLE ?></title>
|
||||||
|
</head>
|
||||||
|
<body class="no-transitions">
|
||||||
|
|
||||||
|
<?php
|
||||||
|
if (isset($ota) && $ota != '') {
|
||||||
|
$partial = new Partial('oauthLoginOta');
|
||||||
|
$partial->prepare('ota',$ota);
|
||||||
|
$partial->render();
|
||||||
|
} else {
|
||||||
|
$partial = new Partial('oauthLoginForm');
|
||||||
|
$partial->prepare('responseType',$RESPONSETYPE);
|
||||||
|
$partial->prepare('redirectUrl',$REDIRECTURL);
|
||||||
|
$partial->prepare('clientId',$CLIENTID);
|
||||||
|
$partial->prepare('state',$STATE);
|
||||||
|
$partial->render();
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$partial = new Partial('footer');
|
||||||
|
$partial->render();
|
||||||
|
?>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -18,9 +18,9 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<link rel="stylesheet" href="./css/main.css?v2">
|
<link rel="stylesheet" href="<?php echo $BASEDIR; ?>public/css/main.css?v2">
|
||||||
<link rel="stylesheet" href="./css/font-awesome.min.css">
|
<link rel="stylesheet" href="<?php echo $BASEDIR; ?>public/css/font-awesome.min.css">
|
||||||
<link rel="stylesheet" href="./css/modal.css">
|
<link rel="stylesheet" href="<?php echo $BASEDIR; ?>public/css/modal.css">
|
||||||
<link rel="stylesheet" href="./css/pre.css">
|
<link rel="stylesheet" href="<?php echo $BASEDIR; ?>public/css/pre.css">
|
||||||
<link rel="stylesheet" href="./css/loading.css">
|
<link rel="stylesheet" href="<?php echo $BASEDIR; ?>public/css/loading.css">
|
||||||
<link rel="stylesheet" href="./css/override.css">
|
<link rel="stylesheet" href="<?php echo $BASEDIR; ?>public/css/override.css">
|
||||||
|
22
app/views/templates/part/oauthLoginForm.phtml
Normal file
22
app/views/templates/part/oauthLoginForm.phtml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<div class="modal-container">
|
||||||
|
<div class="modal">
|
||||||
|
<h4 class="mb-4">Login</h4>
|
||||||
|
<form method="post">
|
||||||
|
<div class="field">
|
||||||
|
<div class="label">Name:</div>
|
||||||
|
<input class="input" type="text" name="username" placeholder="Jméno.."/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="label">Password:</div>
|
||||||
|
<input class="input" type="password" name="password" placeholder="Heslo.."/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="responseType" value="<?php echo $RESPONSETYPE; ?>"/>
|
||||||
|
<input type="hidden" name="redirectUrl" value="<?php echo $REDIRECTURL; ?>"/>
|
||||||
|
<input type="hidden" name="clientId" value="<?php echo $CLIENTID; ?>"/>
|
||||||
|
<input type="hidden" name="state" value="<?php echo $STATE; ?>"/>
|
||||||
|
|
||||||
|
<input type="submit" class="button" name="login" value="Login"/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
16
app/views/templates/part/oauthLoginOta.phtml
Normal file
16
app/views/templates/part/oauthLoginOta.phtml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<div class="modal-container">
|
||||||
|
<div class="modal">
|
||||||
|
<h4 class="mb-4">OTA</h4>
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="otaSecret" value="<?php echo $OTA; ?>"/>
|
||||||
|
<div class="field">
|
||||||
|
<div class="label">Code:</div>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
?>
|
||||||
|
<input class="input" type="text" name="otaCode" placeholder=""/>
|
||||||
|
</div>
|
||||||
|
<input type="submit" class="button" name="login" value="Login"/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -95,7 +95,6 @@
|
|||||||
<h4 class="mb-4"><?php $LANGMNG->echo('t_ota') ?></h4>
|
<h4 class="mb-4"><?php $LANGMNG->echo('t_ota') ?></h4>
|
||||||
<?php if (!empty($QRURL)) {?>
|
<?php if (!empty($QRURL)) {?>
|
||||||
<img src="<?php echo $QRURL;?>" />
|
<img src="<?php echo $QRURL;?>" />
|
||||||
<?php echo $OTACODE; ?>
|
|
||||||
<form method="post" action="setting">
|
<form method="post" action="setting">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="label"><?php $LANGMNG->echo('l_gooleAutenticatorOtaCode') ?>:</div>
|
<div class="label"><?php $LANGMNG->echo('l_gooleAutenticatorOtaCode') ?>:</div>
|
||||||
|
252
library/vendor/GoogleAuthenticator.php
vendored
252
library/vendor/GoogleAuthenticator.php
vendored
@ -1,252 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user