Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prüfung auf gültige Autentifizierung impelentiert #15

Merged
merged 2 commits into from
Oct 28, 2024
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
2 changes: 2 additions & 0 deletions config/pipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -37,6 +38,7 @@
UrlHelperMiddleware::class,

ClientIdentificationMiddleware::class,
AccountAuthenticationMiddleware::class,

DispatchMiddleware::class,

Expand Down
1 change: 1 addition & 0 deletions src/App/ConfigProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
63 changes: 63 additions & 0 deletions src/App/Middleware/AccountAuthenticationMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types=1);

namespace Stormannsgal\App\Middleware;

use Fig\Http\Message\StatusCodeInterface as HTTP;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Stormannsgal\App\DTO\AuthenticationFailureMessage;
use Stormannsgal\App\Service\AccessTokenService;
use Stormannsgal\Core\Entity\AccountInterface;
use Stormannsgal\Core\Repository\AccountRepositoryInterface;

use function strlen;

readonly class AccountAuthenticationMiddleware implements MiddlewareInterface
{
public function __construct(
private AccessTokenService $accessTokenService,
private AccountRepositoryInterface $accountRepository,
private LoggerInterface $logger
) {
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$authorization = $request->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));
}
}
24 changes: 24 additions & 0 deletions src/App/Middleware/AccountAuthenticationMiddlewareFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php declare(strict_types=1);

namespace Stormannsgal\App\Middleware;

use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Stormannsgal\App\Service\AccessTokenService;
use Stormannsgal\Core\Repository\AccountRepositoryInterface;

class AccountAuthenticationMiddlewareFactory
{
public function __invoke(ContainerInterface $container): AccountAuthenticationMiddleware
{
$accessTokenService = $container->get(AccessTokenService::class);
$accountRepository = $container->get(AccountRepositoryInterface::class);
$loggerInterface = $container->get(LoggerInterface::class);

return new AccountAuthenticationMiddleware(
$accessTokenService,
$accountRepository,
$loggerInterface
);
}
}
3 changes: 2 additions & 1 deletion src/App/Middleware/GenerateAccessTokenMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
8 changes: 4 additions & 4 deletions src/App/Service/AccessTokenService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();

Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types=1);

namespace Stormannsgal\AppTest\Middleware;

use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Stormannsgal\App\Middleware\AccountAuthenticationMiddleware;
use Stormannsgal\App\Middleware\AccountAuthenticationMiddlewareFactory;
use Stormannsgal\App\Service\AccessTokenService;
use Stormannsgal\Core\Repository\AccountRepositoryInterface;
use Stormannsgal\FunctionalTest\Mock\NullLogger;
use Stormannsgal\Mock\MockContainer;
use Stormannsgal\Mock\Repository\MockAccountRepository;
use Stormannsgal\Mock\Service\MockAccessTokenService;

class AccountAuthenticationMiddlewareFactoryTest extends TestCase
{
public function testCanCreateAccountAuthenticationMiddlewareInstance(): void
{
$container = new MockContainer(
[
AccessTokenService::class => new MockAccessTokenService(),
AccountRepositoryInterface::class => new MockAccountRepository(),
LoggerInterface::class => new NullLogger(),
]
);

$middleware = (new AccountAuthenticationMiddlewareFactory())($container);

$this->assertInstanceOf(AccountAuthenticationMiddleware::class, $middleware);
}
}
116 changes: 116 additions & 0 deletions tests/AppTest/Middleware/AccountAuthenticationMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php declare(strict_types=1);

namespace Stormannsgal\AppTest\Middleware;

use Fig\Http\Message\StatusCodeInterface as HTTP;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Stormannsgal\App\Middleware\AccountAuthenticationMiddleware;
use Stormannsgal\App\Service\AccessTokenService;
use Stormannsgal\Core\Repository\AccountRepositoryInterface;
use Stormannsgal\FunctionalTest\Mock\NullLogger;
use Stormannsgal\Mock\Constants\Account;
use Stormannsgal\Mock\MockAccountAuthenticationMiddlewareRequestHandler;
use Stormannsgal\Mock\Repository\MockAccountRepository;
use Stormannsgal\Mock\Repository\MockAccountRepositoryAccountAuthenticationMiddlewareInvalidToken;
use Stormannsgal\Mock\Service\MockAccessTokenService;
use Stormannsgal\Mock\Service\MockAccessTokenServiceWithoutDuration;

use function json_decode;
use function property_exists;

use const JSON_THROW_ON_ERROR;

class AccountAuthenticationMiddlewareTest extends AbstractTestMiddleware
{
private AccessTokenService $accessTokenService;
private AccountRepositoryInterface $accountRepository;
private LoggerInterface $logger;

protected function setUp(): void
{
parent::setUp();
$this->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');
}
}
4 changes: 2 additions & 2 deletions tests/AppTest/Service/AccessTokenServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -44,6 +44,6 @@ public function testGenerateValidAccessTokenFails(): void

$this->expectException(DomainException::class);

$service->generate($this->account);
$service->generate($this->account->getUuid());
}
}
25 changes: 25 additions & 0 deletions tests/Mock/MockAccountAuthenticationMiddlewareRequestHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types=1);

namespace Stormannsgal\Mock;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Stormannsgal\Core\Entity\AccountInterface;

class MockAccountAuthenticationMiddlewareRequestHandler implements RequestHandlerInterface
{

public function handle(ServerRequestInterface $request): ResponseInterface
{

$account = $request->getAttribute(AccountInterface::AUTHENTICATED);
$response = new MockResponse();

if ($account instanceof AccountInterface) {
return $response->withHeader('Authorization', 'true');
}

return $response;
}
}
8 changes: 6 additions & 2 deletions tests/Mock/MockResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

class MockResponse implements ResponseInterface
{
private array $headers = [];

public function getProtocolVersion()
{
// TODO: Implement getProtocolVersion() method.
Expand Down Expand Up @@ -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)
Expand Down
Loading