Security update

This commit is contained in:
2026-04-01 21:28:18 +02:00
parent 779a2632ed
commit 4b9077b05f
7 changed files with 89 additions and 29 deletions
+3 -1
View File
@@ -18,7 +18,9 @@
]
},
"require": {
"php": ">=7.4"
"php": ">=8.2",
"ext-yaml": "*",
"ext-pdo": "*"
},
"require-dev": {
"phpunit/phpunit": "^8.4"
+9 -6
View File
@@ -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];
}
}
+4
View File
@@ -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: []
+1 -1
View File
@@ -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;
+29 -12
View File
@@ -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;
}
}
+19 -5
View File
@@ -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')));
}
}
+24 -4
View File
@@ -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')));
}
}