Increase password reset token validity to 72 hours and update related email message
This commit is contained in:
@@ -18,13 +18,18 @@ class customer_password_reset_keys_o extends db
|
||||
public object_property $updated_at;
|
||||
public object_property $deleted_at;
|
||||
const TOKEN_LENGTH = 32;
|
||||
const TOKEN_EXPIRY_SECONDS = 3600; // 1 hour
|
||||
const TOKEN_EXPIRY_SECONDS = 72 * 60 * 60; // 72 hours
|
||||
|
||||
public function structure(): void
|
||||
{
|
||||
$this->setTable('customer_password_reset_keys');
|
||||
}
|
||||
|
||||
private function validTokenWhereClause(): string
|
||||
{
|
||||
return "deleted_at IS NULL AND created_at >= DATE_SUB(NOW(), INTERVAL " . self::TOKEN_EXPIRY_SECONDS . " SECOND)";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a new customer reset key
|
||||
@@ -65,9 +70,8 @@ class customer_password_reset_keys_o extends db
|
||||
if (strlen($token) !== self::TOKEN_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
// Query the database for a valid token
|
||||
$current_time = date('Y-m-d H:i:s');
|
||||
$sql = "SELECT id FROM $this->table WHERE token = '" . $db->escape_string($token) . "' AND deleted_at IS NULL AND created_at >= DATE_SUB('$current_time', INTERVAL " . self::TOKEN_EXPIRY_SECONDS . " SECOND) LIMIT 1";
|
||||
// Query the database for a valid token using the same clock that writes created_at.
|
||||
$sql = "SELECT id FROM $this->table WHERE token = '" . $db->escape_string($token) . "' AND " . $this->validTokenWhereClause() . " LIMIT 1";
|
||||
$result = $db->query($sql);
|
||||
if ($result->num_rows === 0) {
|
||||
return null;
|
||||
@@ -85,13 +89,11 @@ class customer_password_reset_keys_o extends db
|
||||
*/
|
||||
public function isValidToken(): bool
|
||||
{
|
||||
global $db;
|
||||
self::requireSelected();
|
||||
$created_at = strtotime($this->created_at->value());
|
||||
$current_time = time();
|
||||
return (
|
||||
($current_time - $created_at) <= self::TOKEN_EXPIRY_SECONDS) &&
|
||||
($this->deleted_at->value() === null
|
||||
);
|
||||
$sql = "SELECT id FROM $this->table WHERE id = " . (int)$this->id . " AND " . $this->validTokenWhereClause() . " LIMIT 1";
|
||||
$result = $db->query($sql);
|
||||
return $result->num_rows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,4 +142,4 @@ class customer_password_reset_keys_o extends db
|
||||
{
|
||||
//TODO: Add cache invalidation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,7 +570,8 @@ class authRoute
|
||||
$reset_link = "https://truckwash.io/auth/password-reset/" . $token;
|
||||
|
||||
$subject = 'Adgangskode nulstilling';
|
||||
$message = "Du har anmodet om at nulstille din adgangskode. Klik på linket herunder for at fortsætte:<br><br><a href='$reset_link'>$reset_link</a><br><br>Linket er gyldigt i 1 time.";
|
||||
$valid_hours = (int)(customer_password_reset_keys_o::TOKEN_EXPIRY_SECONDS / 3600);
|
||||
$message = "Du har anmodet om at nulstille din adgangskode. Klik på linket herunder for at fortsætte:<br><br><a href='$reset_link'>$reset_link</a><br><br>Linket er gyldigt i $valid_hours timer.";
|
||||
|
||||
try {
|
||||
$email->sendEmail($email_address, $user->display_name->value() ?? 'Kunde', $subject, $message, null);
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
use objects\customer_password_reset_keys_o;
|
||||
|
||||
app_require('objects/customer_password_reset_keys_o.php');
|
||||
|
||||
if (!class_exists('PasswordResetTokenExpiryFakeResult')) {
|
||||
class PasswordResetTokenExpiryFakeResult
|
||||
{
|
||||
public int $num_rows;
|
||||
|
||||
public function __construct(private readonly array $rows)
|
||||
{
|
||||
$this->num_rows = count($rows);
|
||||
}
|
||||
|
||||
public function fetch_assoc(): ?array
|
||||
{
|
||||
return $this->rows[0] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('PasswordResetTokenExpiryFakeDb')) {
|
||||
class PasswordResetTokenExpiryFakeDb
|
||||
{
|
||||
public array $queries = [];
|
||||
|
||||
public function __construct(private readonly array $results)
|
||||
{
|
||||
}
|
||||
|
||||
public function escape_string(string $string): string
|
||||
{
|
||||
return addslashes($string);
|
||||
}
|
||||
|
||||
public function query(string $sql): PasswordResetTokenExpiryFakeResult
|
||||
{
|
||||
$this->queries[] = $sql;
|
||||
|
||||
return $this->results[count($this->queries) - 1] ?? new PasswordResetTokenExpiryFakeResult([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('PasswordResetTokenExpiryProbe')) {
|
||||
class PasswordResetTokenExpiryProbe extends customer_password_reset_keys_o
|
||||
{
|
||||
public function getObjectProperties(): void
|
||||
{
|
||||
}
|
||||
|
||||
public function forceSelectedId(int $id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function (): void {
|
||||
$this->previousDb = $GLOBALS['db'] ?? null;
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
if ($this->previousDb !== null) {
|
||||
$GLOBALS['db'] = $this->previousDb;
|
||||
return;
|
||||
}
|
||||
|
||||
unset($GLOBALS['db']);
|
||||
});
|
||||
|
||||
it('keeps password reset tokens valid for 72 hours', function (): void {
|
||||
expect(customer_password_reset_keys_o::TOKEN_EXPIRY_SECONDS)->toBe(72 * 60 * 60);
|
||||
});
|
||||
|
||||
it('looks up reset tokens using the database 72 hour validity window', function (): void {
|
||||
$GLOBALS['db'] = new PasswordResetTokenExpiryFakeDb([
|
||||
new PasswordResetTokenExpiryFakeResult([['id' => 42]]),
|
||||
]);
|
||||
|
||||
$token = str_repeat('a', customer_password_reset_keys_o::TOKEN_LENGTH);
|
||||
$probe = new PasswordResetTokenExpiryProbe();
|
||||
|
||||
$found = $probe->findValidByToken($token);
|
||||
|
||||
expect($found)->toBe($probe)
|
||||
->and($probe->id)->toBe(42)
|
||||
->and($GLOBALS['db']->queries[0])->toContain('created_at >= DATE_SUB(NOW(), INTERVAL 259200 SECOND)')
|
||||
->and($GLOBALS['db']->queries[0])->not->toContain("DATE_SUB('");
|
||||
});
|
||||
|
||||
it('uses the same database 72 hour window for the selected token guard', function (): void {
|
||||
$GLOBALS['db'] = new PasswordResetTokenExpiryFakeDb([
|
||||
new PasswordResetTokenExpiryFakeResult([['id' => 42]]),
|
||||
]);
|
||||
|
||||
$probe = new PasswordResetTokenExpiryProbe();
|
||||
$probe->forceSelectedId(42);
|
||||
|
||||
expect($probe->isValidToken())->toBeTrue()
|
||||
->and($GLOBALS['db']->queries[0])->toContain('id = 42')
|
||||
->and($GLOBALS['db']->queries[0])->toContain('created_at >= DATE_SUB(NOW(), INTERVAL 259200 SECOND)');
|
||||
});
|
||||
Reference in New Issue
Block a user