Security update
This commit is contained in:
+3
-1
@@ -18,7 +18,9 @@
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.4"
|
||||
"php": ">=8.2",
|
||||
"ext-yaml": "*",
|
||||
"ext-pdo": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^8.4"
|
||||
|
||||
+9
-6
@@ -3,14 +3,17 @@
|
||||
namespace Kavalanche\Security;
|
||||
|
||||
function config(string $key) {
|
||||
$config = yaml_parse_file(__DIR__ . '/parameters.yaml');
|
||||
$userConfigPath = __DIR__ . '/../../../../config/security.yaml';
|
||||
if (file_exists($userConfigPath)) {
|
||||
$userConfig = yaml_parse_file($userConfigPath);
|
||||
$config = array_merge($config, $userConfig);
|
||||
static $config = null;
|
||||
if ($config === null) {
|
||||
$config = yaml_parse_file(__DIR__ . '/parameters.yaml');
|
||||
$userConfigPath = __DIR__ . '/../../../../config/security.yaml';
|
||||
if (file_exists($userConfigPath)) {
|
||||
$userConfig = yaml_parse_file($userConfigPath);
|
||||
$config = array_merge($config, $userConfig);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($config[$key])) {
|
||||
if (array_key_exists($key, $config) && $config[$key] !== '' && $config[$key] !== null) {
|
||||
return $config[$key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ login-form-identifier-field: email
|
||||
login-form-password-field: password
|
||||
login-form-remember-me-field: rememberme
|
||||
remember-me-cookie-lifetime: 2592000 # 30 days
|
||||
remember-me-token-length: 20
|
||||
password-reset-email-field: email
|
||||
password-reset-password-field: password
|
||||
password-reset-password-confirm-field: passwordConfirm
|
||||
@@ -17,5 +18,8 @@ password-reset-token-table-name: password_reset_token
|
||||
remember-user-token-table-name: remember_user_token
|
||||
user-table-name: user
|
||||
redirect-path: /
|
||||
logout-redirect-path: /
|
||||
base-url: ''
|
||||
password-reset-token-length: 20
|
||||
lang: en
|
||||
access-list: []
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Kavalanche\Security\Interfaces;
|
||||
* @author Wojciech Burda <wojciech.burda@rp.pl>
|
||||
*/
|
||||
interface UserInterface {
|
||||
|
||||
public function getId(): int;
|
||||
public function getUsername(): string;
|
||||
|
||||
public function getEmail(): string;
|
||||
|
||||
@@ -9,6 +9,7 @@ use Kavalanche\Security\Interfaces\Provider\UserProviderInterface;
|
||||
use Kavalanche\Security\Interfaces\Token\RememberUserTokenInterface;
|
||||
use Kavalanche\Security\Interfaces\UserInterface;
|
||||
use Kavalanche\Security\Service\RememberUserService;
|
||||
use Random\RandomException;
|
||||
use function Kavalanche\Security\_t;
|
||||
use function Kavalanche\Security\config;
|
||||
|
||||
@@ -25,6 +26,10 @@ class AuthenticationProvider {
|
||||
$this->rememberUserService = $rememberUserService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SecurityException
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function authenticate(): ?UserInterface {
|
||||
$user = null;
|
||||
$identifier = config('identifier');
|
||||
@@ -36,9 +41,7 @@ class AuthenticationProvider {
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_ACTIVE && !empty($_SESSION[$identifier])) {
|
||||
$user = $this->userProvider->loadUser($_SESSION[$identifier]);
|
||||
$this->saveUserInSession($user);
|
||||
return $user;
|
||||
return $this->userProvider->loadUser($_SESSION[$identifier]);
|
||||
}
|
||||
|
||||
if (!empty($_COOKIE[$rememberMeFormField])) {
|
||||
@@ -53,6 +56,7 @@ class AuthenticationProvider {
|
||||
if (!empty($_POST[$identifierFormField])) {
|
||||
$user = $this->userProvider->loadUser($_POST[$identifierFormField]);
|
||||
if (password_verify($_POST[$passwordFormField], $user->getPassword())) {
|
||||
session_regenerate_id(true);
|
||||
if (!empty($_POST[$rememberMeFormField])) {
|
||||
$this->rememberUserService->setRememberUserToken($user->getId());
|
||||
}
|
||||
@@ -75,17 +79,23 @@ class AuthenticationProvider {
|
||||
public function redirectAfterLogin(): void {
|
||||
$identifier = config('identifier');
|
||||
if (session_status() === PHP_SESSION_ACTIVE && !empty($_SESSION[$identifier])) {
|
||||
$redirectPath = $_SESSION['redirect-path'] ?? config('redirect-path');
|
||||
$redirectPath = config('redirect-path');
|
||||
if (!empty($_SESSION['redirect-path'])) {
|
||||
$parsed = parse_url($_SESSION['redirect-path']);
|
||||
if ($parsed !== false && empty($parsed['host'])) {
|
||||
$redirectPath = $_SESSION['redirect-path'];
|
||||
}
|
||||
}
|
||||
unset($_SESSION['redirect-path']);
|
||||
header('Location: ' . $redirectPath);
|
||||
$_SESSION['redirect-path'] = null;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if User has role needed to access given route.
|
||||
* Checks if a User has a role needed to access a given route.
|
||||
*
|
||||
* Route must be defined as regular expression.
|
||||
* Route must be defined as a regular expression.
|
||||
* Examples:
|
||||
* - ^/$ - Matches only "/"
|
||||
* - ^/admin - Matches "/admin*"
|
||||
@@ -106,6 +116,9 @@ class AuthenticationProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function getCurrentUser(): ?UserInterface {
|
||||
$identifier = config('identifier');
|
||||
$rememberMeFormField = config('login-form-remember-me-field');
|
||||
@@ -124,9 +137,6 @@ class AuthenticationProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo: Add redirect location to config
|
||||
*/
|
||||
public function logout(): void {
|
||||
if (!empty($_COOKIE[config('login-form-remember-me-field')])) {
|
||||
$incomingToken = $_COOKIE[config('login-form-remember-me-field')];
|
||||
@@ -134,10 +144,17 @@ class AuthenticationProvider {
|
||||
if ($token instanceof RememberUserTokenInterface) {
|
||||
$this->rememberUserService->deleteToken($token);
|
||||
}
|
||||
setcookie(config('login-form-remember-me-field'), '', time() - 3600, '/');
|
||||
setcookie(config('login-form-remember-me-field'), '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
]);
|
||||
}
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header('Location: /');
|
||||
header('Location: ' . config('logout-redirect-path'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use Kavalanche\Security\Interfaces\MailerInterface;
|
||||
use Kavalanche\Security\Interfaces\Provider\UserProviderInterface;
|
||||
use Kavalanche\Security\Interfaces\Repository\PasswordResetTokenRepositoryInterface;
|
||||
use Kavalanche\Security\Interfaces\Token\PasswordResetTokenInterface;
|
||||
use Random\RandomException;
|
||||
use function Kavalanche\Security\config;
|
||||
|
||||
/**
|
||||
@@ -26,8 +27,15 @@ class PasswordResetService {
|
||||
$this->mailer = $mailer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function processResetRequest(): bool {
|
||||
$user = $this->userProvider->loadUser(filter_var($_POST[config('password-reset-email-field')], FILTER_SANITIZE_EMAIL));
|
||||
try {
|
||||
$user = $this->userProvider->loadUser(filter_var($_POST[config('password-reset-email-field')], FILTER_SANITIZE_EMAIL));
|
||||
} catch (\Throwable) {
|
||||
return true;
|
||||
}
|
||||
$token = $this->setResetToken($user->getId());
|
||||
return $this->sendResetToken($token, $user->getEmail());
|
||||
}
|
||||
@@ -45,18 +53,21 @@ class PasswordResetService {
|
||||
}
|
||||
|
||||
private function sendResetToken(PasswordResetTokenInterface $token, string $email): bool {
|
||||
$url = $_SERVER['HTTP_HOST'];
|
||||
$url = config('base-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__ . '/../../../../../' . config('password-reset-mail-template') . '.php');
|
||||
}
|
||||
$search = ['{url}', '{token}'];
|
||||
$replace = [$_SERVER['HTTP_HOST'], $token->getToken()];
|
||||
$replace = [$url, $token->getToken()];
|
||||
$body = str_replace($search, $replace, $body);
|
||||
|
||||
return $this->mailer->send(config('password-reset-mail-from'), $email, config('password-reset-mail-subject'), $body, config('password-reset-mail-headers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
private function setResetToken(int $userId): PasswordResetTokenInterface {
|
||||
$token = $this->generateToken();
|
||||
$expirationTime = time() + config('password-reset-token-lifetime');
|
||||
@@ -68,7 +79,10 @@ class PasswordResetService {
|
||||
return $passwordResetToken;
|
||||
}
|
||||
|
||||
private function generateToken(int $length = 20): string {
|
||||
return bin2hex(random_bytes($length));
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
private function generateToken(): string {
|
||||
return bin2hex(random_bytes(config('password-reset-token-length')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Kavalanche\Security\Service;
|
||||
use Kavalanche\Security\Entity\RememberUserToken;
|
||||
use Kavalanche\Security\Interfaces\Repository\RememberUserTokenRepositoryInterface;
|
||||
use Kavalanche\Security\Interfaces\Token\RememberUserTokenInterface;
|
||||
use Random\RandomException;
|
||||
use function Kavalanche\Security\config;
|
||||
|
||||
/**
|
||||
@@ -20,10 +21,19 @@ class RememberUserService {
|
||||
$this->rememberUserRepository = $rememberUserRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
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, '/');
|
||||
setcookie(config('login-form-remember-me-field'), $token, [
|
||||
'expires' => $expirationTime,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
]);
|
||||
$rememberUserToken = new RememberUserToken();
|
||||
$rememberUserToken->setExpirationTime($expirationTime);
|
||||
$rememberUserToken->setToken($token);
|
||||
@@ -32,6 +42,9 @@ class RememberUserService {
|
||||
return $rememberUserToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
public function checkRememberUserToken(): ?RememberUserTokenInterface {
|
||||
$incomingToken = $_COOKIE[config('login-form-remember-me-field')];
|
||||
$token = $this->rememberUserRepository->getToken($incomingToken);
|
||||
@@ -45,16 +58,23 @@ class RememberUserService {
|
||||
return $this->rememberUserRepository->getToken($token);
|
||||
}
|
||||
|
||||
public function deleteToken(RememberUserTokenInterface $token) {
|
||||
public function deleteToken(RememberUserTokenInterface $token): void
|
||||
{
|
||||
$this->rememberUserRepository->deleteToken($token);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
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));
|
||||
/**
|
||||
* @throws RandomException
|
||||
*/
|
||||
private function generateToken(): string {
|
||||
return bin2hex(random_bytes(config('remember-me-token-length')));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user