diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index eb4bc686..d594f3e8 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -3,4 +3,11 @@ api_platform: title: 'Part-DB API' description: 'API of Part-DB' - version: '0.1.0' \ No newline at end of file + version: '0.1.0' + + swagger: + api_keys: + # overridden in OpenApiFactoryDecorator + access_token: + name: Authorization + type: header \ No newline at end of file diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 92b9f188..116bdf40 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -21,6 +21,9 @@ security: user_checker: App\Security\UserChecker entry_point: form_login + access_token: + token_handler: App\Security\ApiTokenHandler + # Enable user impersonation switch_user: { role: CAN_SWITCH_USER } diff --git a/src/ApiPlatform/OpenApiFactoryDecorator.php b/src/ApiPlatform/OpenApiFactoryDecorator.php new file mode 100644 index 00000000..af213e14 --- /dev/null +++ b/src/ApiPlatform/OpenApiFactoryDecorator.php @@ -0,0 +1,50 @@ +. + */ + +declare(strict_types=1); + + +namespace App\ApiPlatform; + +use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; +use ApiPlatform\OpenApi\Model\SecurityScheme; +use ApiPlatform\OpenApi\OpenApi; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; + +#[AsDecorator('api_platform.openapi.factory')] +class OpenApiFactoryDecorator implements OpenApiFactoryInterface +{ + public function __construct(private readonly OpenApiFactoryInterface $decorated) + { + } + + public function __invoke(array $context = []): OpenApi + { + $openApi = $this->decorated->__invoke($context); + $securitySchemes = $openApi->getComponents()->getSecuritySchemes() ?: new \ArrayObject(); + $securitySchemes['access_token'] = new SecurityScheme( + type: 'http', + description: 'Use an API token to authenticate', + name: 'Authorization', + scheme: 'bearer', + ); + return $openApi; + } +} \ No newline at end of file diff --git a/src/Repository/UserSystem/ApiTokenRepository.php b/src/Repository/UserSystem/ApiTokenRepository.php index 7e11ee53..d0de0f7e 100644 --- a/src/Repository/UserSystem/ApiTokenRepository.php +++ b/src/Repository/UserSystem/ApiTokenRepository.php @@ -23,10 +23,11 @@ declare(strict_types=1); namespace App\Repository\UserSystem; -use App\Repository\NamedDBElementRepository; +use App\Entity\UserSystem\ApiToken; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\EntityRepository; +use Doctrine\Persistence\ManagerRegistry; class ApiTokenRepository extends EntityRepository { - } \ No newline at end of file diff --git a/src/Security/ApiTokenHandler.php b/src/Security/ApiTokenHandler.php new file mode 100644 index 00000000..30067c1d --- /dev/null +++ b/src/Security/ApiTokenHandler.php @@ -0,0 +1,61 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Security; + +use App\Entity\UserSystem\ApiToken; +use App\Repository\UserSystem\ApiTokenRepository; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; +use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; + +class ApiTokenHandler implements AccessTokenHandlerInterface +{ + private readonly ApiTokenRepository $apiTokenRepository; + + public function __construct(EntityManagerInterface $entityManager) + { + $this->apiTokenRepository = $entityManager->getRepository(ApiToken::class); + } + + public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge + { + $token = $this->apiTokenRepository->findOneBy(['token' => $accessToken]); + + if (!$token instanceof ApiToken) { + throw new BadCredentialsException(); + } + + if (!$token->isValid()) { + throw new CustomUserMessageAuthenticationException('Token expired'); + } + + return new UserBadge( + userIdentifier: $token->getUser()?->getUserIdentifier() ?? throw new BadCredentialsException(), + ); + } +} \ No newline at end of file