Replaces RememberUserProvider with RememberUserService

Adds password reset functionality
This commit is contained in:
2023-06-08 02:37:17 +02:00
parent 99373fd76c
commit 30b525b4a9
13 changed files with 296 additions and 101 deletions
+8
View File
@@ -1,5 +1,13 @@
# Changelog
## v2.0.0
Released 2023-06-08
- Introduces breaking changes
- Replaces RememberUserProvider with RememberUserService
- Adds password reset functionality
## v1.1.3
Released 2023-06-07
+32 -3
View File
@@ -11,10 +11,12 @@ Simple security library for web applications.
2. Create `UserProvider`. Refer to [Custom UserProvider](#custom-userprovider) section.
3. Instantiate `AuthenticationProvider` and inject your `UserProvider` into it.
3. Create instance of `Kavalanche\Security\Service\RememberUserService`.
3. Create instance of `AuthenticationProvider` and inject your `UserProvider` and `RememberUserService` into it.
```php
$authenticationProvider = new Kavalanche\Security\Provider\AuthenticationProvider($userProvider);
$authenticationProvider = new Kavalanche\Security\Provider\AuthenticationProvider($userProvider, $rememberUserService);
```
4. Check if user is authenticated.
@@ -71,12 +73,39 @@ You can configure identifier type in your `{app_root}/config/security.yaml` file
## Remember user
From version v1.1.0 there is a possibility to add `Remember me` checkbox to login form. This option sets a cookie in user's browser and logs him in automatically.
From version `v1.1.0` there is a possibility to add `Remember me` checkbox to login form. This option sets a cookie in user's browser and logs him in automatically.
Default configuration:
- `login-form-remember-me-field`: "rememberme"
- `remember-me-cookie-lifetime`: 2592000 # 30 days
## Password reset
From version `v2.0.0` there is a new functionality of resetting user password. You can use it as follows:
1. Add route to form with `email` field (configurable)
2. Check default email template and replace it with your own if needed (`password-reset-mail-template` in configuration file)
3. Add route to send email that includes code:
```
# You must create a service for sending e-mails that implements Kavalanche\Security\Interfaces\MailerInterface
$passwordResetService = new Kavalanche\Security\Service\PasswordResetService($passwordResetRepo, $userProvider, $mailer);
# Form data is handled internally if you use correctly configured fields (please refer to parameters.yaml file)
$passwordResetService->processResetRequest();
# You can redirect user according to the output of this method
```
4. Add new password handling route that includes code:
```
# Form data is handled internally if you use correctly configured fields (please refer to parameters.yaml file)
$passwordResetService->resetPassword();
# You can redirect user according to the output of this method
```
## Other informations
- It's your obligation to ensure that the usernames are unique.
+14 -4
View File
@@ -1,7 +1,17 @@
identifier: "email"
login-form-identifier-field: "email"
login-form-password-field: "password"
login-form-remember-me-field: "rememberme"
identifier: email
login-form-identifier-field: email
login-form-password-field: password
login-form-remember-me-field: rememberme
remember-me-cookie-lifetime: 2592000 # 30 days
password-reset-email-field: email
password-reset-password-field: password
password-reset-password-confirm-field: passwordConfirm
password-reset-token-field: token
password-reset-token-lifetime: 900 # 15 minutes
password-reset-mail-from: ''
password-reset-mail-to: ''
password-reset-mail-subject: ''
password-reset-mail-headers: ''
password-reset-mail-template: 'templates/email/passwordResetToken'
redirect-path: "/"
lang: en
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security\Entity;
use Kavalanche\Security\Interfaces\Token\PasswordResetTokenInterface;
/**
* @author Wojciech Burda <w.lk@wp.pl>
*/
class PasswordResetToken implements PasswordResetTokenInterface {
private int $expirationTime;
private string $token;
private int $userId;
public function getExpirationTime(): int {
return $this->expirationTime;
}
public function getToken(): string {
return $this->token;
}
public function getUserId(): int {
return $this->userId;
}
public function setExpirationTime(int $expirationTime): void {
$this->expirationTime = $expirationTime;
}
public function setToken(string $token): void {
$this->token = $token;
}
public function setUserId(int $userId): void {
$this->userId = $userId;
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security\Interfaces;
/**
* @author wojtek
*/
interface MailerInterface {
public function send(string $from, string $to, string $subject, string $body, array $headers): bool;
}
@@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security\Interfaces\Provider;
use Kavalanche\Security\Interfaces\Token\RememberUserTokenInterface;
/**
* @author Wojciech Burda <w.lk@wp.pl>
*/
interface RememberUserProviderInterface {
public function storeToken(RememberUserTokenInterface $token): void;
public function getToken(string $token): ?RememberUserTokenInterface;
public function deleteToken(RememberUserTokenInterface $token): void;
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security\Interfaces\Repository;
use Kavalanche\Security\Interfaces\Token\PasswordResetTokenInterface;
/**
* @author Wojciech Burda <w.lk@wp.pl>
*/
interface PasswordResetTokenRepositoryInterface {
public function getToken(string $token): ?PasswordResetTokenInterface;
public function storeToken(PasswordResetTokenInterface $token): void;
public function deleteToken(PasswordResetTokenInterface $token): void;
public function changePassword(int $userId, string $password): bool;
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security\Interfaces\Token;
/**
* @author Wojciech Burda <w.lk@wp.pl>
*/
interface PasswordResetTokenInterface {
public function getUserId(): int;
public function getToken(): string;
public function getExpirationTime(): int;
}
+9 -41
View File
@@ -4,13 +4,11 @@ declare(strict_types=1);
namespace Kavalanche\Security\Provider;
use Kavalanche\Security\Entity\RememberUserToken;
use Kavalanche\Security\Exception\SecurityException;
use Kavalanche\Security\Exception\UserNotAuthenticatedException;
use Kavalanche\Security\Interfaces\Provider\RememberUserProviderInterface;
use Kavalanche\Security\Interfaces\Provider\UserProviderInterface;
use Kavalanche\Security\Interfaces\Token\RememberUserTokenInterface;
use Kavalanche\Security\Interfaces\UserInterface;
use Kavalanche\Security\Service\RememberUserService;
use function Kavalanche\Security\_t;
use function Kavalanche\Security\config;
@@ -20,11 +18,11 @@ use function Kavalanche\Security\config;
class AuthenticationProvider {
private UserProviderInterface $userProvider;
private RememberUserProvider $rememberUserProvider;
private RememberUserService $rememberUserService;
public function __construct(UserProviderInterface $userProvider, RememberUserProviderInterface $rememberUserProvider) {
public function __construct(UserProviderInterface $userProvider, RememberUserService $rememberUserService) {
$this->userProvider = $userProvider;
$this->rememberUserProvider = $rememberUserProvider;
$this->rememberUserService = $rememberUserService;
}
public function authenticate(): ?UserInterface {
@@ -41,7 +39,7 @@ class AuthenticationProvider {
}
if (!empty($_COOKIE[$rememberMeFormField])) {
$token = $this->checkRememberUserToken();
$token = $this->rememberUserService->checkRememberUserToken();
if ($token) {
return $this->userProvider->loadUserById($token->getUserId());
}
@@ -58,7 +56,7 @@ class AuthenticationProvider {
if ($_POST[$rememberMeFormField]) {
$user = $this->userProvider->loadUser($_SESSION[$identifier]);
$this->setRememberUserToken($user->getId());
$this->rememberUserService->setRememberUserToken($user->getId());
}
$redirectPath = $_SESSION['redirect-path'] ?? config('redirect-path');
@@ -76,42 +74,12 @@ class AuthenticationProvider {
public function logout(): void {
if (!empty($_COOKIE[config('login-form-remember-me-field')])) {
$incomingToken = $_COOKIE[config('login-form-remember-me-field')];
$token = $this->rememberUserProvider->getToken($incomingToken);
$this->rememberUserProvider->deleteToken($token);
setcookie(config('login-form-remember-me-field'), '', time() - 3600);
$token = $this->rememberUserService->getToken($incomingToken);
$this->rememberUserService->deleteToken($token);
setcookie(config('login-form-remember-me-field'), '', time() - 3600, '/');
}
session_destroy();
header('Location: /');
exit;
}
private function setRememberUserToken(int $userId): RememberUserTokenInterface {
$token = $this->generateToken();
$expirationTime = time() + config('remember-me-cookie-lifetime');
setcookie(config('login-form-remember-me-field'), $token, $expirationTime, '/');
$rememberUserToken = new RememberUserToken();
$rememberUserToken->setExpirationTime($expirationTime);
$rememberUserToken->setToken($token);
$rememberUserToken->setUserId($userId);
$this->rememberUserProvider->storeToken($rememberUserToken);
return $rememberUserToken;
}
private function reloadRememberUserToken(RememberUserTokenInterface $token): RememberUserTokenInterface {
$this->rememberUserProvider->deleteToken($token);
return $this->setRememberUserToken($token->getUserId());
}
private function checkRememberUserToken(): ?RememberUserTokenInterface {
$incomingToken = $_COOKIE[config('login-form-remember-me-field')];
$token = $this->rememberUserProvider->getToken($incomingToken);
if ($token && $token->getExpirationTime() >= time()) {
return $this->reloadRememberUserToken($token);
}
return null;
}
private function generateToken(int $length = 20): string {
return bin2hex(random_bytes($length));
}
}
-34
View File
@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security\Provider;
use Kavalanche\Security\Interfaces\Provider\RememberUserProviderInterface;
use Kavalanche\Security\Interfaces\Repository\RememberUserTokenRepositoryInterface;
use Kavalanche\Security\Interfaces\Token\RememberUserTokenInterface;
/**
* @author Wojciech Burda <w.lk@wp.pl>
*/
class RememberUserProvider implements RememberUserProviderInterface {
private RememberUserTokenRepositoryInterface $repository;
public function __construct(RememberUserTokenRepositoryInterface $repository) {
$this->repository = $repository;
}
public function storeToken(RememberUserTokenInterface $token): void {
$this->repository->storeToken($token);
}
public function getToken(string $token): ?RememberUserTokenInterface {
return $this->repository->getToken($token);
}
public function deleteToken(RememberUserTokenInterface $token): void {
$this->repository->deleteToken($token);
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security\Service;
use Kavalanche\Security\Entity\PasswordResetToken;
use Kavalanche\Security\Interfaces\MailerInterface;
use Kavalanche\Security\Interfaces\Provider\UserProviderInterface;
use Kavalanche\Security\Interfaces\Repository\PasswordResetTokenRepositoryInterface;
use Kavalanche\Security\Interfaces\Token\PasswordResetTokenInterface;
use function Kavalanche\Security\config;
/**
* @author Wojciech Burda <w.lk@wp.pl>
*/
class PasswordResetService {
private PasswordResetTokenRepositoryInterface $passwordResetRepo;
private UserProviderInterface $userProvider;
private MailerInterface $mailer;
public function __construct(PasswordResetTokenRepositoryInterface $passwordResetRepo, UserProviderInterface $userProvider, MailerInterface $mailer) {
$this->passwordResetRepo = $passwordResetRepo;
$this->userProvider = $userProvider;
$this->mailer = $mailer;
}
public function processResetRequest(): bool {
$user = $this->userProvider->loadUser(filter_var($_POST[config('password-reset-email-field')], FILTER_SANITIZE_EMAIL));
$token = $this->setResetToken($user->getId());
return $this->sendResetToken($token, $user->getEmail());
}
public function resetPassword(): bool {
$newPassword = filter_var($_POST[config('password-reset-password-field')], FILTER_SANITIZE_STRING);
if ($newPassword === filter_var($_POST[config('password-reset-password-confirm-field')], FILTER_SANITIZE_STRING)) {
$token = $this->passwordResetRepo->getToken(filter_var($_POST[config('password-reset-token-field')], FILTER_SANITIZE_STRING));
if ($token && $token->getExpirationTime() >= time()) {
$this->passwordResetRepo->deleteToken($token);
return $this->passwordResetRepo->changePassword($token->getUserId(), $newPassword);
}
}
return false;
}
private function sendResetToken(PasswordResetToken $token, string $email): bool {
$url = $_SERVER['HTTP_HOST'];
$body = file_get_contents(__DIR__ . '/../../templates/email/passwordResetToken.php');
if (file_exists(__DIR__ . '/../../../../../' . config('password-reset-mail-template') . '.php')) {
$body = file_get_contents(__DIR__ . '/../../../../../templates/email/passwordResetToken.php');
}
return $this->mailer->send(config('password-reset-mail-from'), config('password-reset-mail-to'), config('password-reset-mail-subject'), $body, config('password-reset-mail-headers'));
}
private function setResetToken(int $userId): PasswordResetTokenInterface {
$token = $this->generateToken();
$expirationTime = time() + config('password-reset-token-lifetime');
$passwordResetToken = new PasswordResetToken();
$passwordResetToken->setExpirationTime($expirationTime);
$passwordResetToken->setToken($token);
$passwordResetToken->setUserId($userId);
$this->passwordResetRepo->storeToken($passwordResetToken);
return $passwordResetToken;
}
private function generateToken(int $length = 20): string {
return bin2hex(random_bytes($length));
}
}
/**
* 1. GET /password/reset
* 2. POST email
* 3. INSERT token into database (user_id, token (hashed), datetime)
* 4. Send email with token and url
* 5. Check user, token (hashed) with token hash and time offset (15 mins?)
* 6. Save new password or redirect with error
*/
+57
View File
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security\Service;
use Kavalanche\Security\Interfaces\Repository\RememberUserTokenRepositoryInterface;
/**
* @author wojtek
*/
class RememberUserService {
private RememberUserTokenRepositoryInterface $rememberUserRepository;
public function __construct(RememberUserTokenRepositoryInterface $rememberUserRepository) {
$this->rememberUserRepository = $rememberUserRepository;
}
public function setRememberUserToken(int $userId): RememberUserTokenInterface {
$token = $this->generateToken();
$expirationTime = time() + config('remember-me-cookie-lifetime');
setcookie(config('login-form-remember-me-field'), $token, $expirationTime, '/');
$rememberUserToken = new RememberUserToken();
$rememberUserToken->setExpirationTime($expirationTime);
$rememberUserToken->setToken($token);
$rememberUserToken->setUserId($userId);
$this->rememberUserRepository->storeToken($rememberUserToken);
return $rememberUserToken;
}
public function checkRememberUserToken(): ?RememberUserTokenInterface {
$incomingToken = $_COOKIE[config('login-form-remember-me-field')];
$token = $this->rememberUserRepository->getToken($incomingToken);
if ($token && $token->getExpirationTime() >= time()) {
return $this->reloadRememberUserToken($token);
}
return null;
}
public function getToken(string $token): \Kavalanche\Security\Interfaces\Token\RememberUserTokenInterface {
return $this->rememberUserRepository->getToken($token);
}
public function deleteToken(\Kavalanche\Security\Interfaces\Token\RememberUserTokenInterface $token) {
$this->rememberUserRepository->deleteToken($token);
}
private function reloadRememberUserToken(RememberUserTokenInterface $token): RememberUserTokenInterface {
$this->rememberUserRepository->deleteToken($token);
return $this->setRememberUserToken($token->getUserId());
}
private function generateToken(int $length = 20): string {
return bin2hex(random_bytes($length));
}
}
+5
View File
@@ -0,0 +1,5 @@
<h2>Password reset link</h2>
<p>By clicking the following link you can set new password for <?php echo $url; ?>:</p>
<p><a href="<?php echo $url; ?>/password/set?token=<?php echo $token; ?>"><?php echo $url; ?>/password/set?token=<?php echo $token; ?></a></p>
<p>Regards,<br>
<?php echo $url; ?> Admin</p>