From 52eeb5874331df597de10d2b803bbadfe44404e8 Mon Sep 17 00:00:00 2001 From: proggeler Date: Thu, 23 Apr 2026 17:08:08 +0200 Subject: [PATCH 1/3] added event dispatcher --- composer.json | 3 +- src/AuthenticationService.php | 93 +++++++-------- src/AuthenticationServiceProvider.php | 22 +++- src/Contracts/UserEntity.php | 3 + .../AuthenticationServiceEventDispatcher.php | 20 ++++ src/Event/Model/AccessToken.php | 16 +++ src/Event/Model/AuthenticatedUser.php | 16 +++ src/Event/Model/CreateToken.php | 19 ++++ src/Event/Model/GeneratedToken.php | 13 +++ src/Event/Model/UpdatePassword.php | 16 +++ src/Event/Model/UpdatedUser.php | 13 +++ src/Event/Model/UserCredentials.php | 22 ++++ src/Event/Model/ValidatedToken.php | 23 ++++ ...ersistAuthenticatedUserEventSubscriber.php | 37 ++++++ .../Subscribers/ReasonTypeEventSubscriber.php | 46 ++++++++ .../EmailPasswordAuthenticationHandler.php | 74 ------------ .../Accessor/TokenAuthenticationHandler.php | 101 ----------------- .../UsernamePasswordAuthenticationHandler.php | 76 ------------- .../EmailPasswordAuthenticationHandler.php | 70 ++++++++++++ .../TokenReasonAuthenticationHandler.php | 77 +++++++++++++ src/Handlers/PrepareParametersTrait.php | 30 ----- .../EmailPasswordAuthenticationHandler.php | 71 ------------ .../TokenAuthenticationHandler.php | 100 ----------------- .../UsernamePasswordAuthenticationHandler.php | 73 ------------ .../TokenAuthenticationHandlerInterface.php | 16 +-- .../UserAuthenticationHandlerInterface.php | 10 +- tests/AuthenticationServiceTest.php | 19 +++- .../EmailPasswordHandlerTest.php | 105 ----------------- .../PublicProperty/UserTokenHandlerTest.php | 74 ------------ .../UsernamePasswordHandlerTest.php | 106 ------------------ 30 files changed, 483 insertions(+), 881 deletions(-) create mode 100644 src/Event/AuthenticationServiceEventDispatcher.php create mode 100644 src/Event/Model/AccessToken.php create mode 100644 src/Event/Model/AuthenticatedUser.php create mode 100644 src/Event/Model/CreateToken.php create mode 100644 src/Event/Model/GeneratedToken.php create mode 100644 src/Event/Model/UpdatePassword.php create mode 100644 src/Event/Model/UpdatedUser.php create mode 100644 src/Event/Model/UserCredentials.php create mode 100644 src/Event/Model/ValidatedToken.php create mode 100644 src/Event/Subscribers/PersistAuthenticatedUserEventSubscriber.php create mode 100644 src/Event/Subscribers/ReasonTypeEventSubscriber.php delete mode 100644 src/Handlers/Accessor/EmailPasswordAuthenticationHandler.php delete mode 100644 src/Handlers/Accessor/TokenAuthenticationHandler.php delete mode 100644 src/Handlers/Accessor/UsernamePasswordAuthenticationHandler.php create mode 100644 src/Handlers/Entity/EmailPasswordAuthenticationHandler.php create mode 100644 src/Handlers/Entity/TokenReasonAuthenticationHandler.php delete mode 100644 src/Handlers/PrepareParametersTrait.php delete mode 100644 src/Handlers/PublicProperty/EmailPasswordAuthenticationHandler.php delete mode 100644 src/Handlers/PublicProperty/TokenAuthenticationHandler.php delete mode 100644 src/Handlers/PublicProperty/UsernamePasswordAuthenticationHandler.php delete mode 100644 tests/Handlers/PublicProperty/EmailPasswordHandlerTest.php delete mode 100644 tests/Handlers/PublicProperty/UserTokenHandlerTest.php delete mode 100644 tests/Handlers/PublicProperty/UsernamePasswordHandlerTest.php diff --git a/composer.json b/composer.json index fd3246f..6a10514 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "dmt-software/mail-service": "^1.0", "doctrine/orm": "^3.6", "psr/http-server-middleware": "^1.0", - "psr/http-factory": "^1.1" + "psr/http-factory": "^1.1", + "symfony/event-dispatcher": "^8.0" }, "require-dev": { "phpunit/phpunit": "^11.0", diff --git a/src/AuthenticationService.php b/src/AuthenticationService.php index eb6ab0c..25ee368 100644 --- a/src/AuthenticationService.php +++ b/src/AuthenticationService.php @@ -7,18 +7,19 @@ use DateTimeImmutable; use DMT\AuthenticationService\Contracts\UserEntity; use DMT\AuthenticationService\Contracts\TokenEntity; +use DMT\AuthenticationService\Event\Model\AccessToken; +use DMT\AuthenticationService\Event\Model\CreateToken; +use DMT\AuthenticationService\Event\Model\UpdatePassword; +use DMT\AuthenticationService\Event\Model\UserCredentials; use DMT\AuthenticationService\Exceptions\AuthenticationException; -use DMT\AuthenticationService\Handlers\UserAuthenticationHandlerInterface; use DMT\AuthenticationService\Handlers\TokenAuthenticationHandlerInterface; +use DMT\AuthenticationService\Handlers\UserAuthenticationHandlerInterface; use DMT\AuthenticationService\Mailer\MailManagerInterface; -use DMT\AuthenticationService\Password\PasswordHandlerInterface; use DMT\AuthenticationService\Session\SessionHandlerInterface; use DMT\DependencyInjection\Attributes\ConfigValue; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use InvalidArgumentException; -use ReflectionException; -use ReflectionProperty; +use Psr\EventDispatcher\EventDispatcherInterface; use SensitiveParameter; readonly class AuthenticationService @@ -28,7 +29,7 @@ public function __construct( private EntityManagerInterface $entityManager, private SessionHandlerInterface $sessionHandler, - private PasswordHandlerInterface $passwordHandler, + private EventDispatcherInterface $eventDispatcher, private UserAuthenticationHandlerInterface $userAuthenticationHandler, private TokenAuthenticationHandlerInterface $tokenAuthenticationHandler, private MailManagerInterface $mailManager, @@ -43,15 +44,16 @@ public function __construct( */ public function authenticate(#[SensitiveParameter] array $parameters, bool $persist = false): UserEntity { - $user = $this->userAuthenticationHandler->authenticate($parameters); + $credentials = new UserCredentials(...$parameters); - if ($persist) { - $userId = new ReflectionProperty($user, 'id')->getValue($user); + $this->eventDispatcher->dispatch($credentials); - $this->sessionHandler->login($userId); - } + $authenticatedUser = $this->userAuthenticationHandler->authenticate($credentials); + $authenticatedUser->persist = $persist; - return $user; + $this->eventDispatcher->dispatch($authenticatedUser); + + return $authenticatedUser->user; } /** @@ -59,21 +61,16 @@ public function authenticate(#[SensitiveParameter] array $parameters, bool $pers */ public function authenticateByToken(#[SensitiveParameter] array $parameters, bool $persist = false): TokenEntity { - $token = $this->tokenAuthenticationHandler->authenticate($parameters); + $accessToken = new AccessToken(...$parameters); - if ($persist) { - try { - $user = new ReflectionProperty($token, 'user')->getValue($token); - } catch (ReflectionException) { - throw new InvalidArgumentException('Can not persist, invalid token user'); - } + $this->eventDispatcher->dispatch($accessToken); - $userId = new ReflectionProperty($user, 'id')->getValue($user); + $validatedToken = $this->tokenAuthenticationHandler->authenticate($accessToken); + $validatedToken->persist = $persist; - $this->sessionHandler->login($userId); - } + $this->eventDispatcher->dispatch($validatedToken); - return $token; + return $validatedToken->token; } public function clear(): void @@ -90,37 +87,43 @@ public function forgotPassword(string $email): void return; } - $token = $this->tokenAuthenticationHandler->generateToken([ - 'user' => $user, - 'token' => uniqid('d', true), - 'reason' => 'forgot-password', - 'expiresAt' => new DateTimeImmutable('+20 minutes'), - ]); + $createToken = new CreateToken( + uniqid('d', true), + 'forgot-password', + $user, + new DateTimeImmutable('+20 minutes') + ); + + $this->eventDispatcher->dispatch($createToken); - $this->mailManager->sendForgotPasswordLink($email, $token); + $generatedToken = $this->tokenAuthenticationHandler->generateToken($createToken); + + $this->eventDispatcher->dispatch($generatedToken); + + $this->mailManager->sendForgotPasswordLink($email, $generatedToken->token); } public function resetPassword(string $token, string $password): void { - $parameters = [ - 'token' => $token, - 'reason' => 'forgot-password', - ]; + $accessToken = new AccessToken($token, 'forgot-password'); + + $this->entityManager->wrapInTransaction(function () use ($accessToken, $password): void { + $this->eventDispatcher->dispatch($accessToken); + + $validatedToken = $this->tokenAuthenticationHandler->authenticate($accessToken); + + $this->eventDispatcher->dispatch($validatedToken); + + $updatePassword = new UpdatePassword($validatedToken->user, $password); - $this->entityManager->wrapInTransaction(function () use ($parameters, $password): void { - $token = $this->tokenAuthenticationHandler->authenticate($parameters); - $token->markUsed(); + $this->eventDispatcher->dispatch($updatePassword); - try { - $user = new ReflectionProperty($token, 'user')->getValue($token); - } catch (ReflectionException) { - throw new InvalidArgumentException('Can not persist, invalid token user'); - } + $changedUser = $this->userAuthenticationHandler->updatePassword($updatePassword); - $this->userAuthenticationHandler->updatePassword($user, $this->passwordHandler->hash($password)); + $this->eventDispatcher->dispatch($changedUser); - $this->entityManager->persist($user); - $this->entityManager->persist($token); + $this->entityManager->persist($changedUser->user); + $this->entityManager->persist($validatedToken->token); }); } diff --git a/src/AuthenticationServiceProvider.php b/src/AuthenticationServiceProvider.php index 76b166c..0005d2b 100644 --- a/src/AuthenticationServiceProvider.php +++ b/src/AuthenticationServiceProvider.php @@ -5,8 +5,11 @@ namespace DMT\AuthenticationService; use DMT\Apps\App; -use DMT\AuthenticationService\Handlers\PublicProperty\EmailPasswordAuthenticationHandler; -use DMT\AuthenticationService\Handlers\PublicProperty\TokenAuthenticationHandler; +use DMT\AuthenticationService\Event\AuthenticationServiceEventDispatcher; +use DMT\AuthenticationService\Event\Subscribers\PersistAuthenticatedUserEventSubscriber; +use DMT\AuthenticationService\Event\Subscribers\ReasonTypeEventSubscriber; +use DMT\AuthenticationService\Handlers\Entity\EmailPasswordAuthenticationHandler; +use DMT\AuthenticationService\Handlers\Entity\TokenReasonAuthenticationHandler; use DMT\AuthenticationService\Handlers\UserAuthenticationHandlerInterface; use DMT\AuthenticationService\Handlers\TokenAuthenticationHandlerInterface; use DMT\AuthenticationService\Mailer\HtmlMailManager; @@ -28,8 +31,8 @@ public function __construct( #[ConfigValue('authentication.userHandler', EmailPasswordAuthenticationHandler::class)] private string $userEntityClass = EmailPasswordAuthenticationHandler::class, - #[ConfigValue('authentication.tokenHandler', TokenAuthenticationHandler::class)] - private string $tokenEntityClass = TokenAuthenticationHandler::class, + #[ConfigValue('authentication.tokenHandler', TokenReasonAuthenticationHandler::class)] + private string $tokenEntityClass = TokenReasonAuthenticationHandler::class, #[ConfigValue('authentication.mailManager', HtmlMailManager::class)] private string $mailManagerClass = TextMailManager::class, #[ConfigValue('authentication.sessionHandler', DefaultSessionHandler::class)] @@ -70,6 +73,17 @@ public function register(Container $container): void value: fn (): UserAuthenticationHandlerInterface => $container->get($this->userEntityClass) ); + $container->set( + id: AuthenticationService::class, + value: fn (): AuthenticationService => $container->get( + AuthenticationService::class, + eventDispatcher: new AuthenticationServiceEventDispatcher( + $container->get(ReasonTypeEventSubscriber::class), + $container->get(PersistAuthenticatedUserEventSubscriber::class), + ) + ) + ); + if ($container->has(App::class)) { $container->get(App::class)->addMiddleware( middleware: $container->get(AuthenticationMiddleware::class), diff --git a/src/Contracts/UserEntity.php b/src/Contracts/UserEntity.php index b38e444..5347111 100644 --- a/src/Contracts/UserEntity.php +++ b/src/Contracts/UserEntity.php @@ -6,5 +6,8 @@ interface UserEntity { + /** + * Check if the user is active. + */ public function isActive(): bool; } diff --git a/src/Event/AuthenticationServiceEventDispatcher.php b/src/Event/AuthenticationServiceEventDispatcher.php new file mode 100644 index 0000000..f7a54e2 --- /dev/null +++ b/src/Event/AuthenticationServiceEventDispatcher.php @@ -0,0 +1,20 @@ +addSubscriber($eventSubscriber); + } + } +} diff --git a/src/Event/Model/AccessToken.php b/src/Event/Model/AccessToken.php new file mode 100644 index 0000000..f66dd42 --- /dev/null +++ b/src/Event/Model/AccessToken.php @@ -0,0 +1,16 @@ + $value) { + $this->{$property} = $value; + } + } +} diff --git a/src/Event/Model/ValidatedToken.php b/src/Event/Model/ValidatedToken.php new file mode 100644 index 0000000..1edc7f2 --- /dev/null +++ b/src/Event/Model/ValidatedToken.php @@ -0,0 +1,23 @@ +token, 'user') + ->getValue($this->token); + } + } + + public function __construct( + public readonly TokenEntity $token, + public bool $persist = false, + ) { + } +} diff --git a/src/Event/Subscribers/PersistAuthenticatedUserEventSubscriber.php b/src/Event/Subscribers/PersistAuthenticatedUserEventSubscriber.php new file mode 100644 index 0000000..afbbb56 --- /dev/null +++ b/src/Event/Subscribers/PersistAuthenticatedUserEventSubscriber.php @@ -0,0 +1,37 @@ +persist) { + $userId = new ReflectionProperty($model->user, 'id') + ->getValue($model->user); + + $this->sessionHandler->login($userId); + } + } + + public static function getSubscribedEvents(): array + { + return [ + AuthenticatedUser::class => ['saveAuthenticatedUser', 100], + ValidatedToken::class => ['saveAuthenticatedUser', 100], + ]; + } +} diff --git a/src/Event/Subscribers/ReasonTypeEventSubscriber.php b/src/Event/Subscribers/ReasonTypeEventSubscriber.php new file mode 100644 index 0000000..fc54eab --- /dev/null +++ b/src/Event/Subscribers/ReasonTypeEventSubscriber.php @@ -0,0 +1,46 @@ +tokenEntity, 'reason')->getType(); + + if (is_scalar($model->reason) && $reasonPropertyType->isBuiltin()) { + return; + } + + if ( + is_scalar($model->reason) + && is_subclass_of($reasonPropertyType->getName(), BackedEnum::class) + ) { + /** @var BackedEnum $enum */ + $enum = $reasonPropertyType->getName(); + + $model->reason = $enum::tryFrom($model->reason); + } + } + + public static function getSubscribedEvents(): array + { + return [ + AccessToken::class => ['fixReasonPropertyType', 255], + CreateToken::class => ['fixReasonPropertyType', 255], + ]; + } +} \ No newline at end of file diff --git a/src/Handlers/Accessor/EmailPasswordAuthenticationHandler.php b/src/Handlers/Accessor/EmailPasswordAuthenticationHandler.php deleted file mode 100644 index faf159c..0000000 --- a/src/Handlers/Accessor/EmailPasswordAuthenticationHandler.php +++ /dev/null @@ -1,74 +0,0 @@ -userRepository = $entityManager->getRepository($userEntity); - } - - /** - * Authenticate using email and password. - * - * {@inheritDoc} - */ - public function authenticate(#[SensitiveParameter] array $parameters): UserEntity - { - if (!isset($parameters['email']) || !isset($parameters['password'])) { - throw new AuthenticationException('Invalid credentials.'); - } - - /** @var UserEntity $user */ - $user = $this->userRepository->findOneBy(['email' => $parameters['email']]); - - if ($user === null || !$user->isActive()) { - throw new AuthenticationException('Invalid credentials.'); - } - - if (!$this->passwordHandler->verify($parameters['password'], $user->password)) { - throw new AuthenticationException('Invalid credentials.'); - } - - return $user; - } - - /** - * @param object|UserEntity{setPassword: callable(string, string):void} $user - */ - public function updatePassword(UserEntity $user, #[SensitiveParameter] string $password): void - { - $user->setPassword($password); - } -} diff --git a/src/Handlers/Accessor/TokenAuthenticationHandler.php b/src/Handlers/Accessor/TokenAuthenticationHandler.php deleted file mode 100644 index 2c762f3..0000000 --- a/src/Handlers/Accessor/TokenAuthenticationHandler.php +++ /dev/null @@ -1,101 +0,0 @@ -tokenRepository = $entityManager->getRepository($this->tokenEntity); - } - - /** - * Authenticate using a user token. - * - * {@inheritDoc} - * - * @throws ReflectionException - */ - public function authenticate(#[SensitiveParameter] array $parameters): TokenEntity - { - if (!isset($parameters['token']) || !isset($parameters['reason'])) { - throw new AuthenticationException('Invalid token.'); - } - - /** @var TokenEntity $token */ - $token = $this->tokenRepository->findOneBy( - $this->prepareParameters($parameters, $this->tokenEntity) - ); - - if ($token === null || !$token->isValid()) { - throw new AuthenticationException('Invalid token.'); - } - - return $token; - } - - /** - * @inheritDoc - */ - public function generateToken(#[SensitiveParameter] array $parameters): TokenEntity - { - if ( - !isset($parameters['token']) - || !isset($parameters['reason']) - || !isset($parameters['user']) - || !isset($parameters['expiresAt']) - ) { - throw new InvalidArgumentException('Cannot generate token.'); - } - - try { - /** @var TokenEntity $token */ - $token = new ReflectionClass($this->tokenEntity)->newInstance(); - - foreach ($this->prepareParameters($parameters, $this->tokenEntity) as $property => $value) { - $setter = 'set' . ucfirst($property); - $token->$setter($value); - } - - $this->entityManager->persist($token); - $this->entityManager->flush(); - - return $token; - } catch (ReflectionException) { - throw new InvalidArgumentException('Cannot generate token.'); - } - } -} diff --git a/src/Handlers/Accessor/UsernamePasswordAuthenticationHandler.php b/src/Handlers/Accessor/UsernamePasswordAuthenticationHandler.php deleted file mode 100644 index a25a9c6..0000000 --- a/src/Handlers/Accessor/UsernamePasswordAuthenticationHandler.php +++ /dev/null @@ -1,76 +0,0 @@ -userRepository = $entityManager->getRepository($userEntity); - } - - /** - * Authenticate using username and password. - * - * {@inheritDoc} - */ - public function authenticate(#[SensitiveParameter] array $parameters): UserEntity - { - if (!isset($parameters['username']) || !isset($parameters['password'])) { - throw new AuthenticationException('Invalid credentials.'); - } - - /** @var UserEntity $user */ - $user = $this->userRepository->findOneBy([ - 'username' => $parameters['username'] - ]); - - if ($user === null || !$user->isActive()) { - throw new AuthenticationException('Invalid credentials.'); - } - - if (!$this->passwordHandler->verify($parameters['password'], $user->password)) { - throw new AuthenticationException('Invalid credentials.'); - } - - return $user; - } - - /** - * @param object|UserEntity{setPassword: callable(string):void} $user - */ - public function updatePassword(UserEntity $user, #[SensitiveParameter] string $password): void - { - $user->setPassword($password); - } -} diff --git a/src/Handlers/Entity/EmailPasswordAuthenticationHandler.php b/src/Handlers/Entity/EmailPasswordAuthenticationHandler.php new file mode 100644 index 0000000..2752b00 --- /dev/null +++ b/src/Handlers/Entity/EmailPasswordAuthenticationHandler.php @@ -0,0 +1,70 @@ + $userEntity + */ + public function __construct( + private EntityManagerInterface $entityManager, + private PasswordHandlerInterface $passwordHandler, + #[ConfigValue('authentication.user', 'DMT\Entity\User')] + private string $userEntity + ) { + } + + /** + * Authenticate user by email and password. + * + * {@inheritDoc} + */ + public function authenticate(UserCredentials $credentials): AuthenticatedUser + { + if (!isset($credentials->email) || empty($credentials->password)) { + throw new AuthenticationException('Invalid credentials.'); + } + + /** @var UserEntity $user */ + $user = $this->entityManager + ->getRepository($this->userEntity) + ->findOneBy([ + 'email' => $credentials->email + ]); + + if ($user === null || !$user->isActive()) { + throw new AuthenticationException('Invalid credentials.'); + } + + $password = new ReflectionProperty($user, 'password')->getValue($user); + + if (!$this->passwordHandler->verify($credentials->password, $password)) { + throw new AuthenticationException('Invalid credentials.'); + } + + return new AuthenticatedUser($user); + } + + public function updatePassword(UpdatePassword $updatePassword): UpdatedUser + { + $password = $this->passwordHandler->hash($updatePassword->password); + + new ReflectionProperty($updatePassword->user, 'password') + ->setValue($updatePassword->user, $password); + + return new UpdatedUser($updatePassword->user); + } +} diff --git a/src/Handlers/Entity/TokenReasonAuthenticationHandler.php b/src/Handlers/Entity/TokenReasonAuthenticationHandler.php new file mode 100644 index 0000000..0fec597 --- /dev/null +++ b/src/Handlers/Entity/TokenReasonAuthenticationHandler.php @@ -0,0 +1,77 @@ + $tokenEntity + */ + public function __construct( + private EntityManagerInterface $entityManager, + #[ConfigValue('authentication.token', 'DMT\Entity\UserToken')] + private string $tokenEntity + ) { + } + + /** + * Authenticate using a user token. + * + * {@inheritDoc} + */ + public function authenticate(AccessToken $accessToken): ValidatedToken + { + if (empty($accessToken->token) || empty($accessToken->reason)) { + throw new AuthenticationException('Invalid token.'); + } + + /** @var TokenEntity $token */ + $token = $this->entityManager + ->getRepository($this->tokenEntity) + ->findOneBy([ + 'token' => $accessToken->token, + 'reason' => $accessToken->reason, + ]); + + if ($token === null || !$token->isValid()) { + throw new AuthenticationException('Invalid token.'); + } + + $token->markUsed(); + + return new ValidatedToken($token); + } + + /** + * Generate a token. + * + * {@inheritDoc} + */ + public function generateToken(CreateToken $createToken): GeneratedToken + { + $tokenClass = new ReflectionClass($this->tokenEntity); + /** @var TokenEntity $token */ + $token = $tokenClass->newInstance(); + + $tokenClass->getProperty('token')->setValue($token, $createToken->token); + $tokenClass->getProperty('reason')->setValue($token, $createToken->reason); + $tokenClass->getProperty('expiresAt')->setValue($token, $createToken->expiresAt); + $tokenClass->getProperty('user')->setValue($token, $createToken->user); + + $this->entityManager->persist($token); + $this->entityManager->flush(); + + return new GeneratedToken($token); + } +} diff --git a/src/Handlers/PrepareParametersTrait.php b/src/Handlers/PrepareParametersTrait.php deleted file mode 100644 index 7d18b9a..0000000 --- a/src/Handlers/PrepareParametersTrait.php +++ /dev/null @@ -1,30 +0,0 @@ -getType(); - - if (is_subclass_of($reasonPropertyType->getName(), BackedEnum::class)) { - /** @var BackedEnum $enum */ - $enum = $reasonPropertyType->getName(); - - if (is_scalar($parameters['reason'])) { - $parameters['reason'] = $enum::tryFrom($parameters['reason']); - } - } elseif ($reasonPropertyType->isBuiltin()) { - settype($parameters['reason'], $reasonPropertyType->getName()); - } else { - throw new ReflectionException('Invalid type for reason property'); - } - - return $parameters; - } -} diff --git a/src/Handlers/PublicProperty/EmailPasswordAuthenticationHandler.php b/src/Handlers/PublicProperty/EmailPasswordAuthenticationHandler.php deleted file mode 100644 index 53bd119..0000000 --- a/src/Handlers/PublicProperty/EmailPasswordAuthenticationHandler.php +++ /dev/null @@ -1,71 +0,0 @@ -userRepository = $entityManager->getRepository($userEntity); - } - - /** - * Authenticate using email and password. - * - * {@inheritDoc} - */ - public function authenticate(#[SensitiveParameter] array $parameters): UserEntity - { - if (!isset($parameters['email']) || !isset($parameters['password'])) { - throw new AuthenticationException('Invalid credentials.'); - } - - /** @var UserEntity $user */ - $user = $this->userRepository->findOneBy(['email' => $parameters['email']]); - - if ($user === null || !$user->isActive()) { - throw new AuthenticationException('Invalid credentials.'); - } - - if (!$this->passwordHandler->verify($parameters['password'], $user->password)) { - throw new AuthenticationException('Invalid credentials.'); - } - - return $user; - } - - /** - * @param UserEntity{password: string} $user - */ - public function updatePassword(UserEntity $user, #[SensitiveParameter] string $password): void - { - $user->password = $password; - } -} diff --git a/src/Handlers/PublicProperty/TokenAuthenticationHandler.php b/src/Handlers/PublicProperty/TokenAuthenticationHandler.php deleted file mode 100644 index 3c6d6fa..0000000 --- a/src/Handlers/PublicProperty/TokenAuthenticationHandler.php +++ /dev/null @@ -1,100 +0,0 @@ -tokenRepository = $entityManager->getRepository($this->tokenEntity); - } - - /** - * Authenticate using a user token. - * - * {@inheritDoc} - * - * @throws ReflectionException - */ - public function authenticate(#[SensitiveParameter] array $parameters): TokenEntity - { - if (!isset($parameters['token']) || !isset($parameters['reason'])) { - throw new AuthenticationException('Invalid token.'); - } - - /** @var TokenEntity $token */ - $token = $this->tokenRepository->findOneBy( - $this->prepareParameters($parameters, $this->tokenEntity) - ); - - if ($token === null || !$token->isValid()) { - throw new AuthenticationException('Invalid token.'); - } - - return $token; - } - - /** - * @inheritDoc - */ - public function generateToken(#[SensitiveParameter] array $parameters): TokenEntity - { - if ( - !isset($parameters['token']) - || !isset($parameters['reason']) - || !isset($parameters['user']) - || !isset($parameters['expiresAt']) - ) { - throw new InvalidArgumentException('Cannot generate token.'); - } - - try { - /** @var TokenEntity $token */ - $token = new ReflectionClass($this->tokenEntity)->newInstance(); - - foreach ($this->prepareParameters($parameters, $this->tokenEntity) as $property => $value) { - $token->$property = $value; - } - - $this->entityManager->persist($token); - $this->entityManager->flush(); - - return $token; - } catch (ReflectionException) { - throw new InvalidArgumentException('Cannot generate token.'); - } - } -} diff --git a/src/Handlers/PublicProperty/UsernamePasswordAuthenticationHandler.php b/src/Handlers/PublicProperty/UsernamePasswordAuthenticationHandler.php deleted file mode 100644 index 2808637..0000000 --- a/src/Handlers/PublicProperty/UsernamePasswordAuthenticationHandler.php +++ /dev/null @@ -1,73 +0,0 @@ -userRepository = $entityManager->getRepository($userEntity); - } - - /** - * Authenticate using username and password. - * - * {@inheritDoc} - */ - public function authenticate(#[SensitiveParameter] array $parameters): UserEntity - { - if (!isset($parameters['username']) || !isset($parameters['password'])) { - throw new AuthenticationException('Invalid credentials.'); - } - - /** @var UserEntity $user */ - $user = $this->userRepository->findOneBy([ - 'username' => $parameters['username'] - ]); - - if ($user === null || !$user->isActive()) { - throw new AuthenticationException('Invalid credentials.'); - } - - if (!$this->passwordHandler->verify($parameters['password'], $user->password)) { - throw new AuthenticationException('Invalid credentials.'); - } - - return $user; - } - - /** - * @param UserEntity{password: string} $user - */ - public function updatePassword(UserEntity $user, #[SensitiveParameter] string $password): void - { - $user->password = $password; - } -} diff --git a/src/Handlers/TokenAuthenticationHandlerInterface.php b/src/Handlers/TokenAuthenticationHandlerInterface.php index b477f4e..b131231 100644 --- a/src/Handlers/TokenAuthenticationHandlerInterface.php +++ b/src/Handlers/TokenAuthenticationHandlerInterface.php @@ -4,26 +4,22 @@ namespace DMT\AuthenticationService\Handlers; -use DateTimeImmutable; -use DMT\AuthenticationService\Contracts\UserEntity; -use DMT\AuthenticationService\Contracts\TokenEntity; +use DMT\AuthenticationService\Event\Model\AccessToken; +use DMT\AuthenticationService\Event\Model\CreateToken; +use DMT\AuthenticationService\Event\Model\GeneratedToken; +use DMT\AuthenticationService\Event\Model\ValidatedToken; use DMT\AuthenticationService\Exceptions\AuthenticationException; use InvalidArgumentException; -use SensitiveParameter; interface TokenAuthenticationHandlerInterface { /** - * @param array{token: string, reason: string} $parameters - * * @throws AuthenticationException */ - public function authenticate(#[SensitiveParameter] array $parameters): TokenEntity; + public function authenticate(AccessToken $accessToken): ValidatedToken; /** - * @param array{user: UserEntity, token: string, reason: string, expiresAt: DateTimeImmutable} $parameters - * * @throws InvalidArgumentException */ - public function generateToken(#[SensitiveParameter] array $parameters): TokenEntity; + public function generateToken(CreateToken $createToken): GeneratedToken; } diff --git a/src/Handlers/UserAuthenticationHandlerInterface.php b/src/Handlers/UserAuthenticationHandlerInterface.php index 0579109..4d57509 100644 --- a/src/Handlers/UserAuthenticationHandlerInterface.php +++ b/src/Handlers/UserAuthenticationHandlerInterface.php @@ -4,16 +4,16 @@ namespace DMT\AuthenticationService\Handlers; -use DMT\AuthenticationService\Contracts\UserEntity; +use DMT\AuthenticationService\Event\Model\AuthenticatedUser; +use DMT\AuthenticationService\Event\Model\UpdatedUser; +use DMT\AuthenticationService\Event\Model\UpdatePassword; +use DMT\AuthenticationService\Event\Model\UserCredentials; use DMT\AuthenticationService\Exceptions\AuthenticationException; -use SensitiveParameter; interface UserAuthenticationHandlerInterface { /** * @throws AuthenticationException */ - public function authenticate(#[SensitiveParameter] array $parameters): UserEntity; - - public function updatePassword(UserEntity $user, #[SensitiveParameter] string $password): void; + public function authenticate(UserCredentials $credentials): AuthenticatedUser; } diff --git a/tests/AuthenticationServiceTest.php b/tests/AuthenticationServiceTest.php index 5eb884d..272ba04 100644 --- a/tests/AuthenticationServiceTest.php +++ b/tests/AuthenticationServiceTest.php @@ -5,8 +5,11 @@ namespace DMT\Test\AuthenticationService; use DMT\AuthenticationService\AuthenticationService; -use DMT\AuthenticationService\Handlers\Accessor\TokenAuthenticationHandler; -use DMT\AuthenticationService\Handlers\PublicProperty\EmailPasswordAuthenticationHandler; +use DMT\AuthenticationService\Event\AuthenticationServiceEventDispatcher; +use DMT\AuthenticationService\Event\Subscribers\PersistAuthenticatedUserEventSubscriber; +use DMT\AuthenticationService\Event\Subscribers\ReasonTypeEventSubscriber; +use DMT\AuthenticationService\Handlers\Entity\EmailPasswordAuthenticationHandler; +use DMT\AuthenticationService\Handlers\Entity\TokenReasonAuthenticationHandler; use DMT\AuthenticationService\Handlers\TokenAuthenticationHandlerInterface; use DMT\AuthenticationService\Handlers\UserAuthenticationHandlerInterface; use DMT\AuthenticationService\Mailer\MailManagerInterface; @@ -36,7 +39,7 @@ public function testAuthenticateWithUserCredentials(): void ); $container->set( TokenAuthenticationHandlerInterface::class, - fn (): TokenAuthenticationHandlerInterface => new TokenAuthenticationHandler( + fn (): TokenAuthenticationHandlerInterface => new TokenReasonAuthenticationHandler( $entityManager, Token::class, ) @@ -52,7 +55,9 @@ public function testAuthenticateWithUserCredentials(): void AuthenticationService::class, $entityManager, $sessionHandler, - new NativePasswordHandler(), + new AuthenticationServiceEventDispatcher( + $container->get(PersistAuthenticatedUserEventSubscriber::class, $sessionHandler), + ), $container->get(UserAuthenticationHandlerInterface::class), $container->get(TokenAuthenticationHandlerInterface::class), $this->createMock(MailManagerInterface::class), @@ -82,7 +87,7 @@ public function testAuthenticateWithUserToken(): void ); $container->set( TokenAuthenticationHandlerInterface::class, - fn (): TokenAuthenticationHandlerInterface => new TokenAuthenticationHandler( + fn (): TokenAuthenticationHandlerInterface => new TokenReasonAuthenticationHandler( $entityManager, Token::class, ) @@ -97,7 +102,9 @@ public function testAuthenticateWithUserToken(): void AuthenticationService::class, $entityManager, $sessionHandler, - new NativePasswordHandler(), + new AuthenticationServiceEventDispatcher( + $container->get(ReasonTypeEventSubscriber::class, Token::class), + ), $container->get(UserAuthenticationHandlerInterface::class), $container->get(TokenAuthenticationHandlerInterface::class), $this->createMock(MailManagerInterface::class), diff --git a/tests/Handlers/PublicProperty/EmailPasswordHandlerTest.php b/tests/Handlers/PublicProperty/EmailPasswordHandlerTest.php deleted file mode 100644 index 7ccdf53..0000000 --- a/tests/Handlers/PublicProperty/EmailPasswordHandlerTest.php +++ /dev/null @@ -1,105 +0,0 @@ -id = 1; - $user->email = 'user@example.com'; - $user->password = password_hash('password', PASSWORD_DEFAULT); - - $handler = new EmailPasswordAuthenticationHandler( - $this->getEntityManagerForUser($user), - new NativePasswordHandler(), - User::class - ); - - $handler->authenticate(['email' => 'user@example.com', 'password' => 'password']); - } - - public function testAuthenticateWithUnknownUser(): void - { - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('Invalid credentials.'); - - $handler = new EmailPasswordAuthenticationHandler( - $this->getEntityManagerForUser(null), - new NativePasswordHandler(), - User::class - ); - - $handler->authenticate(['email' => 'user@example.com', 'password' => 'password']); - } - - public function testAuthenticateWithInvalidCredentials(): void - { - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('Invalid credentials.'); - - $user = new User(); - $user->id = 1; - $user->email = 'user@example.com'; - $user->password = password_hash('other-password', PASSWORD_DEFAULT); - - $handler = new EmailPasswordAuthenticationHandler( - $this->getEntityManagerForUser($user), - new NativePasswordHandler(), - User::class - ); - - $handler->authenticate(['email' => 'user@example.com', 'password' => 'password']); - } - - public function testAuthenticateWithInactiveUser(): void - { - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('Invalid credentials.'); - - $user = new User(); - $user->email = 'user@example.com'; - $user->password = password_hash('password', PASSWORD_DEFAULT); - - $handler = new EmailPasswordAuthenticationHandler( - $this->getEntityManagerForUser($user), - new NativePasswordHandler(), - User::class - ); - - $handler->authenticate(['email' => 'user@example.com', 'password' => 'password']); - } - - private function getEntityManagerForUser(?User $user): EntityManagerInterface - { - $manager = $this->createMock(EntityManagerInterface::class); - $manager - ->expects($this->once()) - ->method('getRepository') - ->with(User::class) - ->willReturnCallback( - function () use ($user) { - $repository = $this->createMock(EntityRepository::class); - $repository - ->expects($this->once()) - ->method('findOneBy') - ->willReturnCallback(fn() => $user); - - return $repository; - } - ); - - return $manager; - } -} diff --git a/tests/Handlers/PublicProperty/UserTokenHandlerTest.php b/tests/Handlers/PublicProperty/UserTokenHandlerTest.php deleted file mode 100644 index 29a803f..0000000 --- a/tests/Handlers/PublicProperty/UserTokenHandlerTest.php +++ /dev/null @@ -1,74 +0,0 @@ -id = 1; - $token->token = '69c67c86054e3'; - $token->reason = 'activate'; - $token->user = new User(); - - $handler = new TokenAuthenticationHandler($this->getEntityManagerForUserToken($token), Token::class); - $handler->authenticate(['token' => '69c67c86054e3', 'reason' => 'activate']); - } - - public function testAuthenticateWithInvalidToken(): void - { - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('Invalid token.'); - - $token = new Token(); - $token->token = '7c8669c6054e3'; - $token->reason = 'forgot-password'; - $token->user = new User(); - - $handler = new TokenAuthenticationHandler($this->getEntityManagerForUserToken($token), Token::class); - $handler->authenticate(['token' => '7c8669c6054e3', 'reason' => 'forgot-password']); - } - - public function testAuthenticateWithUnknownToken(): void - { - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('Invalid token.'); - - $handler = new TokenAuthenticationHandler($this->getEntityManagerForUserToken(null), Token::class); - $handler->authenticate(['token' => '7c8669c6054e3', 'reason' => 'forgot-password']); - } - - private function getEntityManagerForUserToken(?Token $userToken): EntityManagerInterface - { - $manager = $this->createMock(EntityManagerInterface::class); - $manager - ->expects($this->once()) - ->method('getRepository') - ->with(Token::class) - ->willReturnCallback( - function () use ($userToken) { - $repository = $this->createMock(EntityRepository::class); - $repository - ->expects($this->once()) - ->method('findOneBy') - ->willReturnCallback(fn() => $userToken); - - return $repository; - } - ); - - - return $manager; - } -} diff --git a/tests/Handlers/PublicProperty/UsernamePasswordHandlerTest.php b/tests/Handlers/PublicProperty/UsernamePasswordHandlerTest.php deleted file mode 100644 index ef8ed25..0000000 --- a/tests/Handlers/PublicProperty/UsernamePasswordHandlerTest.php +++ /dev/null @@ -1,106 +0,0 @@ -id = 1; - $user->username = 'admin'; - $user->password = password_hash('password', PASSWORD_DEFAULT); - - $handler = new UsernamePasswordAuthenticationHandler( - $this->getEntityManagerForUser($user), - new NativePasswordHandler(), - User::class - ); - - $handler->authenticate(['username' => 'admin', 'password' => 'password']); - } - - public function testAuthenticateWithUnknownUser(): void - { - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('Invalid credentials.'); - - $handler = new UsernamePasswordAuthenticationHandler( - $this->getEntityManagerForUser(null), - new NativePasswordHandler(), - User::class - ); - - $handler->authenticate(['username' => 'admin', 'password' => 'password']); - } - - public function testAuthenticateWithInvalidCredentials(): void - { - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('Invalid credentials.'); - - $user = new User(); - $user->id = 1; - $user->email = 'user@example.com'; - $user->username = 'admin'; - $user->password = password_hash('other-password', PASSWORD_DEFAULT); - - $handler = new UsernamePasswordAuthenticationHandler( - $this->getEntityManagerForUser($user), - new NativePasswordHandler(), - User::class - ); - - $handler->authenticate(['username' => 'admin', 'password' => 'password']); - } - - public function testAuthenticateWithInactiveUser(): void - { - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('Invalid credentials.'); - - $user = new User(); - $user->username = 'admin'; - $user->password = password_hash('password', PASSWORD_DEFAULT); - - $handler = new UsernamePasswordAuthenticationHandler( - $this->getEntityManagerForUser($user), - new NativePasswordHandler(), - User::class - ); - - $handler->authenticate(['username' => 'admin', 'password' => 'password']); - } - - private function getEntityManagerForUser(?User $user): EntityManagerInterface - { - $manager = $this->createMock(EntityManagerInterface::class); - $manager - ->expects($this->once()) - ->method('getRepository') - ->with(User::class) - ->willReturnCallback( - function () use ($user) { - $repository = $this->createMock(EntityRepository::class); - $repository - ->expects($this->once()) - ->method('findOneBy') - ->willReturnCallback(fn() => $user); - - return $repository; - } - ); - - return $manager; - } -} From 27237a438b90dd22709040bbfb57bfae044f06cb Mon Sep 17 00:00:00 2001 From: proggeler Date: Thu, 23 Apr 2026 18:58:36 +0200 Subject: [PATCH 2/3] added event dispatcher --- src/AuthenticationServiceProvider.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/AuthenticationServiceProvider.php b/src/AuthenticationServiceProvider.php index 0005d2b..c86e74a 100644 --- a/src/AuthenticationServiceProvider.php +++ b/src/AuthenticationServiceProvider.php @@ -73,14 +73,19 @@ public function register(Container $container): void value: fn (): UserAuthenticationHandlerInterface => $container->get($this->userEntityClass) ); + $container->set( + id: AuthenticationServiceEventDispatcher::class, + value: fn (): AuthenticationServiceEventDispatcher => new AuthenticationServiceEventDispatcher( + $container->get(ReasonTypeEventSubscriber::class), + $container->get(PersistAuthenticatedUserEventSubscriber::class), + ) + ); + $container->set( id: AuthenticationService::class, value: fn (): AuthenticationService => $container->get( AuthenticationService::class, - eventDispatcher: new AuthenticationServiceEventDispatcher( - $container->get(ReasonTypeEventSubscriber::class), - $container->get(PersistAuthenticatedUserEventSubscriber::class), - ) + eventDispatcher: $container->get(AuthenticationServiceEventDispatcher::class) ) ); From a700db321c41d40238b5e9de3244e0a948bbd0dc Mon Sep 17 00:00:00 2001 From: proggeler Date: Thu, 23 Apr 2026 22:51:36 +0200 Subject: [PATCH 3/3] added event dispatcher, fix coding style --- src/Event/Model/ValidatedToken.php | 7 +++---- src/Event/Subscribers/ReasonTypeEventSubscriber.php | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Event/Model/ValidatedToken.php b/src/Event/Model/ValidatedToken.php index 1edc7f2..62bf31e 100644 --- a/src/Event/Model/ValidatedToken.php +++ b/src/Event/Model/ValidatedToken.php @@ -8,12 +8,11 @@ class ValidatedToken { + // phpcs:disable public UserEntity $user { - get { - return new ReflectionProperty($this->token, 'user') - ->getValue($this->token); - } + get => new ReflectionProperty($this->token, 'user')->getValue($this->token); } + // phpcs:enable public function __construct( public readonly TokenEntity $token, diff --git a/src/Event/Subscribers/ReasonTypeEventSubscriber.php b/src/Event/Subscribers/ReasonTypeEventSubscriber.php index fc54eab..2b0f093 100644 --- a/src/Event/Subscribers/ReasonTypeEventSubscriber.php +++ b/src/Event/Subscribers/ReasonTypeEventSubscriber.php @@ -43,4 +43,4 @@ public static function getSubscribedEvents(): array CreateToken::class => ['fixReasonPropertyType', 255], ]; } -} \ No newline at end of file +}