daikon-cqrs/security-interop

View on GitHub
src/Middleware/JwtAuthenticator.php

Summary

Maintainability
A
3 hrs
Test Coverage
F
0%
<?php declare(strict_types=1);
/**
 * This file is part of the daikon-cqrs/security-interop project.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Daikon\Security\Middleware;

use Daikon\Boot\Middleware\RoutingHandler;
use Daikon\Config\ConfigProviderInterface;
use Daikon\Interop\Assertion;
use Daikon\Security\Authentication\AuthenticatorInterface;
use Daikon\Security\Authentication\JwtAuthenticationServiceInterface;
use Daikon\Security\Exception\AuthenticationException;
use Daikon\Security\Middleware\Action\SecureActionInterface;
use Fig\Http\Message\StatusCodeInterface;
use Middlewares\Utils\Factory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class JwtAuthenticator implements MiddlewareInterface, StatusCodeInterface
{
    public const AUTHENTICATOR = '_authenticator';

    private ConfigProviderInterface $config;

    private JwtAuthenticationServiceInterface $authenticationService;

    public function __construct(
        ConfigProviderInterface $config,
        JwtAuthenticationServiceInterface $authenticationService
    ) {
        $this->config = $config;
        $this->authenticationService = $authenticationService;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $requestHandler = $request->getAttribute(RoutingHandler::REQUEST_HANDLER);
        if (!$requestHandler instanceof SecureActionInterface) {
            return $handler->handle($request);
        }

        $authConfig = $this->config->get('project.authentication', []);
        Assertion::true(
            is_a($authConfig['default_role'], AuthenticatorInterface::class, true),
            sprintf("Authentication default role must implement '%s'.", AuthenticatorInterface::class)
        );
        $jwtAttribute = $authConfig['cookies']['jwt']['attribute'] ?? JwtDecoder::DEFAULT_ATTR_JWT;
        $xsrfAttribute = $authConfig['cookies']['xsrf']['attribute'] ?? JwtDecoder::DEFAULT_ATTR_XSRF;

        $jwt = $request->getAttribute($jwtAttribute);
        $xsrfToken = $request->getAttribute($xsrfAttribute);

        try {
            if ($requestHandler->isSecure()) {
                if (!$jwt) {
                    throw new AuthenticationException('Missing JWT.');
                }
                if (!$xsrfToken) {
                    throw new AuthenticationException('Missing XSRF token.');
                }
            }

            if ($jwt) {
                if (!$jwt->uid || !$jwt->jti) {
                    throw new AuthenticationException('Invalid JWT.');
                }
                if ($jwt->xsrf !== $xsrfToken) {
                    throw new AuthenticationException('XSRF token does not match JWT.');
                }
                /** @var AuthenticatorInterface $authenticator */
                $authenticator = $this->authenticationService->authenticateJWT($jwt->uid, $jwt->jti, $jwt->xsrf);
            }
        } catch (AuthenticationException $error) {
            return Factory::createResponse(self::STATUS_UNAUTHORIZED);
        }

        /** @psalm-suppress UndefinedClass */
        return $handler->handle(
            $request->withAttribute(self::AUTHENTICATOR, $authenticator ?? new $authConfig['default_role'])
        );
    }
}