From 833e24f4713dc9e7bef81708889d8f6ebc70ae27 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Thu, 7 Nov 2024 21:38:11 -0600 Subject: [PATCH 1/2] [11.x] Cache token repository (#53428) * wip * Update CacheTokenRepository.php - store the carbon date as a formatted string - use the existing `delete()` method - simplify constructor and properties * Update PasswordBrokerManager.php have the `PasswordBrokerManager` pass in expiration as seconds * styling * minor styling * check for set "driver" key --- Passwords/CacheTokenRepository.php | 125 ++++++++++++++++++++++++++++ Passwords/PasswordBrokerManager.php | 12 ++- 2 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 Passwords/CacheTokenRepository.php diff --git a/Passwords/CacheTokenRepository.php b/Passwords/CacheTokenRepository.php new file mode 100644 index 00000000..6a9e88bd --- /dev/null +++ b/Passwords/CacheTokenRepository.php @@ -0,0 +1,125 @@ +getEmailForPasswordReset(); + + $this->delete($user); + + $token = hash_hmac('sha256', Str::random(40), $this->hashKey); + + $this->cache->put($email, [$token, Carbon::now()->format($this->format)], $this->expires); + + return $token; + } + + /** + * Determine if a token record exists and is valid. + * + * @param \Illuminate\Contracts\Auth\CanResetPassword $user + * @param string $token + * @return bool + */ + public function exists(CanResetPasswordContract $user, #[\SensitiveParameter] $token) + { + [$record, $createdAt] = $this->cache->get($user->getEmailForPasswordReset()); + + return $record + && ! $this->tokenExpired($createdAt) + && $this->hasher->check($token, $record); + } + + /** + * Determine if the token has expired. + * + * @param string $createdAt + * @return bool + */ + protected function tokenExpired($createdAt) + { + return Carbon::createFromFormat($this->format, $createdAt)->addSeconds($this->expires)->isPast(); + } + + /** + * Determine if the given user recently created a password reset token. + * + * @param \Illuminate\Contracts\Auth\CanResetPassword $user + * @return bool + */ + public function recentlyCreatedToken(CanResetPasswordContract $user) + { + [$record, $createdAt] = $this->cache->get($user->getEmailForPasswordReset()); + + return $record && $this->tokenRecentlyCreated($createdAt); + } + + /** + * Determine if the token was recently created. + * + * @param string $createdAt + * @return bool + */ + protected function tokenRecentlyCreated($createdAt) + { + if ($this->throttle <= 0) { + return false; + } + + return Carbon::createFromFormat($this->format, $createdAt)->addSeconds( + $this->throttle + )->isFuture(); + } + + /** + * Delete a token record. + * + * @param \Illuminate\Contracts\Auth\CanResetPassword $user + * @return void + */ + public function delete(CanResetPasswordContract $user) + { + $this->cache->forget($user->getEmailForPasswordReset()); + } + + /** + * Delete expired tokens. + * + * @return void + */ + public function deleteExpired() + { + } +} diff --git a/Passwords/PasswordBrokerManager.php b/Passwords/PasswordBrokerManager.php index e11e4449..502fe28d 100644 --- a/Passwords/PasswordBrokerManager.php +++ b/Passwords/PasswordBrokerManager.php @@ -88,10 +88,18 @@ protected function createTokenRepository(array $config) $key = base64_decode(substr($key, 7)); } - $connection = $config['connection'] ?? null; + if (isset($config['driver']) && $config['driver'] === 'cache') { + return new CacheTokenRepository( + $this->app['cache']->store($config['store'] ?? null), + $this->app['hash'], + $key, + ($config['expire'] ?? 60) * 60, + $config['throttle'] ?? 0 + ); + } return new DatabaseTokenRepository( - $this->app['db']->connection($connection), + $this->app['db']->connection($config['connection'] ?? null), $this->app['hash'], $config['table'], $key, From f6612045a32a1fcc6143bb6c5b56a0991e8f6ffc Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Fri, 8 Nov 2024 18:08:05 -0600 Subject: [PATCH 2/2] [11.x] add optional prefix for cache key (#53448) * add optional prefix for cache key if a user does not decide to use a dedicated cache store for their password resets, there's a risk of collision/overwriting of the cache keys in the default cache store, since we are just using the user's email. this allows the user to set an optional config value to use a prefix on the cache key. * minor formatting --- Passwords/CacheTokenRepository.php | 17 ++++++++++------- Passwords/PasswordBrokerManager.php | 3 ++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Passwords/CacheTokenRepository.php b/Passwords/CacheTokenRepository.php index 6a9e88bd..8a2ec3de 100644 --- a/Passwords/CacheTokenRepository.php +++ b/Passwords/CacheTokenRepository.php @@ -23,7 +23,8 @@ public function __construct( protected HasherContract $hasher, protected string $hashKey, protected int $expires = 3600, - protected int $throttle = 60 + protected int $throttle = 60, + protected string $prefix = '', ) { } @@ -35,13 +36,15 @@ public function __construct( */ public function create(CanResetPasswordContract $user) { - $email = $user->getEmailForPasswordReset(); - $this->delete($user); $token = hash_hmac('sha256', Str::random(40), $this->hashKey); - $this->cache->put($email, [$token, Carbon::now()->format($this->format)], $this->expires); + $this->cache->put( + $this->prefix.$user->getEmailForPasswordReset(), + [$token, Carbon::now()->format($this->format)], + $this->expires, + ); return $token; } @@ -55,7 +58,7 @@ public function create(CanResetPasswordContract $user) */ public function exists(CanResetPasswordContract $user, #[\SensitiveParameter] $token) { - [$record, $createdAt] = $this->cache->get($user->getEmailForPasswordReset()); + [$record, $createdAt] = $this->cache->get($this->prefix.$user->getEmailForPasswordReset()); return $record && ! $this->tokenExpired($createdAt) @@ -81,7 +84,7 @@ protected function tokenExpired($createdAt) */ public function recentlyCreatedToken(CanResetPasswordContract $user) { - [$record, $createdAt] = $this->cache->get($user->getEmailForPasswordReset()); + [$record, $createdAt] = $this->cache->get($this->prefix.$user->getEmailForPasswordReset()); return $record && $this->tokenRecentlyCreated($createdAt); } @@ -111,7 +114,7 @@ protected function tokenRecentlyCreated($createdAt) */ public function delete(CanResetPasswordContract $user) { - $this->cache->forget($user->getEmailForPasswordReset()); + $this->cache->forget($this->prefix.$user->getEmailForPasswordReset()); } /** diff --git a/Passwords/PasswordBrokerManager.php b/Passwords/PasswordBrokerManager.php index 502fe28d..c388c693 100644 --- a/Passwords/PasswordBrokerManager.php +++ b/Passwords/PasswordBrokerManager.php @@ -94,7 +94,8 @@ protected function createTokenRepository(array $config) $this->app['hash'], $key, ($config['expire'] ?? 60) * 60, - $config['throttle'] ?? 0 + $config['throttle'] ?? 0, + $config['prefix'] ?? '', ); }