Skip to content
Snippets Groups Projects
Unverified Commit 457efa70 authored by Pavel Břoušek's avatar Pavel Břoušek
Browse files

Initial commit

parent 43f7746c
No related branches found
No related tags found
No related merge requests found
......@@ -6,9 +6,50 @@ Heavily based on [Device cookie example](https://github.com/Rundiz/device-cookie
## Installation
```
composer install php-device-cookies
composer require cesnet/php-device-cookies
```
## Usage
Use the provided `DeviceCookies` class.
### Example
```php
$login = filter_input(INPUT_POST, 'login', FILTER_SANITIZE_EMAIL);
$password = filter_input(INPUT_POST, 'password');
if (!empty($login) && !empty($password)) {
$user = getUserByLogin($login);
$user_id = $user !== null ? $login : null;
$isPasswordCorrect = password_verify($password, $user->password);
$dbh = new PDO('dsn', 'username', 'password');
$deviceCookies = new DeviceCookies([
'Dbh' => $dbh,
'deviceCookieExpire' => 730, // The number of days that this cookie will be expired
'maxAttempt' => 10, // max number of authentication attempts allowed during "time period"
'secretKey' => 'SkfEED4aKrNWFUNqgqf6hrFsJQ6K6Jhh', // server’s secret cryptographic key
'timePeriod' => 60, // time period (in seconds)
]);
$result = $deviceCookies->check($login, $user_id, $isPasswordCorrect);
switch ($result) {
case DeviceCookiesResult::SUCCESS:
echo 'Logged in';
break;
case DeviceCookiesResult::REJECT:
case DeviceCookiesResult::REJECT_VALID:
http_response_code(403);
exit;
case DeviceCookiesResult::WRONG_PASSWORD:
case DeviceCookiesResult::LOCKOUT:
http_response_code(401);
exit;
}
}
// This should be called from a cron job once a day.
$deviceCookies->cleanup($dbh);
```
{
"name": "cesnet/php-device-cookies",
"description": "Implementation of Device Cookies in PHP",
"authors": [
{
"name": "Pavel Břoušek",
"email": "brousek@ics.muni.cz"
}
],
"require": {
"php": ">=7.1.0"
}
}
db.sql 0 → 100644
CREATE TABLE IF NOT EXISTS `user_devicecookie_failedattempts` (
`attempt_id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT 'refer to users.id',
`login` varchar(255) DEFAULT NULL,
`datetime` datetime DEFAULT current_timestamp() COMMENT 'failed authentication on date/time',
`devicecookie_nonce` varchar(50) DEFAULT NULL COMMENT 'device cookie NONCE (if present).',
`devicecookie_signature` longtext DEFAULT NULL COMMENT 'device cookie signature (if present).',
PRIMARY KEY (`attempt_id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='contain login failed attempt for existing users.';
CREATE TABLE IF NOT EXISTS `user_devicecookie_lockout` (
`lockout_id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT 'refer to users.id',
`devicecookie_nonce` varchar(50) DEFAULT NULL COMMENT 'device cookie NONCE.',
`devicecookie_signature` longtext DEFAULT NULL COMMENT 'device cookie signature.',
`lockout_untrusted_clients` int(1) NOT NULL DEFAULT 0 COMMENT '0=just lockout selected device cookie, 1=lockout all untrusted clients.',
`lockout_until` datetime DEFAULT NULL COMMENT 'lockout selected user (user_id) until date/time.',
PRIMARY KEY (`lockout_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='contain user account lockout.';
parameters:
sets:
- 'clean-code'
- 'common'
- 'psr12'
skip:
PhpCsFixer\Fixer\Operator\NotOperatorWithSuccessorSpaceFixer: ~
services:
<?php
namespace DeviceCookies;
/**
* Device cookies check result.
*/
abstract class DeviceCookieResult
{
/**
* Untrusted clients or invalid device cookie and is in lockout
*/
public const REJECT = 'reject';
/**
* There is valid device cookie but entered wrong credentials too many attempts until gets lockout
*/
public const REJECT_VALID = 'rejectvalid';
/**
* Able to continue authentication
*/
public const AUTHENTICATE = 'authenticate';
/**
* Logged in successfully
*/
public const SUCCESS = 'success';
/**
* Incorrect login or password (threshold has not been reached)
*/
public const WRONG_PASSWORD = 'wrongpassword';
/**
* Incorrect login or password too many times resulted in lockout
*/
public const LOCKOUT = 'lockout';
}
<?php
namespace DeviceCookies;
use DeviceCookies\Models\UserDeviceCookieFailedAttempts;
use DeviceCookies\Models\UserDeviceCookieLockout;
/**
* Device cookies class for help prevent brute-force attack.
* @see https://www.owasp.org/index.php/Slow_Down_Online_Guessing_Attacks_with_Device_Cookies
*/
class DeviceCookies
{
/**
* @var \PDO
*/
protected $Dbh;
/**
* @var string The name of device cookie.
*/
protected $deviceCookieName = 'deviceCookie';
/**
* @var int The number of days that this cookie will be expired.
*/
protected $deviceCookieExpire = 730;
/**
* @var int Current failed attempts with in time period.
*/
protected $currentFailedAttempt = 0;
/**
* @var array|null Contain lockout result object from `$UserDeviceCookieLockout->isInLockoutList()` method.
*/
protected $lockoutResult;
/**
* @var int Max number of authentication attempts allowed during "time period".
*/
protected $maxAttempt = 10;
/**
* @var string Server’s secret cryptographic key.
*/
protected $secretKey = 'SkfEED4aKrNWFUNqgqf6hrFsJQ6K6Jhh';
/**
* @var int Time period (in seconds).
*/
protected $timePeriod = 60;
/**
* Class constructor.
*
* @param array $options The options in associative array format.
*/
public function __construct(array $options)
{
foreach ($options as $option => $value) {
if (property_exists($this, $option)) {
$this->{$option} = $value;
}
}// endforeach;
unset($option, $value);
}
/**
* Check for brute force.
* @return DeviceCookiesResult What to do.
*/
public function check(string $login, ?string $userId, bool $isPasswordCorrect)
{
$entrypoint = $this->checkEntryPoint($login, $userId);
if ($entrypoint !== DeviceCookiesResult::AUTHENTICATE) {
return $entrypoint;
}
return $this->checkAuthenticate($login, $isPasswordCorrect);
}
/**
* Garbage collection to remove old data that is no longer used from DB.
*/
public function cleanup($dbh)
{
$sql = 'DELETE FROM `user_devicecookie_failedattempts` WHERE `datetime` < NOW() - INTERVAL :time_period SECOND';
$Sth = $dbh->prepare($sql);
$Sth->bindValue(':time_period', $this->timePeriod + 10);
$Sth->execute();
$affected1 = $Sth->rowCount();
$sql = 'DELETE FROM `user_devicecookie_lockout` WHERE `lockout_until` < NOW()';
$Sth = $dbh->prepare($sql);
$Sth->execute();
$affected2 = $Sth->rowCount();
return $affected1 + $affected2;
}
private function checkAuthenticate(string $login, ?string $userId, bool $isPasswordCorrect)
{
// 1. check user credentials
if ($isPasswordCorrect) {
// 2. if credentials are valid.
// a. issue new device cookie to user’s client
$this->issueNewDeviceCookie($login);
// b. proceed with authenticated user
$output = DeviceCookiesResult::SUCCESS;
} else {
// 3. else
// a. register failed authentication attempt
if ($userId !== null) {
$this->registerFailedAuth($login, $userId);
}
// b. finish with failed user’s authentication
if (
is_numeric($this->currentFailedAttempt) &&
$this->currentFailedAttempt > 0 &&
is_numeric($this->maxAttempt) &&
is_numeric($this->timePeriod)
) {
$output = DeviceCookiesResult::LOCKOUT;
} else {
$output = DeviceCookiesResult::WRONG_PASSWORD;
}
}
return $output;
}
/**
* Entry point for authentication request
*
* @param string $login The login ID such as email.
* @return DeviceCookiesResult What to do next.
*/
private function checkEntryPoint(string $login, ?string $userId): string
{
$UserDeviceCookieLockout = new UserDeviceCookieLockout($this->Dbh);
$output = '';
if ($this->hasDeviceCookie() === true) {
// 1. if the incoming request contains a device cookie.
// --- a. validate device cookie
$validateDeviceCookieResult = $this->validateDeviceCookie($login);
if ($validateDeviceCookieResult !== true) {
// b. if device cookie is not valid.
// proceed to step 2.
$this->removeDeviceCookie();
$step2 = true;
} elseif ($UserDeviceCookieLockout->isInLockoutList($this->getDeviceCookie()) === true) {
// c. if the device cookie is in the lockout list (valid but in lockout list).
// reject authentication attempt∎
$output = DeviceCookiesResult::REJECT_VALID;
$this->lockoutResult = $UserDeviceCookieLockout->getLockoutResult();
} else {
// d. else
// authenticate user∎
$output = DeviceCookiesResult::AUTHENTICATE;
}
} else {
$step2 = true;
}// endif;
if (isset($step2) && $step2 === true) {
if ($UserDeviceCookieLockout->isInLockoutList(null, $userId) === true) {
// 2. if authentication from untrusted clients is locked out for the specific user.
// reject authentication attempt∎
$output = DeviceCookiesResult::REJECT;
$this->lockoutResult = $UserDeviceCookieLockout->getLockoutResult();
} else {
// 3. else
// authenticate user∎
$output = DeviceCookiesResult::AUTHENTICATE;
}// endif;
} else {
if (empty($output)) {
// i don't think someone will be in this condition.
$output = DeviceCookiesResult::REJECT;
$this->lockoutResult = $UserDeviceCookieLockout->getLockoutResult();
}
}// endif;
return $output;
}
/**
* Get device cookie content
*
* @return string Return cookie value or content.
*/
private function getDeviceCookie(): string
{
if ($this->hasDeviceCookie() === true) {
return $_COOKIE[$this->deviceCookieName];
}
return '';
}
/**
* Get device cookie as array.
*
* @param string|null $cookieValue The cookie value. Leave null to get it from cookie variable.
*@return array Return array where 0 is login, 1 is nonce, 2 is signature.
*/
private function getDeviceCookieArray(string $cookieValue = null): array
{
if ($cookieValue === null) {
$cookieValue = $this->getDeviceCookie();
}
$exploded = explode(',', $cookieValue);
if (is_array($exploded) && count($exploded) >= 3) {
$output = $exploded;
} else {
$output = [
'',
null,
null,
];
}
unset($cookieValue, $exploded);
return $output;
}
/**
* Check if the incoming request contains a device cookie.
*
* This is just check that there is device cookie or not. It was not check for valid or invalid device cookie.
*
* @return bool Return `true` if there is device cookie. Return `false` if not.
*/
private function hasDeviceCookie(): bool
{
if (isset($_COOKIE[$this->deviceCookieName])) {
return true;
}
return false;
}
/**
* Issue new device cookie to user’s client.
*
* Issue a browser cookie with a value.
*
* @param string $login The login name (or internal ID).
*/
private function issueNewDeviceCookie(string $login)
{
$nonce = $this->generateNonce();
$signature = $this->getHmacSignature($login, $nonce);
setcookie(
$this->deviceCookieName,
$login . ',' . $nonce . ',' . $signature,
time() + ($this->deviceCookieExpire * 24 * 60 * 60),
'/'
);
}
/**
* Register failed authentication attempt.
*/
private function registerFailedAuth(string $login, ?string $userId)
{
$data = [
'login' => $login,
'user_id' => $userId,
];
// get additional data from previous cookie.
if (isset($data['login']) && $this->validateDeviceCookie($data['login']) === true) {
// if a valid device cookie presented.
$validDeviceCookie = true; // mark that valid device cookie is presented.
list($login, $nonce, $signature) = $this->getDeviceCookieArray();
$data['devicecookie_nonce'] = $nonce;
$data['devicecookie_signature'] = $signature;
unset($login, $nonce, $signature);
}
// sanitize $data
if (isset($data['devicecookie_nonce']) && empty($data['devicecookie_nonce'])) {
$data['devicecookie_nonce'] = null;
unset($validDeviceCookie);
}
if (isset($data['devicecookie_signature']) && empty($data['devicecookie_signature'])) {
$data['devicecookie_signature'] = null;
unset($validDeviceCookie);
}
// 1. register a failed authentication attempt
$UserDeviceCookieFailedAttempts = new UserDeviceCookieFailedAttempts($this->Dbh);
$UserDeviceCookieFailedAttempts->addFailedAttempt($data);
// 2. depending on whether a valid device cookie is present in the request,
// count the number of failed authentication attempts within period T
if (!isset($data['devicecookie_nonce']) || !isset($data['devicecookie_signature'])) {
// a. all untrusted clients
$where = [];
$where['devicecookie_signature'] = null;
$failedAttempts = $UserDeviceCookieFailedAttempts->countFailedAttemptInPeriod($this->timePeriod, $where);
} else {
// b. a specific device cookie
$where = [];
$where['devicecookie_signature'] = $data['devicecookie_signature'];
$failedAttempts = $UserDeviceCookieFailedAttempts->countFailedAttemptInPeriod($this->timePeriod, $where);
}
$this->currentFailedAttempt = $failedAttempts;
unset($UserDeviceCookieFailedAttempts, $where);
// 3. if "number of failed attempts within period T" > N
if ($failedAttempts > $this->maxAttempt) {
$dataUpdate = [];
$dataUpdate['user_id'] = $data['user_id'];
$Datetime = new \Datetime();
$Datetime->add(new \DateInterval('PT' . $this->timePeriod . 'S'));
$dataUpdate['lockout_until'] = $Datetime->format('Y-m-d H:i:s');
unset($Datetime);
if (
isset($validDeviceCookie) &&
$validDeviceCookie === true
) {
// a. if a valid device cookie is presented
// put the device cookie into the lockout list for device cookies until now+T
$dataUpdate['devicecookie_nonce'] = $data['devicecookie_nonce'];
$dataUpdate['devicecookie_signature'] = $data['devicecookie_signature'];
} else {
// b. else
// lockout all authentication attempts for a specific user from all untrusted clients until now+T
$dataUpdate['lockout_untrusted_clients'] = 1;
}
$UserDeviceCookieLockout = new UserDeviceCookieLockout($this->Dbh);
$UserDeviceCookieLockout->addUpdateLockoutList($dataUpdate);
unset($UserDeviceCookieLockout);
}
}
/**
* Remove a device cookie.
*/
private function removeDeviceCookie()
{
setcookie($this->deviceCookieName, '', time() - ($this->deviceCookieExpire * 24 * 60 * 60), '/');
}
/**
* Validate device cookie.
*
* @partam string $userLogin The login ID input from user.
* @return bool Return `true` if device cookie is correct and the `login` contained in the cookie is matches.
*/
private function validateDeviceCookie(string $userLogin): bool
{
if ($this->hasDeviceCookie() === true) {
$cookieValue = $_COOKIE[$this->deviceCookieName];
list($login, $nonce, $signature) = $this->getDeviceCookieArray($cookieValue);
if ($userLogin . ',' . $nonce . ',' . $signature === $cookieValue) {
// 1. Validate that the device cookie is formatted as described
if (
hash_equals(
$this->getHmacSignature($userLogin, $nonce),
$signature
)
) {
// 2. Validate that SIGNATURE == HMAC(secret-key, “LOGIN,NONCE”)
if ($login === $userLogin) {
// 3. Validate that LOGIN represents the user who is actually trying to authenticate
return true;
}
}
}
}
return false;
}
/**
* Generate nonce
*
* @param int $length The string length.
* @return string Return generated nonce.
*/
private function generateNonce(int $length = 32): string
{
return base64_encode(random_bytes($length));
}
/**
* Get HMAC signature content.
*
* @param string $login The login name.
* @param string $nonce NONCE.
* @return string Return generated string from HMAC.
*/
private function getHmacSignature(string $login, string $nonce): string
{
return hash_hmac('sha512', $login . ',' . $nonce, $this->secretKey);
}
}
<?php
namespace DeviceCookies\Models;
abstract class BaseModel
{
/**
* @var \PDO
*/
protected $Dbh;
/**
* @var \PDOStatement
*/
protected $Sth;
/**
* Class constructor.
*/
public function __construct(\PDO $dbh)
{
$this->Dbh = $dbh;
}
/**
* Delete data from DB table.
*
* @see https://github.com/doctrine/dbal/blob/master/lib/Doctrine/DBAL/Connection.php#L643
* @param string $tableName The table name. This table name will NOT auto add prefix.
* @param array $identifier The identifier for use in `WHERE` statement.
* @return bool Return PDOStatement::execute(). Return `true` on success, `false` for otherwise.
* @throws \InvalidArgumentException Throw the error if `$identifier` is incorrect value.
*/
public function delete(string $tableName, array $identifier)
{
if (empty($identifier)) {
throw new \InvalidArgumentException(
'The argument $identifier is required associative array column - value pairs.'
);
}
$columns = [];
$placeholders = [];
$values = [];
$conditions = [];
foreach ($identifier as $columnName => $value) {
$columns[] = '`' . $columnName . '`';
$conditions[] = '`' . $columnName . '` = ?';
$values[] = $value;
}// endforeach;
unset($columnName, $value);
$sql = 'DELETE FROM `' . $tableName . '` WHERE ' . implode(' AND ', $conditions);
$this->Sth = $this->Dbh->prepare($sql);
unset($columns, $placeholders, $sql);
return $this->Sth->execute($values);
}
/**
* Insert data into DB table.
*
* @see https://github.com/doctrine/dbal/blob/master/lib/Doctrine/DBAL/Connection.php#L749
* @param string $tableName The table name. This table name will NOT auto add prefix.
* @param array $data The associative array where column name is the key and its value is the value pairs.
* @return bool Return PDOStatement::execute(). Return `true` on success, `false` for otherwise.
* @throws \InvalidArgumentException Throw the error if `$data` is invalid.
*/
public function insert(string $tableName, array $data): bool
{
if (empty($data)) {
throw new \InvalidArgumentException(
'The argument $data is required associative array column - value pairs.'
);
}
$columns = [];
$placeholders = [];
$values = [];
foreach ($data as $columnName => $value) {
$columns[] = '`' . $columnName . '`';
$placeholders[] = '?';
$values[] = $value;
}// endforeach;
unset($columnName, $value);
$sql = 'INSERT INTO `' . $tableName . '` (' . implode(', ', $columns) . ') VALUES ('
. implode(', ', $placeholders) . ')';
$this->Sth = $this->Dbh->prepare($sql);
unset($columns, $placeholders, $sql);
return $this->Sth->execute($values);
}
/**
* Get PDO statement after called `insert()`, `update()`, `delete()`.
*
* @return \PDOStatement|null Return `\PDOStatement` object if exists, `null` if not exists.
*/
public function PDOStatement()
{
return $this->Sth;
}
/**
* Update data into DB table.
*
* @see https://github.com/doctrine/dbal/blob/master/lib/Doctrine/DBAL/Connection.php#L714
* @param string $tableName The table name. This table name will NOT auto add prefix.
* @param array $data The associative array where column name is the key and its value is the value pairs.
* @param array $identifier The identifier for use in `WHERE` statement.
* @return bool Return PDOStatement::execute(). Return `true` on success, `false` for otherwise.
* @throws \InvalidArgumentException Throw the error if `$data` or `$identifier` is incorrect value.
*/
public function update(string $tableName, array $data, array $identifier): bool
{
if (empty($data)) {
throw new \InvalidArgumentException(
'The argument $data is required associative array column - value pairs.'
);
}
if (empty($identifier)) {
throw new \InvalidArgumentException(
'The argument $identifier is required associative array column - value pairs.'
);
}
$columns = [];
$placeholders = [];
$values = [];
$conditions = [];
foreach ($data as $columnName => $value) {
$columns[] = '`' . $columnName . '`';
$placeholders[] = '`' . $columnName . '` = ?';
$values[] = $value;
}// endforeach;
unset($columnName, $value);
foreach ($identifier as $columnName => $value) {
$columns[] = '`' . $columnName . '`';
$conditions[] = '`' . $columnName . '` = ?';
$values[] = $value;
}// endforeach;
unset($columnName, $value);
$sql = 'UPDATE `' . $tableName . '` SET ' . implode(', ', $placeholders) . ' WHERE '
. implode(' AND ', $conditions);
$this->Sth = $this->Dbh->prepare($sql);
unset($columns, $placeholders, $sql);
return $this->Sth->execute($values);
}
}
<?php
namespace DeviceCookies\Models;
/**
* User device cookie failed attempts model.
*/
class UserDeviceCookieFailedAttempts extends BaseModel
{
protected $tableName = 'user_devicecookie_failedattempts';
/**
* Add/register failed authentication attempt.
*
* @param array $data The associative array where key is field.
* @throws \InvalidArgumentException Throw the error if `$data` is invalid.
*/
public function addFailedAttempt(array $data)
{
if (!isset($data['user_id'])) {
throw new \InvalidArgumentException('The `$data` must contain `user_id` in the array key.');
}
$this->insert($this->tableName, $data);
}
/**
* Count the number of failed authentication within period.
*
* @param int $timePeriod The time period.
* @param array $where The associative array where key is field.
* @return int Return total number of failed authentication counted.
*/
public function countFailedAttemptInPeriod(int $timePeriod, array $where = []): int
{
$sql = 'SELECT COUNT(*) AS `total_failed` FROM `' . $this->tableName
. '`WHERE `datetime` >= NOW() - INTERVAL :time_period SECOND';
$values = [];
$values[':time_period'] = $timePeriod;
foreach ($where as $field => $value) {
$sql .= ' AND ';
if ($value === null) {
$sql .= ' `' . $field . '` IS NULL';
} else {
$sql .= ' `' . $field . '` = :' . $field;
$values[':' . $field] = $value;
}
}// endforeach;
unset($field, $value);
$this->Sth = $this->Dbh->prepare($sql);
foreach ($values as $placeholder => $value) {
$this->Sth->bindValue($placeholder, $value);
}// endforeach;
unset($placeholder, $sql, $value, $values);
$this->Sth->execute();
$result = $this->Sth->fetchObject();
if (is_object($result) && isset($result->total_failed)) {
return intval($result->total_failed);
}
return 0;
}
}
<?php
namespace DeviceCookies\Models;
/**
* User device cookie lockout model.
*/
class UserDeviceCookieLockout extends BaseModel
{
/**
* @var array|null Contain lockout result object from `isInLockoutList()` method.
*/
protected $lockoutResult;
protected $tableName = 'user_devicecookie_lockout';
/**
* Put out the device cookie in the lockout list or lockout all untrusted clients.
*
* Check first that the specific data is already exists then update the data, otherwise add the data.
*
* @param array $data The associative array where key is field.
* @throws \InvalidArgumentException Throw the error if `$data` is invalid.
*/
public function addUpdateLockoutList(array $data)
{
if (!isset($data['user_id'])) {
throw new \InvalidArgumentException('The `$data` must contain `user_id` in the array key.');
}
if (!isset($data['lockout_until'])) {
throw new \InvalidArgumentException('The `$data` must contain `lockout_until` in the array key.');
}
if (!isset($data['devicecookie_signature']) && !isset($data['lockout_untrusted_clients'])) {
throw new \InvalidArgumentException(
'The `$data` must contain `devicecookie_signature` OR `lockout_untrusted_clients` in the array key.'
);
}
// check that data already exists in DB.
$sql = 'SELECT `user_id`, `devicecookie_nonce`, `devicecookie_signature`, `lockout_untrusted_clients`,'
. ' `lockout_until` FROM `' . $this->tableName . '` WHERE `user_id` = :user_id';
$where = [];
$where[':user_id'] = $data['user_id'];
if (isset($data['devicecookie_signature'])) {
$sql .= ' AND`devicecookie_signature` = :devicecookie_signature';
$where[':devicecookie_signature'] = $data['devicecookie_signature'];
}
if (isset($data['lockout_untrusted_clients'])) {
$sql .= ' AND `lockout_untrusted_clients` = :lockout_untrusted_clients';
$where[':lockout_untrusted_clients'] = $data['lockout_untrusted_clients'];
}
$this->Sth = $this->Dbh->prepare($sql);
foreach ($where as $field => $value) {
$this->Sth->bindValue($field, $value);
}// endforeach;
$this->Sth->execute();
$result = $this->Sth->fetchAll();
if (is_array($result) && count($result) >= 1) {
$useInsert = false;
} else {
$useInsert = true;
}
// if not exists use insert, otherwise use update.
if (isset($useInsert) && $useInsert === true) {
$this->insert($this->tableName, $data);
} else {
$where = [];
$where['user_id'] = $data['user_id'];
$dataUpdate = [];
$dataUpdate['lockout_until'] = $data['lockout_until'];
if (isset($data['devicecookie_signature'])) {
$where['devicecookie_signature'] = $data['devicecookie_signature'];
}
if (isset($data['lockout_untrusted_clients'])) {
$where['lockout_untrusted_clients'] = $data['lockout_untrusted_clients'];
}
// reset
$this->update($this->tableName, $dataUpdate, $where);
}
}
/**
* Get result object after called to `isInLockoutList()` method.
*
*@return array Return result object if required method was called, return null if nothing.
*/
public function getLockoutResult(): array
{
if (is_array($this->lockoutResult)) {
return $this->lockoutResult;
}
return [];
}
/**
* Check if current user is in lockout list.
*
* If device cookie content is specified, then it will check for specific device cookie.<br>
* If device cookie is `null` then it will check for untrusted clients.
*
* @param string $deviceCookie The device cookie content.
* @param int $user_id The user id. For checking from untrusted clients.
* @return bool Return `true` if current user is in the lockout list, return `false` for otherwise.
*/
public function isInLockoutList(string $deviceCookie = null, int $user_id = null): bool
{
$sql = 'SELECT `user_id`, `devicecookie_nonce`, `devicecookie_signature`, `lockout_untrusted_clients`,'
. ' `lockout_until` FROM `' . $this->tableName . '` WHERE `lockout_until` >= NOW()';
$where = [];
if ($deviceCookie !== null) {
// if there is device cookie.
$exploded = explode(',', $deviceCookie);
if (is_array($exploded) && count($exploded) >= 3) {
list($login, $nonce, $signature) = $exploded;
} else {
list($login, $nonce, $signature) = [
'',
null,
null,
];
}
unset($exploded);
if ($signature === null) {
$sql .= ' AND `devicecookie_signature` IS NULL';
} else {
$sql .= ' AND `devicecookie_signature` = :devicecookie_signature';
$where[':devicecookie_signature'] = $signature;
}
unset($login, $nonce, $signature);
} else {
// if there is NO device cookie.
if ($user_id !== null && !empty($user_id)) {
// if there is a specific user_id.
$sql .= ' AND `user_id` = :user_id';
$where[':user_id'] = $user_id;
} elseif ($user_id === null) {
// if no specific user_id.
// this is may because of client enter invalid user login ID that cause this to be null.
// just return false and let them authenticate.
unset($sql, $where);
return false;
}
$sql .= ' AND `lockout_untrusted_clients` = :lockout_untrusted_clients';
$where[':lockout_untrusted_clients'] = 1;
}// endif;
$this->Sth = $this->Dbh->prepare($sql);
foreach ($where as $field => $value) {
$this->Sth->bindValue($field, $value);
}// endforeach;
unset($field, $sql, $value, $where);
$this->Sth->execute();
$result = $this->Sth->fetchAll();
if (is_array($result) && count($result) >= 1) {
// if found in lockout list
$this->lockoutResult = $result;
unset($result);
return true;
}
unset($result);
return false;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment