From 3760b3354080f1a265cfb538bbee69cb856b3fa5 Mon Sep 17 00:00:00 2001 From: BibaltiK Date: Mon, 28 Oct 2024 13:35:36 +0100 Subject: [PATCH 1/2] =?UTF-8?q?Pr=C3=BCfung=20auf=20g=C3=BCltige=20Autenti?= =?UTF-8?q?fizierung=20impelentiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/pipeline.php | 2 + src/App/ConfigProvider.php | 1 + .../AccountAuthenticationMiddleware.php | 63 +++++++++++++++++++ ...AccountAuthenticationMiddlewareFactory.php | 24 +++++++ 4 files changed, 90 insertions(+) create mode 100644 src/App/Middleware/AccountAuthenticationMiddleware.php create mode 100644 src/App/Middleware/AccountAuthenticationMiddlewareFactory.php diff --git a/config/pipeline.php b/config/pipeline.php index 8ebc191..36d9b41 100644 --- a/config/pipeline.php +++ b/config/pipeline.php @@ -15,6 +15,7 @@ use Mezzio\Router\Middleware\MethodNotAllowedMiddleware; use Mezzio\Router\Middleware\RouteMiddleware; use Psr\Container\ContainerInterface; +use Stormannsgal\App\Middleware\AccountAuthenticationMiddleware; use Stormannsgal\App\Middleware\ClientIdentificationMiddleware; use Stormannsgal\Core\Middleware\RouteNotFoundMiddleware; @@ -37,6 +38,7 @@ UrlHelperMiddleware::class, ClientIdentificationMiddleware::class, + AccountAuthenticationMiddleware::class, DispatchMiddleware::class, diff --git a/src/App/ConfigProvider.php b/src/App/ConfigProvider.php index b268d6b..7cab832 100644 --- a/src/App/ConfigProvider.php +++ b/src/App/ConfigProvider.php @@ -52,6 +52,7 @@ public function getDependencies(): array ], 'factories' => [ Middleware\AccountAccessAuthPersistMiddleware::class => Middleware\AccountAccessAuthPersistMiddlewareFactory::class, + Middleware\AccountAuthenticationMiddleware::class => Middleware\AccountAuthenticationMiddlewareFactory::class, Middleware\AuthenticationConditionsMiddleware::class => Middleware\AuthenticationConditionsMiddlewareFactory::class, Middleware\AuthenticationMiddleware::class => Middleware\AuthenticationMiddlewareFactory::class, Middleware\AuthenticationValidationMiddleware::class => Middleware\AuthenticationValidationMiddlewareFactory::class, diff --git a/src/App/Middleware/AccountAuthenticationMiddleware.php b/src/App/Middleware/AccountAuthenticationMiddleware.php new file mode 100644 index 0000000..dd98fd8 --- /dev/null +++ b/src/App/Middleware/AccountAuthenticationMiddleware.php @@ -0,0 +1,63 @@ +getHeaderLine('Authorization'); + + if (strlen($authorization) === 0) { + $this->logger->info('{Host} as {User} call -> {URI}', ['User' => 'Guest',]); + + return $handler->handle($request); + } + + if (!$this->accessTokenService->isValid($authorization)) { + $this->logger->notice('{Host} has call {URI} with expired Access Token'); + + $message = AuthenticationFailureMessage::create(HTTP::STATUS_UNAUTHORIZED, 'Token expired'); + + return new JsonResponse($message, $message->statusCode); + } + + $authorization = $this->accessTokenService->decode($authorization); + $uuid = Uuid::fromString($authorization->uuid); + $account = $this->accountRepository->findByUuid($uuid); + + if (!($account instanceof AccountInterface)) { + $this->logger->notice('{Host} has call {URI} with invalid Access Token'); + + $message = AuthenticationFailureMessage::create(HTTP::STATUS_UNAUTHORIZED, 'Token invalid'); + + return new JsonResponse($message, $message->statusCode); + } + + $this->logger->info('{Host} as {User} call -> {URI}', ['User' => $account->getName(),]); + + return $handler->handle($request->withAttribute(AccountInterface::AUTHENTICATED, $account)); + } +} diff --git a/src/App/Middleware/AccountAuthenticationMiddlewareFactory.php b/src/App/Middleware/AccountAuthenticationMiddlewareFactory.php new file mode 100644 index 0000000..72c022d --- /dev/null +++ b/src/App/Middleware/AccountAuthenticationMiddlewareFactory.php @@ -0,0 +1,24 @@ +get(AccessTokenService::class); + $accountRepository = $container->get(AccountRepositoryInterface::class); + $loggerInterface = $container->get(LoggerInterface::class); + + return new AccountAuthenticationMiddleware( + $accessTokenService, + $accountRepository, + $loggerInterface + ); + } +} From 307906033521bb9f5341ae8bbd783839d6fac423 Mon Sep 17 00:00:00 2001 From: BibaltiK Date: Mon, 28 Oct 2024 23:14:20 +0100 Subject: [PATCH 2/2] added Tests --- .../AccountAuthenticationMiddleware.php | 4 +- .../GenerateAccessTokenMiddleware.php | 3 +- src/App/Service/AccessTokenService.php | 8 +- ...untAuthenticationMiddlewareFactoryTest.php | 32 +++++ .../AccountAuthenticationMiddlewareTest.php | 116 ++++++++++++++++++ .../Service/AccessTokenServiceTest.php | 4 +- ...AuthenticationMiddlewareRequestHandler.php | 25 ++++ tests/Mock/MockResponse.php | 8 +- tests/Mock/MockServerRequest.php | 23 ++-- ...ntAuthenticationMiddlewareInvalidToken.php | 15 +++ tests/Mock/Service/MockAccessTokenService.php | 21 +++- .../MockAccessTokenServiceWithoutDuration.php | 35 ++++++ ...ntAuthenticationMiddlewareInvalidToken.php | 83 +++++++++++++ 13 files changed, 353 insertions(+), 24 deletions(-) create mode 100644 tests/AppTest/Middleware/AccountAuthenticationMiddlewareFactoryTest.php create mode 100644 tests/AppTest/Middleware/AccountAuthenticationMiddlewareTest.php create mode 100644 tests/Mock/MockAccountAuthenticationMiddlewareRequestHandler.php create mode 100644 tests/Mock/Repository/MockAccountRepositoryAccountAuthenticationMiddlewareInvalidToken.php create mode 100644 tests/Mock/Service/MockAccessTokenServiceWithoutDuration.php create mode 100644 tests/Mock/Table/MockAccountTableAccountAuthenticationMiddlewareInvalidToken.php diff --git a/src/App/Middleware/AccountAuthenticationMiddleware.php b/src/App/Middleware/AccountAuthenticationMiddleware.php index dd98fd8..c66a8a3 100644 --- a/src/App/Middleware/AccountAuthenticationMiddleware.php +++ b/src/App/Middleware/AccountAuthenticationMiddleware.php @@ -11,9 +11,9 @@ use Psr\Log\LoggerInterface; use Ramsey\Uuid\Uuid; use Stormannsgal\App\DTO\AuthenticationFailureMessage; -use Stormannsgal\App\Repository\AccountRepository; use Stormannsgal\App\Service\AccessTokenService; use Stormannsgal\Core\Entity\AccountInterface; +use Stormannsgal\Core\Repository\AccountRepositoryInterface; use function strlen; @@ -21,7 +21,7 @@ { public function __construct( private AccessTokenService $accessTokenService, - private AccountRepository $accountRepository, + private AccountRepositoryInterface $accountRepository, private LoggerInterface $logger ) { } diff --git a/src/App/Middleware/GenerateAccessTokenMiddleware.php b/src/App/Middleware/GenerateAccessTokenMiddleware.php index ff52275..876eb0c 100644 --- a/src/App/Middleware/GenerateAccessTokenMiddleware.php +++ b/src/App/Middleware/GenerateAccessTokenMiddleware.php @@ -19,9 +19,10 @@ public function __construct( public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + /** @var AccountInterface $account */ $account = $request->getAttribute(AccountInterface::AUTHENTICATED); - $accessToken = $this->accessTokenService->generate($account); + $accessToken = $this->accessTokenService->generate($account->getUuid()); $accessToken = AccessToken::fromString($accessToken); diff --git a/src/App/Service/AccessTokenService.php b/src/App/Service/AccessTokenService.php index 1aca62e..094fb16 100644 --- a/src/App/Service/AccessTokenService.php +++ b/src/App/Service/AccessTokenService.php @@ -3,8 +3,8 @@ namespace Stormannsgal\App\Service; use Firebase\JWT\JWT; +use Ramsey\Uuid\UuidInterface; use Stormannsgal\App\DTO\JwtTokenConfig; -use Stormannsgal\Core\Entity\AccountInterface; use function time; @@ -13,11 +13,11 @@ use JwtTokenTrait; public function __construct( - private JwtTokenConfig $config, + protected JwtTokenConfig $config, ) { } - public function generate(AccountInterface $account): string + public function generate(UuidInterface $uuid): string { $now = time(); @@ -26,7 +26,7 @@ public function generate(AccountInterface $account): string 'aud' => $this->config->aud, 'iat' => $now, 'exp' => $now + $this->config->duration, - 'uuid' => $account->getUuid()->getHex()->toString(), + 'uuid' => $uuid->getHex()->toString(), ]; return JWT::encode($payload, $this->config->key, $this->config->algorithmus); diff --git a/tests/AppTest/Middleware/AccountAuthenticationMiddlewareFactoryTest.php b/tests/AppTest/Middleware/AccountAuthenticationMiddlewareFactoryTest.php new file mode 100644 index 0000000..a264ae4 --- /dev/null +++ b/tests/AppTest/Middleware/AccountAuthenticationMiddlewareFactoryTest.php @@ -0,0 +1,32 @@ + new MockAccessTokenService(), + AccountRepositoryInterface::class => new MockAccountRepository(), + LoggerInterface::class => new NullLogger(), + ] + ); + + $middleware = (new AccountAuthenticationMiddlewareFactory())($container); + + $this->assertInstanceOf(AccountAuthenticationMiddleware::class, $middleware); + } +} diff --git a/tests/AppTest/Middleware/AccountAuthenticationMiddlewareTest.php b/tests/AppTest/Middleware/AccountAuthenticationMiddlewareTest.php new file mode 100644 index 0000000..d77b0ad --- /dev/null +++ b/tests/AppTest/Middleware/AccountAuthenticationMiddlewareTest.php @@ -0,0 +1,116 @@ +accessTokenService = new MockAccessTokenService(); + $this->accountRepository = new MockAccountRepository(); + $this->logger = new NullLogger(); + } + + public function testAccountAuthenticatedIsGuest(): void + { + $middleware = new AccountAuthenticationMiddleware( + $this->accessTokenService, + $this->accountRepository, + $this->logger + ); + $handler = new MockAccountAuthenticationMiddlewareRequestHandler(); + $response = $middleware->process($this->request, $handler); + $header = $response->getHeaderLine('Authorization'); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertNotInstanceOf(JsonResponse::class, $response); + $this->assertSame('', $header); + } + + public function testAccountSuccessfulAuthenticated(): void + { + $accessToken = $this->accessTokenService->generate(Uuid::fromString(Account::UUID)); + $request = $this->request->withHeader('Authorization', $accessToken); + + $middleware = new AccountAuthenticationMiddleware( + $this->accessTokenService, + $this->accountRepository, + $this->logger + ); + + $handler = new MockAccountAuthenticationMiddlewareRequestHandler(); + $response = $middleware->process($request, $handler); + $header = $response->getHeaderLine('Authorization'); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertNotInstanceOf(JsonResponse::class, $response); + $this->assertSame('true', $header); + } + + public function testTokenHasExpired(): void + { + $accessTokenService = new MockAccessTokenServiceWithoutDuration(); + $accessToken = $accessTokenService->generate(Uuid::fromString(Account::UUID)); + $request = $this->request->withHeader('Authorization', $accessToken); + + $middleware = new AccountAuthenticationMiddleware( + $this->accessTokenService, + $this->accountRepository, + $this->logger + ); + + $response = $middleware->process($request, $this->handler); + $json = json_decode((string)$response->getBody(), null, 512, JSON_THROW_ON_ERROR); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame(HTTP::STATUS_UNAUTHORIZED, $response->getStatusCode()); + $this->assertTrue(property_exists($json, 'message') && $json->message === 'Token expired'); + } + + public function testTokenHasInvalid(): void + { + $accessToken = $this->accessTokenService->generate(Uuid::fromString(Account::UUID)); + $request = $this->request->withHeader('Authorization', $accessToken); + $accountRepository = new MockAccountRepositoryAccountAuthenticationMiddlewareInvalidToken(); + $middleware = new AccountAuthenticationMiddleware( + $this->accessTokenService, + $accountRepository, + $this->logger + ); + + $response = $middleware->process($request, $this->handler); + $json = json_decode((string)$response->getBody(), null, 512, JSON_THROW_ON_ERROR); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertInstanceOf(JsonResponse::class, $response); + $this->assertSame(HTTP::STATUS_UNAUTHORIZED, $response->getStatusCode()); + $this->assertTrue(property_exists($json, 'message') && $json->message === 'Token invalid'); + } +} diff --git a/tests/AppTest/Service/AccessTokenServiceTest.php b/tests/AppTest/Service/AccessTokenServiceTest.php index b0651a9..fc43df5 100644 --- a/tests/AppTest/Service/AccessTokenServiceTest.php +++ b/tests/AppTest/Service/AccessTokenServiceTest.php @@ -27,7 +27,7 @@ public function testGenerateValidAccessToken(): void $service = new AccessTokenService($jwtTokenConfig); - $token = $service->generate($this->account); + $token = $service->generate($this->account->getUuid()); $isValid = $service->isValid($token); @@ -44,6 +44,6 @@ public function testGenerateValidAccessTokenFails(): void $this->expectException(DomainException::class); - $service->generate($this->account); + $service->generate($this->account->getUuid()); } } diff --git a/tests/Mock/MockAccountAuthenticationMiddlewareRequestHandler.php b/tests/Mock/MockAccountAuthenticationMiddlewareRequestHandler.php new file mode 100644 index 0000000..1110bdf --- /dev/null +++ b/tests/Mock/MockAccountAuthenticationMiddlewareRequestHandler.php @@ -0,0 +1,25 @@ +getAttribute(AccountInterface::AUTHENTICATED); + $response = new MockResponse(); + + if ($account instanceof AccountInterface) { + return $response->withHeader('Authorization', 'true'); + } + + return $response; + } +} diff --git a/tests/Mock/MockResponse.php b/tests/Mock/MockResponse.php index cada6ca..30587ca 100644 --- a/tests/Mock/MockResponse.php +++ b/tests/Mock/MockResponse.php @@ -7,6 +7,8 @@ class MockResponse implements ResponseInterface { + private array $headers = []; + public function getProtocolVersion() { // TODO: Implement getProtocolVersion() method. @@ -34,12 +36,14 @@ public function getHeader($name) public function getHeaderLine($name) { - // TODO: Implement getHeaderLine() method. + return $this->headers[$name] ?? ''; } public function withHeader($name, $value) { - // TODO: Implement withHeader() method. + $header = clone $this; + $header->headers[$name] = $value; + return $header; } public function withAddedHeader($name, $value) diff --git a/tests/Mock/MockServerRequest.php b/tests/Mock/MockServerRequest.php index 5130e05..028d963 100644 --- a/tests/Mock/MockServerRequest.php +++ b/tests/Mock/MockServerRequest.php @@ -45,14 +45,15 @@ public function getHeader($name) public function getHeaderLine($name) { - return $this->headers[$name]; + return $this->headers[$name] ?? ''; } public function withHeader($name, $value) { - $this->headers[$name] = $value; + $header = clone $this; + $header->headers[$name] = $value; - return clone $this; + return $header; } public function withAddedHeader($name, $value) @@ -127,9 +128,11 @@ public function getQueryParams() public function withQueryParams(array $query): self { - $this->queryParams = $query; + $queryParams = clone $this; - return clone $this; + $queryParams->queryParams = $query; + + return $queryParams; } public function getUploadedFiles() @@ -149,9 +152,10 @@ public function getParsedBody() public function withParsedBody($data) { - $this->body = $data; + $body = clone $this; + $body->body = $data; - return clone $this; + return $body; } public function getAttributes() @@ -170,9 +174,10 @@ public function getAttribute($name, $default = null) public function withAttribute($name, $value): MockServerRequest { - $this->attributes[$name] = $value; + $attributes = clone $this; + $attributes->attributes[$name] = $value; - return clone $this; + return $attributes; } public function withoutAttribute($name) diff --git a/tests/Mock/Repository/MockAccountRepositoryAccountAuthenticationMiddlewareInvalidToken.php b/tests/Mock/Repository/MockAccountRepositoryAccountAuthenticationMiddlewareInvalidToken.php new file mode 100644 index 0000000..690e4d5 --- /dev/null +++ b/tests/Mock/Repository/MockAccountRepositoryAccountAuthenticationMiddlewareInvalidToken.php @@ -0,0 +1,15 @@ + $this->config->iss, + 'aud' => $this->config->aud, + 'iat' => $now, + 'exp' => $now + $this->config->duration, + 'uuid' => $uuid->getHex()->toString(), + ]; + + return JWT::encode($payload, $this->config->key, $this->config->algorithmus); } } diff --git a/tests/Mock/Service/MockAccessTokenServiceWithoutDuration.php b/tests/Mock/Service/MockAccessTokenServiceWithoutDuration.php new file mode 100644 index 0000000..0b37f8a --- /dev/null +++ b/tests/Mock/Service/MockAccessTokenServiceWithoutDuration.php @@ -0,0 +1,35 @@ + $this->config->iss, + 'aud' => $this->config->aud, + 'iat' => $now, + 'exp' => $now + $this->config->duration, + 'uuid' => $uuid->getHex()->toString(), + ]; + + return JWT::encode($payload, $this->config->key, $this->config->algorithmus); + } +} diff --git a/tests/Mock/Table/MockAccountTableAccountAuthenticationMiddlewareInvalidToken.php b/tests/Mock/Table/MockAccountTableAccountAuthenticationMiddlewareInvalidToken.php new file mode 100644 index 0000000..dd2f6ee --- /dev/null +++ b/tests/Mock/Table/MockAccountTableAccountAuthenticationMiddlewareInvalidToken.php @@ -0,0 +1,83 @@ +getId() !== Account::ID) { + throw new DuplicateEntryException('Account', $data->getId()); + } + + return true; + } + + public function update(AccountInterface $data): true + { + if ($data->getId() !== Account::ID) { + throw new InvalidArgumentException(); + } + + return true; + } + + public function deleteById(int $id): true + { + if ($id !== Account::ID) { + throw new InvalidArgumentException(); + } + + return true; + } + + public function findById(int $id): ?AccountInterface + { + return $id === Account::ID ? $this->hydrator->hydrate(Account::VALID_DATA) : null; + } + + public function findByUuid(UuidInterface $uuid): ?AccountInterface + { + return null; + } + + public function findByName(string $name): ?AccountInterface + { + return $name === Account::NAME ? $this->hydrator->hydrate(Account::VALID_DATA) : null; + } + + public function findByEmail(Email $email): ?AccountInterface + { + return $email->toString() === Account::EMAIL ? $this->hydrator->hydrate(Account::VALID_DATA) : null; + } + + public function findAll(): AccountCollection + { + return $this->hydrator->hydrateCollection([Account::VALID_DATA]); + } +}