Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
93 changes: 48 additions & 45 deletions src/AuthenticationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -43,37 +44,33 @@ 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;
}

/**
* @throws AuthenticationException
*/
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
Expand All @@ -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);
});
}

Expand Down
27 changes: 23 additions & 4 deletions src/AuthenticationServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)]
Expand Down Expand Up @@ -70,6 +73,22 @@ 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: $container->get(AuthenticationServiceEventDispatcher::class)
)
);

if ($container->has(App::class)) {
$container->get(App::class)->addMiddleware(
middleware: $container->get(AuthenticationMiddleware::class),
Expand Down
3 changes: 3 additions & 0 deletions src/Contracts/UserEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@

interface UserEntity
{
/**
* Check if the user is active.
*/
public function isActive(): bool;
}
20 changes: 20 additions & 0 deletions src/Event/AuthenticationServiceEventDispatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace DMT\AuthenticationService\Event;

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class AuthenticationServiceEventDispatcher extends EventDispatcher
{
public function __construct(EventSubscriberInterface ...$eventSubscribers)
{
parent::__construct();

foreach ($eventSubscribers as $eventSubscriber) {
$this->addSubscriber($eventSubscriber);
}
}
}
16 changes: 16 additions & 0 deletions src/Event/Model/AccessToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace DMT\AuthenticationService\Event\Model;

use BackedEnum;
use SensitiveParameter;

class AccessToken
{
public function __construct(
#[SensitiveParameter]
public string $token,
public string|BackedEnum $reason,
) {
}
}
16 changes: 16 additions & 0 deletions src/Event/Model/AuthenticatedUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace DMT\AuthenticationService\Event\Model;

use DMT\AuthenticationService\Contracts\UserEntity;

class AuthenticatedUser
{
public function __construct(
public readonly UserEntity $user,
public bool $persist = false,
) {
}
}
19 changes: 19 additions & 0 deletions src/Event/Model/CreateToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace DMT\AuthenticationService\Event\Model;

use BackedEnum;
use DateTimeImmutable;
use DMT\AuthenticationService\Contracts\UserEntity;

class CreateToken
{
public function __construct(
#[SensitiveParameter]
public string $token,
public string|BackedEnum $reason,
public UserEntity $user,
public ?DateTimeImmutable $expiresAt = null,
) {
}
}
13 changes: 13 additions & 0 deletions src/Event/Model/GeneratedToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace DMT\AuthenticationService\Event\Model;

use DMT\AuthenticationService\Contracts\TokenEntity;

class GeneratedToken
{
public function __construct(
public TokenEntity $token
) {
}
}
16 changes: 16 additions & 0 deletions src/Event/Model/UpdatePassword.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace DMT\AuthenticationService\Event\Model;

use DMT\AuthenticationService\Contracts\UserEntity;
use SensitiveParameter;

class UpdatePassword
{
public function __construct(
public UserEntity $user,
#[SensitiveParameter]
public string $password,
) {
}
}
13 changes: 13 additions & 0 deletions src/Event/Model/UpdatedUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace DMT\AuthenticationService\Event\Model;

use DMT\AuthenticationService\Contracts\UserEntity;

class UpdatedUser
{
public function __construct(
public UserEntity $user
) {
}
}
22 changes: 22 additions & 0 deletions src/Event/Model/UserCredentials.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace DMT\AuthenticationService\Event\Model;

use AllowDynamicProperties;
use SensitiveParameter;

#[AllowDynamicProperties]
class UserCredentials
{
public function __construct(
#[SensitiveParameter]
public string $password,
string ...$properties
) {
foreach ($properties as $property => $value) {
$this->{$property} = $value;
}
}
}
22 changes: 22 additions & 0 deletions src/Event/Model/ValidatedToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace DMT\AuthenticationService\Event\Model;

use DMT\AuthenticationService\Contracts\TokenEntity;
use DMT\AuthenticationService\Contracts\UserEntity;
use ReflectionProperty;

class ValidatedToken
{
// phpcs:disable
public UserEntity $user {
get => new ReflectionProperty($this->token, 'user')->getValue($this->token);
}
// phpcs:enable

public function __construct(
public readonly TokenEntity $token,
public bool $persist = false,
) {
}
}
Loading