diff --git a/src/Auth/Manager.php b/src/Auth/Manager.php index fe4ff7549..ad3e26452 100644 --- a/src/Auth/Manager.php +++ b/src/Auth/Manager.php @@ -5,12 +5,14 @@ use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Request; use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Contracts\Auth\StatefulGuard; +use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Session\SessionManager; /** * Authentication manager */ -class Manager implements \Illuminate\Contracts\Auth\StatefulGuard +class Manager implements StatefulGuard, UserProvider { use \Winter\Storm\Support\Traits\Singleton; @@ -44,6 +46,11 @@ class Manager implements \Illuminate\Contracts\Auth\StatefulGuard */ protected $throttleModel = Models\Throttle::class; + /** + * @var string Password Reset Model Class + */ + protected $passwordResetModel = Models\PasswordReset::class; + /** * @var bool Flag to enable login throttling */ @@ -366,6 +373,21 @@ public function findThrottleByUserId($userId, $ipAddress = null) return $this->throttle[$cacheKey] = $throttle; } + // + // Password Reset + // + + /** + * Creates a new password reset model instance. + * + * @return Models\PasswordReset + */ + public function createPasswordResetModel() + { + $class = '\\' . ltrim($this->passwordResetModel, '\\'); + return new $class(); + } + // // Business Logic // @@ -895,4 +917,48 @@ public function getRealUser() return $this->getUser(); } } + + /** + * @inheritDoc + */ + public function retrieveById($identifier) + { + return $this->findUserById($identifier); + } + + /** + * @inheritDoc + */ + public function retrieveByCredentials(array $credentials) + { + return $this->validateInternal($credentials); + } + + /** + * @inheritDoc + * + * Not implemented. + */ + public function retrieveByToken($identifier, $token) + { + return null; + } + + /** + * @inheritDoc + * + * Not implemented. + */ + public function updateRememberToken(Authenticatable $user, $token) + { + return; + } + + /** + * @inheritDoc + */ + public function validateCredentials(Authenticatable $user, array $credentials) + { + return ($user === $this->validateInternal($credentials)); + } } diff --git a/src/Auth/Migrations/2022_09_07_000007_Db_Password_Resets.php b/src/Auth/Migrations/2022_09_07_000007_Db_Password_Resets.php new file mode 100644 index 000000000..3df7390f0 --- /dev/null +++ b/src/Auth/Migrations/2022_09_07_000007_Db_Password_Resets.php @@ -0,0 +1,22 @@ +string('email')->index(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + } + + public function down() + { + Schema::dropIfExists('password_resets'); + } +} diff --git a/src/Auth/Models/PasswordReset.php b/src/Auth/Models/PasswordReset.php new file mode 100644 index 000000000..8b940e505 --- /dev/null +++ b/src/Auth/Models/PasswordReset.php @@ -0,0 +1,26 @@ +getEmailForPasswordReset()], + $this->defaultPasswordResetEmail($token), + function ($message) { + $message->subject('Password reset request'); + } + ); + } + + /** + * The default password reset email content. + * + * @param string $token + * @return string + */ + protected function defaultPasswordResetEmail($token) + { + $url = Config::get('app.url') . '/reset-password/' . $token; + + return 'Hi,' . "\n\n" + . 'Someone has requested a password reset for your account. If this was you, please go to the following URL to reset your password.' . "\n\n" + . $url . "\n\n" + . 'If this was not you, please feel free to disregard this email.'; + } +} diff --git a/src/Auth/Passwords/PasswordBrokerManager.php b/src/Auth/Passwords/PasswordBrokerManager.php new file mode 100644 index 000000000..6c4adcefb --- /dev/null +++ b/src/Auth/Passwords/PasswordBrokerManager.php @@ -0,0 +1,92 @@ +getConfig($name); + + if (is_null($config)) { + throw new InvalidArgumentException("Password resetter [{$name}] is not defined."); + } + + // The password broker uses a token repository to validate tokens and send user + // password e-mails, as well as validating that password reset process as an + // aggregate service of sorts providing a convenient interface for resets. + return new PasswordBroker( + $this->createTokenRepository($config), + $this->getAuthInstance(), + ); + } + + /** + * {@inheritDoc} + */ + protected function createTokenRepository(array $config) + { + $key = $this->app['config']['app.key']; + + if (str_starts_with($key, 'base64:')) { + $key = base64_decode(substr($key, 7)); + } + + $passwordResetModel = $this->getAuthInstance()->createPasswordResetModel(); + + if (isset($config['connection'])) { + $connection = $config['connection']; + } else { + $connection = $passwordResetModel->getConnectionName(); + } + + return new DatabaseTokenRepository( + $this->app['db']->connection($connection), + $this->app['hash'], + $config['table'] ?? $passwordResetModel->getTable(), + $key, + $config['expire'] ?? 180, + $config['throttle'] ?? 30, + ); + } + + /** + * {@inheritDoc} + * + * @return array|null + */ + protected function getConfig($name) + { + if ($name === 'winter') { + return []; + } + + return $this->app['config']["auth.passwords.{$name}"]; + } + + /** + * {@inheritDoc} + */ + public function getDefaultDriver() + { + return $this->app['config']['auth.defaults.passwords'] ?? 'winter'; + } + + /** + * Returns the active auth instance. + * + * @return \Winter\Storm\Auth\Manager + */ + protected function getAuthInstance() + { + return $this->app['auth']; + } +} diff --git a/src/Auth/Passwords/PasswordResetServiceProvider.php b/src/Auth/Passwords/PasswordResetServiceProvider.php new file mode 100644 index 000000000..fcc9e1e2c --- /dev/null +++ b/src/Auth/Passwords/PasswordResetServiceProvider.php @@ -0,0 +1,27 @@ +app->singleton('auth.password', function ($app) { + return new PasswordBrokerManager($app); + }); + + $this->app->bind('auth.password.broker', function ($app) { + return $app->make('auth.password')->broker(); + }); + } +}