From 68a16152fb71ab8c2188feeb561d3ab053fadeef Mon Sep 17 00:00:00 2001 From: creme332 <65414576+creme332@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:25:05 +0400 Subject: [PATCH] rework algorithm for password reset - after generating token, return request id and token - improve logic for verifying token --- src/models/User.php | 92 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 22 deletions(-) diff --git a/src/models/User.php b/src/models/User.php index 51007e9..50a5e49 100644 --- a/src/models/User.php +++ b/src/models/User.php @@ -4,7 +4,7 @@ namespace Steamy\Model; -use Random\RandomException; +use Exception; use Steamy\Core\Model; abstract class User @@ -217,49 +217,97 @@ public static function getUserIdByEmail(string $email): ?int } /** - * @param int $userId - * @return string|null The hashed token for a password reset - * @throws RandomException Unable to generate token + * Generates a password reset token for a user. A hash of the token + * is stored in the database. + * + * @param int $userId ID of user to which password reset token belongs + * @return array|null An associative array with attributes `request_id` and `token` + * @throws Exception */ - public static function savePasswordChangeRequest(int $userId): ?string + public static function generatePasswordResetToken(int $userId): ?array { if ($userId < 0) { return null; } + $info = []; // array to store token and request ID + // Generate random token and its associated information - $token = bin2hex(random_bytes(16)); // length 32 bytes (hexadecimal format) + $info['token'] = ""; + try { + $info['token'] = bin2hex(random_bytes(16)); // length 32 bytes (hexadecimal format) + } catch (Exception) { + throw new Exception('Token cannot be generated'); + } + $expiryDate = date('Y-m-d H:i:s', strtotime('+1 day')); // Expiry date set to 1 day from now - $tokenHash = password_hash($token, PASSWORD_BCRYPT); // Hashing the token for security + $tokenHash = password_hash($info['token'], PASSWORD_BCRYPT); // Hashing the token for security - // Save password change info to database + // Save token info to database $query = "INSERT INTO password_change_request (user_id, token_hash, expiry_date) VALUES (:userId, :tokenHash, :expiryDate)"; - self::query($query, ['userId' => $userId, 'tokenHash' => $tokenHash, 'expiryDate' => $expiryDate]); - return $tokenHash; + + // get ID of last inserted record + // Ref: https://stackoverflow.com/a/39141771/17627866 + $query = <<< EOL + SELECT LAST_INSERT_ID(request_id) as request_id + FROM password_change_request + ORDER BY LAST_INSERT_ID(request_id) + DESC LIMIT 1; + EOL; + $info['request_id'] = self::query($query)[0]->request_id; + + return $info; } - public static function getUserIdByToken(string $token): ?int + /** + * Uses a password reset token to change the password of a user. + * @param int $requestID request ID corresponding to password reset token + * @param string $token Password reset token + * @param string $new_password New password of user in plain text (not hashed) + * @return bool + */ + public static function resetPassword(int $requestID, string $token, string $new_password): bool { - // Implement logic to fetch user ID by token from the database - $query = "SELECT user_id FROM password_change_request WHERE token_hash = :token AND expiry_date > NOW() AND used = 0"; - $result = self::query($query, ['token' => $token]); + // fetch matching request from database if valid (not expired and unused) + $query = <<< EOL + SELECT * FROM password_change_request + WHERE expiry_date > NOW() AND used = 0 AND request_id = :requestID + EOL; + + $result = self::query($query, ['requestID' => $requestID]); - if ($result && !empty($result[0]->user_id)) { - return $result[0]->user_id; + if (empty($result)) { + return false; } - return null; - } + $request = $result[0]; - public static function updatePassword(int $userId, string $hashedPassword): void - { - // Implement logic to update user's password in the database + // verify token + if (!password_verify($token, $request->token_hash)) { + return false; + } + + // validate password + if (!empty(self::validatePlainPassword($new_password))) { + return false; + } + + $hashedPassword = password_hash($new_password, PASSWORD_BCRYPT); + + // Update user's password in the database $query = "UPDATE user SET password = :password WHERE user_id = :userId"; - self::query($query, ['password' => $hashedPassword, 'userId' => $userId]); + self::query($query, ['password' => $hashedPassword, 'userId' => $request->user_id]); + + + // Invalidate password request token so that token cannot be used again + $query = "UPDATE password_change_request SET used = 1 WHERE request_id = :requestID"; + self::query($query, ['requestID' => $request->request_id]); + + return true; } }