mirror of
https://github.com/delight-im/PHP-Auth.git
synced 2025-08-02 06:10:14 +02:00
Rewrite all SQL operations to use 'delight-im/db' instead of raw PDO
This commit is contained in:
541
src/Auth.php
541
src/Auth.php
@@ -10,6 +10,10 @@ namespace Delight\Auth;
|
||||
|
||||
use Delight\Cookie\Cookie;
|
||||
use Delight\Cookie\Session;
|
||||
use Delight\Db\PdoDatabase;
|
||||
use Delight\Db\PdoDsn;
|
||||
use Delight\Db\Throwable\Error;
|
||||
use Delight\Db\Throwable\IntegrityConstraintViolationException;
|
||||
|
||||
require __DIR__.'/Base64.php';
|
||||
require __DIR__.'/Exceptions.php';
|
||||
@@ -30,7 +34,7 @@ class Auth {
|
||||
const THROTTLE_ACTION_CONSUME_TOKEN = 'confirm_email';
|
||||
const HTTP_STATUS_CODE_TOO_MANY_REQUESTS = 429;
|
||||
|
||||
/** @var \PDO the database connection that will be used */
|
||||
/** @var PdoDatabase the database connection that will be used */
|
||||
private $db;
|
||||
/** @var boolean whether HTTPS (TLS/SSL) will be used (recommended) */
|
||||
private $useHttps;
|
||||
@@ -44,13 +48,25 @@ class Auth {
|
||||
private $throttlingTimeBucketSize;
|
||||
|
||||
/**
|
||||
* @param \PDO $databaseConnection the database connection that will be used
|
||||
* @param PdoDatabase|PdoDsn|\PDO $databaseConnection the database connection that will be used
|
||||
* @param bool $useHttps whether HTTPS (TLS/SSL) will be used (recommended)
|
||||
* @param bool $allowCookiesScriptAccess whether cookies should be accessible via client-side scripts (*not* recommended)
|
||||
* @param string $ipAddress the IP address that should be used instead of the default setting (if any), e.g. when behind a proxy
|
||||
*/
|
||||
public function __construct(\PDO $databaseConnection, $useHttps = false, $allowCookiesScriptAccess = false, $ipAddress = null) {
|
||||
$this->db = $databaseConnection;
|
||||
public function __construct($databaseConnection, $useHttps = false, $allowCookiesScriptAccess = false, $ipAddress = null) {
|
||||
if ($databaseConnection instanceof PdoDatabase) {
|
||||
$this->db = $databaseConnection;
|
||||
}
|
||||
elseif ($databaseConnection instanceof PdoDsn) {
|
||||
$this->db = PdoDatabase::fromDsn($databaseConnection);
|
||||
}
|
||||
elseif ($databaseConnection instanceof \PDO) {
|
||||
$this->db = PdoDatabase::fromPdo($databaseConnection, true);
|
||||
}
|
||||
else {
|
||||
throw new \InvalidArgumentException('The database connection must be an instance of either `PdoDatabase`, `PdoDsn` or `PDO`');
|
||||
}
|
||||
|
||||
$this->useHttps = $useHttps;
|
||||
$this->allowCookiesScriptAccess = $allowCookiesScriptAccess;
|
||||
$this->ipAddress = empty($ipAddress) ? $_SERVER['REMOTE_ADDR'] : $ipAddress;
|
||||
@@ -110,15 +126,20 @@ class Auth {
|
||||
$parts = explode(self::COOKIE_CONTENT_SEPARATOR, $_COOKIE[self::COOKIE_NAME_REMEMBER], 2);
|
||||
// if both selector and token were found
|
||||
if (isset($parts[0]) && isset($parts[1])) {
|
||||
$stmt = $this->db->prepare("SELECT a.user, a.token, a.expires, b.email, b.username FROM users_remembered AS a JOIN users AS b ON a.user = b.id WHERE a.selector = :selector");
|
||||
$stmt->bindValue(':selector', $parts[0], \PDO::PARAM_STR);
|
||||
if ($stmt->execute()) {
|
||||
$rememberData = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if ($rememberData !== false) {
|
||||
if ($rememberData['expires'] >= time()) {
|
||||
if (password_verify($parts[1], $rememberData['token'])) {
|
||||
$this->onLoginSuccessful($rememberData['user'], $rememberData['email'], $rememberData['username'], true);
|
||||
}
|
||||
try {
|
||||
$rememberData = $this->db->selectRow(
|
||||
'SELECT a.user, a.token, a.expires, b.email, b.username FROM users_remembered AS a JOIN users AS b ON a.user = b.id WHERE a.selector = ?',
|
||||
[ $parts[0] ]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
if (!empty($rememberData)) {
|
||||
if ($rememberData['expires'] >= time()) {
|
||||
if (password_verify($parts[1], $rememberData['token'])) {
|
||||
$this->onLoginSuccessful($rememberData['user'], $rememberData['email'], $rememberData['username'], true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,53 +183,33 @@ class Auth {
|
||||
$password = password_hash($password, PASSWORD_DEFAULT);
|
||||
$verified = isset($callback) && is_callable($callback) ? 0 : 1;
|
||||
|
||||
$stmt = $this->db->prepare("INSERT INTO users (email, password, username, verified, registered) VALUES (:email, :password, :username, :verified, :registered)");
|
||||
$stmt->bindValue(':email', $email, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':password', $password, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':username', $username, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':verified', $verified, \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':registered', time(), \PDO::PARAM_INT);
|
||||
|
||||
try {
|
||||
$result = $stmt->execute();
|
||||
$this->db->insert(
|
||||
'users',
|
||||
[
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'username' => $username,
|
||||
'verified' => $verified,
|
||||
'registered' => time()
|
||||
]
|
||||
);
|
||||
}
|
||||
catch (\PDOException $e) {
|
||||
catch (IntegrityConstraintViolationException $e) {
|
||||
// if we have a duplicate entry
|
||||
if ($e->getCode() == '23000') {
|
||||
throw new UserAlreadyExistsException();
|
||||
}
|
||||
// if we have another error
|
||||
else {
|
||||
// throw an exception
|
||||
throw new DatabaseError(null, null, $e);
|
||||
}
|
||||
throw new UserAlreadyExistsException();
|
||||
}
|
||||
|
||||
// if creating the new user was successful
|
||||
if ($result) {
|
||||
// get the ID of the user that we've just created
|
||||
$stmt = $this->db->prepare("SELECT id FROM users WHERE email = :email");
|
||||
$stmt->bindValue(':email', $email, \PDO::PARAM_STR);
|
||||
|
||||
if ($result = $stmt->execute()) {
|
||||
$newUserId = $stmt->fetchColumn();
|
||||
}
|
||||
else {
|
||||
$newUserId = null;
|
||||
}
|
||||
|
||||
if ($verified === 1) {
|
||||
return $newUserId;
|
||||
}
|
||||
else {
|
||||
$this->createConfirmationRequest($email, $callback);
|
||||
|
||||
return $newUserId;
|
||||
}
|
||||
}
|
||||
else {
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
$newUserId = (int) $this->db->getLastInsertId();
|
||||
|
||||
if ($verified === 0) {
|
||||
$this->createConfirmationRequest($email, $callback);
|
||||
}
|
||||
|
||||
return $newUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,24 +233,26 @@ class Auth {
|
||||
$tokenHashed = password_hash($token, PASSWORD_DEFAULT);
|
||||
$expires = time() + 3600 * 24;
|
||||
|
||||
$stmt = $this->db->prepare("INSERT INTO users_confirmations (email, selector, token, expires) VALUES (:email, :selector, :token, :expires)");
|
||||
$stmt->bindValue(':email', $email, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':token', $tokenHashed, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':expires', $expires, \PDO::PARAM_INT);
|
||||
try {
|
||||
$this->db->insert(
|
||||
'users_confirmations',
|
||||
[
|
||||
'email' => $email,
|
||||
'selector' => $selector,
|
||||
'token' => $tokenHashed,
|
||||
'expires' => $expires
|
||||
]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
if (isset($callback) && is_callable($callback)) {
|
||||
$callback($selector, $token);
|
||||
}
|
||||
else {
|
||||
throw new MissingCallbackError();
|
||||
}
|
||||
|
||||
return;
|
||||
if (isset($callback) && is_callable($callback)) {
|
||||
$callback($selector, $token);
|
||||
}
|
||||
else {
|
||||
throw new DatabaseError();
|
||||
throw new MissingCallbackError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,47 +271,49 @@ class Auth {
|
||||
$email = self::validateEmailAddress($email);
|
||||
$password = self::validatePassword($password);
|
||||
|
||||
$stmt = $this->db->prepare("SELECT id, password, verified, username FROM users WHERE email = :email");
|
||||
$stmt->bindValue(':email', $email, \PDO::PARAM_STR);
|
||||
if ($stmt->execute()) {
|
||||
$userData = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if ($userData !== false) {
|
||||
if (password_verify($password, $userData['password'])) {
|
||||
// if the password needs to be re-hashed to keep up with improving password cracking techniques
|
||||
if (password_needs_rehash($userData['password'], PASSWORD_DEFAULT)) {
|
||||
// create a new hash from the password and update it in the database
|
||||
$this->updatePassword($userData['id'], $password);
|
||||
try {
|
||||
$userData = $this->db->selectRow(
|
||||
'SELECT id, password, verified, username FROM users WHERE email = ?',
|
||||
[ $email ]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
if (!empty($userData)) {
|
||||
if (password_verify($password, $userData['password'])) {
|
||||
// if the password needs to be re-hashed to keep up with improving password cracking techniques
|
||||
if (password_needs_rehash($userData['password'], PASSWORD_DEFAULT)) {
|
||||
// create a new hash from the password and update it in the database
|
||||
$this->updatePassword($userData['id'], $password);
|
||||
}
|
||||
|
||||
if ($userData['verified'] === 1) {
|
||||
$this->onLoginSuccessful($userData['id'], $email, $userData['username'], false);
|
||||
|
||||
if ($remember) {
|
||||
$this->createRememberDirective($userData['id']);
|
||||
}
|
||||
|
||||
if ($userData['verified'] == 1) {
|
||||
$this->onLoginSuccessful($userData['id'], $email, $userData['username'], false);
|
||||
|
||||
if ($remember) {
|
||||
$this->createRememberDirective($userData['id']);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else {
|
||||
throw new EmailNotVerifiedException();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN);
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN, $email);
|
||||
|
||||
throw new InvalidPasswordException();
|
||||
throw new EmailNotVerifiedException();
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN);
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN, $email);
|
||||
|
||||
throw new InvalidEmailException();
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new DatabaseError();
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN);
|
||||
$this->throttle(self::THROTTLE_ACTION_LOGIN, $email);
|
||||
|
||||
throw new InvalidEmailException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,20 +371,22 @@ class Auth {
|
||||
$tokenHashed = password_hash($token, PASSWORD_DEFAULT);
|
||||
$expires = time() + 3600 * 24 * 28;
|
||||
|
||||
$stmt = $this->db->prepare("INSERT INTO users_remembered (user, selector, token, expires) VALUES (:user, :selector, :token, :expires)");
|
||||
$stmt->bindValue(':user', $userId, \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':token', $tokenHashed, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':expires', $expires, \PDO::PARAM_INT);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$this->setRememberCookie($selector, $token, $expires);
|
||||
|
||||
return;
|
||||
try {
|
||||
$this->db->insert(
|
||||
'users_remembered',
|
||||
[
|
||||
'user' => $userId,
|
||||
'selector' => $selector,
|
||||
'token' => $tokenHashed,
|
||||
'expires' => $expires
|
||||
]
|
||||
);
|
||||
}
|
||||
else {
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
$this->setRememberCookie($selector, $token, $expires);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,17 +396,17 @@ class Auth {
|
||||
* @throws AuthError if an internal problem occurred (do *not* catch)
|
||||
*/
|
||||
private function deleteRememberDirective($userId) {
|
||||
$stmt = $this->db->prepare("DELETE FROM users_remembered WHERE user = :user");
|
||||
$stmt->bindValue(':user', $userId, \PDO::PARAM_INT);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$this->setRememberCookie(null, null, time() - 3600);
|
||||
|
||||
return;
|
||||
try {
|
||||
$this->db->delete(
|
||||
'users_remembered',
|
||||
[ 'user' => $userId ]
|
||||
);
|
||||
}
|
||||
else {
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
$this->setRememberCookie(null, null, time() - 3600);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -447,12 +454,19 @@ class Auth {
|
||||
* @param string $email the email address of the user who has just logged in
|
||||
* @param string $username the username (if any)
|
||||
* @param bool $remembered whether the user was remembered ("remember me") or logged in actively
|
||||
* @throws AuthError if an internal problem occurred (do *not* catch)
|
||||
*/
|
||||
private function onLoginSuccessful($userId, $email, $username, $remembered) {
|
||||
$stmt = $this->db->prepare("UPDATE users SET last_login = :lastLogin WHERE id = :id");
|
||||
$stmt->bindValue(':lastLogin', time(), \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':id', $userId, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
try {
|
||||
$this->db->update(
|
||||
'users',
|
||||
[ 'last_login' => time() ],
|
||||
[ 'id' => $userId ]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
// re-generate the session ID to prevent session fixation attacks
|
||||
Session::regenerate(true);
|
||||
@@ -533,36 +547,42 @@ class Auth {
|
||||
$this->throttle(self::THROTTLE_ACTION_CONSUME_TOKEN);
|
||||
$this->throttle(self::THROTTLE_ACTION_CONSUME_TOKEN, $selector);
|
||||
|
||||
$stmt = $this->db->prepare("SELECT id, email, token, expires FROM users_confirmations WHERE selector = :selector");
|
||||
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
|
||||
if ($stmt->execute()) {
|
||||
$confirmationData = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
if ($confirmationData !== false) {
|
||||
if (password_verify($token, $confirmationData['token'])) {
|
||||
if ($confirmationData['expires'] >= time()) {
|
||||
$stmt = $this->db->prepare("UPDATE users SET verified = :verified WHERE email = :email");
|
||||
$stmt->bindValue(':verified', 1, \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':email', $confirmationData['email'], \PDO::PARAM_STR);
|
||||
if ($stmt->execute()) {
|
||||
$stmt = $this->db->prepare("DELETE FROM users_confirmations WHERE id = :id");
|
||||
$stmt->bindValue(':id', $confirmationData['id'], \PDO::PARAM_INT);
|
||||
if ($stmt->execute()) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
try {
|
||||
$confirmationData = $this->db->selectRow(
|
||||
'SELECT id, email, token, expires FROM users_confirmations WHERE selector = ?',
|
||||
[ $selector ]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
if (!empty($confirmationData)) {
|
||||
if (password_verify($token, $confirmationData['token'])) {
|
||||
if ($confirmationData['expires'] >= time()) {
|
||||
try {
|
||||
$this->db->update(
|
||||
'users',
|
||||
[ 'verified' => 1 ],
|
||||
[ 'email' => $confirmationData['email'] ]
|
||||
);
|
||||
}
|
||||
else {
|
||||
throw new TokenExpiredException();
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
try {
|
||||
$this->db->delete(
|
||||
'users_confirmations',
|
||||
[ 'id' => $confirmationData['id'] ]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new InvalidSelectorTokenPairException();
|
||||
throw new TokenExpiredException();
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -570,7 +590,7 @@ class Auth {
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new DatabaseError();
|
||||
throw new InvalidSelectorTokenPairException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,21 +610,26 @@ class Auth {
|
||||
|
||||
$userId = $this->getUserId();
|
||||
|
||||
$stmt = $this->db->prepare("SELECT password FROM users WHERE id = :userId");
|
||||
$stmt->bindValue(':userId', $userId, \PDO::PARAM_INT);
|
||||
if ($stmt->execute()) {
|
||||
$passwordInDatabase = $stmt->fetchColumn();
|
||||
try {
|
||||
$passwordInDatabase = $this->db->selectValue(
|
||||
'SELECT password FROM users WHERE id = ?',
|
||||
[ $userId ]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
if (!empty($passwordInDatabase)) {
|
||||
if (password_verify($oldPassword, $passwordInDatabase)) {
|
||||
$this->updatePassword($userId, $newPassword);
|
||||
|
||||
return;
|
||||
}
|
||||
else {
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new DatabaseError();
|
||||
throw new NotLoggedInException();
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -617,14 +642,21 @@ class Auth {
|
||||
*
|
||||
* @param int $userId the ID of the user whose password should be updated
|
||||
* @param string $newPassword the new password
|
||||
* @throws AuthError if an internal problem occurred (do *not* catch)
|
||||
*/
|
||||
private function updatePassword($userId, $newPassword) {
|
||||
$newPassword = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||
|
||||
$stmt = $this->db->prepare("UPDATE users SET password = :password WHERE id = :userId");
|
||||
$stmt->bindValue(':password', $newPassword, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':userId', $userId, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
try {
|
||||
$this->db->update(
|
||||
'users',
|
||||
[ 'password' => $newPassword ],
|
||||
[ 'id' => $userId ]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -685,21 +717,21 @@ class Auth {
|
||||
* @throws AuthError if an internal problem occurred (do *not* catch)
|
||||
*/
|
||||
private function getUserIdByEmailAddress($email) {
|
||||
$stmt = $this->db->prepare("SELECT id FROM users WHERE email = :email");
|
||||
$stmt->bindValue(':email', $email, \PDO::PARAM_STR);
|
||||
try {
|
||||
$userId = $this->db->selectValue(
|
||||
'SELECT id FROM users WHERE email = ?',
|
||||
[ $email ]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$userId = $stmt->fetchColumn();
|
||||
|
||||
if ($userId !== false) {
|
||||
return $userId;
|
||||
}
|
||||
else {
|
||||
throw new InvalidEmailException();
|
||||
}
|
||||
if (!empty($userId)) {
|
||||
return $userId;
|
||||
}
|
||||
else {
|
||||
throw new DatabaseError();
|
||||
throw new InvalidEmailException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,14 +743,23 @@ class Auth {
|
||||
* @throws AuthError if an internal problem occurred (do *not* catch)
|
||||
*/
|
||||
private function getOpenPasswordResetRequests($userId) {
|
||||
$stmt = $this->db->prepare("SELECT COUNT(*) FROM users_resets WHERE user = :userId AND expires > :expiresAfter");
|
||||
$stmt->bindValue(':userId', $userId, \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':expiresAfter', time(), \PDO::PARAM_INT);
|
||||
try {
|
||||
$requests = $this->db->selectValue(
|
||||
'SELECT COUNT(*) FROM users_resets WHERE user = ? AND expires > ?',
|
||||
[
|
||||
$userId,
|
||||
time()
|
||||
]
|
||||
);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
return $stmt->fetchColumn();
|
||||
if (!empty($requests)) {
|
||||
return $requests;
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
}
|
||||
@@ -745,24 +786,26 @@ class Auth {
|
||||
$tokenHashed = password_hash($token, PASSWORD_DEFAULT);
|
||||
$expiresAt = time() + $expiresAfter;
|
||||
|
||||
$stmt = $this->db->prepare("INSERT INTO users_resets (user, selector, token, expires) VALUES (:userId, :selector, :token, :expires)");
|
||||
$stmt->bindValue(':userId', $userId, \PDO::PARAM_INT);
|
||||
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':token', $tokenHashed, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':expires', $expiresAt, \PDO::PARAM_INT);
|
||||
try {
|
||||
$this->db->insert(
|
||||
'users_resets',
|
||||
[
|
||||
'user' => $userId,
|
||||
'selector' => $selector,
|
||||
'token' => $tokenHashed,
|
||||
'expires' => $expiresAt
|
||||
]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
if ($stmt->execute()) {
|
||||
if (isset($callback) && is_callable($callback)) {
|
||||
$callback($selector, $token);
|
||||
}
|
||||
else {
|
||||
throw new MissingCallbackError();
|
||||
}
|
||||
|
||||
return;
|
||||
if (isset($callback) && is_callable($callback)) {
|
||||
$callback($selector, $token);
|
||||
}
|
||||
else {
|
||||
throw new DatabaseError();
|
||||
throw new MissingCallbackError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,34 +827,35 @@ class Auth {
|
||||
$this->throttle(self::THROTTLE_ACTION_CONSUME_TOKEN);
|
||||
$this->throttle(self::THROTTLE_ACTION_CONSUME_TOKEN, $selector);
|
||||
|
||||
$stmt = $this->db->prepare("SELECT id, user, token, expires FROM users_resets WHERE selector = :selector");
|
||||
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
|
||||
if ($stmt->execute()) {
|
||||
$resetData = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||
try {
|
||||
$resetData = $this->db->selectRow(
|
||||
'SELECT id, user, token, expires FROM users_resets WHERE selector = ?',
|
||||
[ $selector ]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
if ($resetData !== false) {
|
||||
if (password_verify($token, $resetData['token'])) {
|
||||
if ($resetData['expires'] >= time()) {
|
||||
$newPassword = self::validatePassword($newPassword);
|
||||
if (!empty($resetData)) {
|
||||
if (password_verify($token, $resetData['token'])) {
|
||||
if ($resetData['expires'] >= time()) {
|
||||
$newPassword = self::validatePassword($newPassword);
|
||||
|
||||
$this->updatePassword($resetData['user'], $newPassword);
|
||||
$this->updatePassword($resetData['user'], $newPassword);
|
||||
|
||||
$stmt = $this->db->prepare("DELETE FROM users_resets WHERE id = :id");
|
||||
$stmt->bindValue(':id', $resetData['id'], \PDO::PARAM_INT);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
try {
|
||||
$this->db->delete(
|
||||
'users_resets',
|
||||
[ 'id' => $resetData['id'] ]
|
||||
);
|
||||
}
|
||||
else {
|
||||
throw new TokenExpiredException();
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new InvalidSelectorTokenPairException();
|
||||
throw new TokenExpiredException();
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -819,7 +863,7 @@ class Auth {
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new DatabaseError();
|
||||
throw new InvalidSelectorTokenPairException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1033,42 +1077,55 @@ class Auth {
|
||||
// get the time bucket that we do the throttling for
|
||||
$timeBucket = self::getTimeBucket();
|
||||
|
||||
$stmt = $this->db->prepare('INSERT INTO users_throttling (action_type, selector, time_bucket, attempts) VALUES (:actionType, :selector, :timeBucket, 1)');
|
||||
$stmt->bindValue(':actionType', $actionType, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':timeBucket', $timeBucket, \PDO::PARAM_INT);
|
||||
try {
|
||||
$stmt->execute();
|
||||
$this->db->insert(
|
||||
'users_throttling',
|
||||
[
|
||||
'action_type' => $actionType,
|
||||
'selector' => $selector,
|
||||
'time_bucket' => $timeBucket,
|
||||
'attempts' => 1
|
||||
]
|
||||
);
|
||||
}
|
||||
catch (\PDOException $e) {
|
||||
// if we have a duplicate entry
|
||||
if ($e->getCode() == '23000') {
|
||||
// update the old entry
|
||||
$stmt = $this->db->prepare('UPDATE users_throttling SET attempts = attempts+1 WHERE action_type = :actionType AND selector = :selector AND time_bucket = :timeBucket');
|
||||
$stmt->bindValue(':actionType', $actionType, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':timeBucket', $timeBucket, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
catch (IntegrityConstraintViolationException $e) {
|
||||
// if we have a duplicate entry, update the old entry
|
||||
try {
|
||||
$this->db->exec(
|
||||
'UPDATE users_throttling SET attempts = attempts+1 WHERE action_type = ? AND selector = ? AND time_bucket = ?',
|
||||
[
|
||||
$actionType,
|
||||
$selector,
|
||||
$timeBucket
|
||||
]
|
||||
);
|
||||
}
|
||||
// if we have another error
|
||||
else {
|
||||
// throw an exception
|
||||
throw new DatabaseError(null, null, $e);
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare('SELECT attempts FROM users_throttling WHERE action_type = :actionType AND selector = :selector AND time_bucket = :timeBucket');
|
||||
$stmt->bindValue(':actionType', $actionType, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':selector', $selector, \PDO::PARAM_STR);
|
||||
$stmt->bindValue(':timeBucket', $timeBucket, \PDO::PARAM_INT);
|
||||
if ($stmt->execute()) {
|
||||
$attempts = $stmt->fetchColumn();
|
||||
try {
|
||||
$attempts = $this->db->selectValue(
|
||||
'SELECT attempts FROM users_throttling WHERE action_type = ? AND selector = ? AND time_bucket = ?',
|
||||
[
|
||||
$actionType,
|
||||
$selector,
|
||||
$timeBucket
|
||||
]
|
||||
);
|
||||
}
|
||||
catch (Error $e) {
|
||||
throw new DatabaseError();
|
||||
}
|
||||
|
||||
if ($attempts !== false) {
|
||||
// if the number of attempts has acceeded our accepted limit
|
||||
if ($attempts > $this->throttlingActionsPerTimeBucket) {
|
||||
self::onTooManyRequests($this->throttlingTimeBucketSize);
|
||||
}
|
||||
if (!empty($attempts)) {
|
||||
// if the number of attempts has acceeded our accepted limit
|
||||
if ($attempts > $this->throttlingActionsPerTimeBucket) {
|
||||
self::onTooManyRequests($this->throttlingTimeBucketSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user