diff --git a/libs/api-bundle/composer.json b/libs/api-bundle/composer.json index d3465c78c..c458e1c4a 100644 --- a/libs/api-bundle/composer.json +++ b/libs/api-bundle/composer.json @@ -7,6 +7,7 @@ "cuyz/valinor-bundle": "^0.4", "keboola/permission-checker": "^2.0", "keboola/service-client": "^1.0", + "keboola/storage-api-php-client-branch-wrapper": "^6.7", "monolog/monolog": "^2.0|^3.0", "symfony/dependency-injection": "^6.0|^7.0", "symfony/monolog-bundle": "^3.8", @@ -16,7 +17,6 @@ "keboola/api-error-control": "4.3", "keboola/coding-standard": "^15.0", "keboola/kbc-manage-api-php-client": "^v9.0", - "keboola/storage-api-php-client-branch-wrapper": "^6.1", "phpstan/phpstan": "^2.1", "phpstan/phpstan-phpunit": "^2.0", "phpstan/phpstan-symfony": "^2.0", diff --git a/libs/api-bundle/src/Security/AttributeAuthenticator.php b/libs/api-bundle/src/Security/AttributeAuthenticator.php index 87b5dd062..1d11cb21d 100644 --- a/libs/api-bundle/src/Security/AttributeAuthenticator.php +++ b/libs/api-bundle/src/Security/AttributeAuthenticator.php @@ -18,7 +18,6 @@ use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; -use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; class AttributeAuthenticator extends AbstractAuthenticator @@ -42,14 +41,10 @@ public function authenticate(Request $request): SelfValidatingPassport $authenticator = $this->authenticators->get($authAttribute->getName()); assert($authenticator instanceof TokenAuthenticatorInterface); - $tokenHeader = $authenticator->getTokenHeader(); - $token = $request->headers->get($tokenHeader); + $token = $authenticator->extractToken($request); if ($token === null) { - $error = new CustomUserMessageAuthenticationException(sprintf( - 'Authentication header "%s" is missing', - $tokenHeader, - )); + $error = new CustomUserMessageAuthenticationException('Authentication token is missing'); continue; } diff --git a/libs/api-bundle/src/Security/ManageApiToken/ManageApiTokenAuthenticator.php b/libs/api-bundle/src/Security/ManageApiToken/ManageApiTokenAuthenticator.php index 4adc618e2..e55f2398b 100644 --- a/libs/api-bundle/src/Security/ManageApiToken/ManageApiTokenAuthenticator.php +++ b/libs/api-bundle/src/Security/ManageApiToken/ManageApiTokenAuthenticator.php @@ -9,6 +9,7 @@ use Keboola\ApiBundle\Security\TokenAuthenticatorInterface; use Keboola\ApiBundle\Security\TokenInterface; use Keboola\ManageApi\ClientException as ManageApiClientException; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; @@ -22,9 +23,9 @@ public function __construct( ) { } - public function getTokenHeader(): string + public function extractToken(Request $request): ?string { - return 'X-KBC-ManageApiToken'; + return $request->headers->get('X-KBC-ManageApiToken'); } public function authenticateToken(AuthAttributeInterface $authAttribute, string $token): ManageApiToken diff --git a/libs/api-bundle/src/Security/StorageApiToken/StorageApiTokenAuthenticator.php b/libs/api-bundle/src/Security/StorageApiToken/StorageApiTokenAuthenticator.php index f4ceca986..f8bf3b636 100644 --- a/libs/api-bundle/src/Security/StorageApiToken/StorageApiTokenAuthenticator.php +++ b/libs/api-bundle/src/Security/StorageApiToken/StorageApiTokenAuthenticator.php @@ -10,6 +10,7 @@ use Keboola\ApiBundle\Security\TokenInterface; use Keboola\StorageApi\ClientException; use Keboola\StorageApiBranch\Factory\StorageClientRequestFactory; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; @@ -25,9 +26,20 @@ public function __construct( ) { } - public function getTokenHeader(): string + public function extractToken(Request $request): ?string { - return 'X-StorageApi-Token'; + // Check Authorization header first + $authHeader = $request->headers->get('Authorization'); + if ($authHeader !== null) { + // Validate it's a Bearer token and strip prefix + if (preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) { + return $matches[1]; + } + return $authHeader; + } + + // Check X-StorageApi-Token header + return $request->headers->get('X-StorageApi-Token'); } public function authenticateToken(AuthAttributeInterface $authAttribute, string $token): StorageApiToken diff --git a/libs/api-bundle/src/Security/TokenAuthenticatorInterface.php b/libs/api-bundle/src/Security/TokenAuthenticatorInterface.php index 76e496493..58fc54818 100644 --- a/libs/api-bundle/src/Security/TokenAuthenticatorInterface.php +++ b/libs/api-bundle/src/Security/TokenAuthenticatorInterface.php @@ -5,6 +5,7 @@ namespace Keboola\ApiBundle\Security; use Keboola\ApiBundle\Attribute\AuthAttributeInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Exception\AuthenticationException; /** @@ -12,7 +13,10 @@ */ interface TokenAuthenticatorInterface { - public function getTokenHeader(): string; + /** + * Extract token from request. Returns null if no valid token header found. + */ + public function extractToken(Request $request): ?string; /** * @return TokenType diff --git a/libs/api-bundle/tests/Security/AttributeAuthenticatorTest.php b/libs/api-bundle/tests/Security/AttributeAuthenticatorTest.php index d27cac778..7f3016d22 100644 --- a/libs/api-bundle/tests/Security/AttributeAuthenticatorTest.php +++ b/libs/api-bundle/tests/Security/AttributeAuthenticatorTest.php @@ -82,7 +82,7 @@ public function __invoke(): void {} $authenticator = $this->createAuthenticator( $controller, [ - StorageApiTokenAuth::class => $this->createSuccessAuthenticator($token), + StorageApiTokenAuth::class => $this->createSuccessAuthenticator($token, $request), ], ); $passport = $authenticator->authenticate($request); @@ -100,12 +100,11 @@ public function __invoke(): void {} $request = $this->createControllerRequest($controller, []); - $token = $this->createToken('user-id'); - $tokenAuthenticator = $this->createMock(TokenAuthenticatorInterface::class); $tokenAuthenticator->expects(self::once()) - ->method('getTokenHeader') - ->willReturn('X-Auth-Token') + ->method('extractToken') + ->with($request) + ->willReturn(null) ; $authenticator = $this->createAuthenticator( @@ -116,7 +115,7 @@ public function __invoke(): void {} ); $this->expectException(CustomUserMessageAuthenticationException::class); - $this->expectExceptionMessage('Authentication header "X-Auth-Token" is missing'); + $this->expectExceptionMessage('Authentication token is missing'); $authenticator->authenticate($request); } @@ -136,7 +135,7 @@ public function __invoke(): void {} $authenticator = $this->createAuthenticator( $controller, [ - StorageApiTokenAuth::class => $this->createAuthenticatorWithFailingAuthentication('X-Auth-Token'), + StorageApiTokenAuth::class => $this->createAuthenticatorWithFailingAuthentication($request), ], ); @@ -164,7 +163,7 @@ public function __invoke(): void {} $controller, [ StorageApiTokenAuth::class => $this->createAuthenticatorWithFailingAuthorization( - 'X-Auth-Token', + $request, $token, ), ], @@ -193,15 +192,16 @@ public function __invoke(): void {} $failingAuthenticator = $this->createMock(TokenAuthenticatorInterface::class); $failingAuthenticator->expects(self::once()) - ->method('getTokenHeader') - ->willReturn('X-Other-Token') + ->method('extractToken') + ->with($request) + ->willReturn(null) ; $authenticator = $this->createAuthenticator( $controller, [ ManageApiTokenAuth::class => $failingAuthenticator, - StorageApiTokenAuth::class => $this->createSuccessAuthenticator($token), + StorageApiTokenAuth::class => $this->createSuccessAuthenticator($token, $request), ], ); $passport = $authenticator->authenticate($request); @@ -228,8 +228,8 @@ public function __invoke(): void {} $authenticator = $this->createAuthenticator( $controller, [ - ManageApiTokenAuth::class => $this->createAuthenticatorWithFailingAuthentication('X-Other-Token'), - StorageApiTokenAuth::class => $this->createSuccessAuthenticator($token), + ManageApiTokenAuth::class => $this->createAuthenticatorWithFailingAuthentication($request), + StorageApiTokenAuth::class => $this->createSuccessAuthenticator($token, $request), ], ); $passport = $authenticator->authenticate($request); @@ -258,10 +258,10 @@ public function __invoke(): void {} $controller, [ ManageApiTokenAuth::class => $this->createAuthenticatorWithFailingAuthorization( - 'X-Other-Token', + $request, $otherToken, ), - StorageApiTokenAuth::class => $this->createSuccessAuthenticator($token), + StorageApiTokenAuth::class => $this->createSuccessAuthenticator($token, $request), ], ); $passport = $authenticator->authenticate($request); @@ -306,12 +306,15 @@ private function createToken(string $userIdentifier): TokenInterface /** * @return TokenAuthenticatorInterface */ - private function createSuccessAuthenticator(TokenInterface $token): TokenAuthenticatorInterface - { + private function createSuccessAuthenticator( + TokenInterface $token, + ?Request $request = null, + ): TokenAuthenticatorInterface { $authenticator = $this->createMock(TokenAuthenticatorInterface::class); $authenticator->expects(self::once()) - ->method('getTokenHeader') - ->willReturn('X-Auth-Token') + ->method('extractToken') + ->with($request ?? $this->anything()) + ->willReturn('token') ; $authenticator->expects(self::once()) @@ -337,12 +340,13 @@ private function createSuccessAuthenticator(TokenInterface $token): TokenAuthent /** * @return TokenAuthenticatorInterface */ - private function createAuthenticatorWithFailingAuthentication(string $tokenHeader): TokenAuthenticatorInterface + private function createAuthenticatorWithFailingAuthentication(Request $request): TokenAuthenticatorInterface { $authenticator = $this->createMock(TokenAuthenticatorInterface::class); $authenticator->expects(self::once()) - ->method('getTokenHeader') - ->willReturn($tokenHeader) + ->method('extractToken') + ->with($request) + ->willReturn('token') ; $authenticator->expects(self::once()) ->method('authenticateToken') @@ -357,13 +361,14 @@ private function createAuthenticatorWithFailingAuthentication(string $tokenHeade * @return TokenAuthenticatorInterface */ private function createAuthenticatorWithFailingAuthorization( - string $tokenHeader, + Request $request, TokenInterface $authenticatedToken, ): TokenAuthenticatorInterface { $authenticator = $this->createMock(TokenAuthenticatorInterface::class); $authenticator->expects(self::once()) - ->method('getTokenHeader') - ->willReturn($tokenHeader) + ->method('extractToken') + ->with($request) + ->willReturn('token') ; $authenticator->expects(self::once()) ->method('authenticateToken') diff --git a/libs/api-bundle/tests/Security/ManageApiToken/ManageApiTokenAuthenticatorTest.php b/libs/api-bundle/tests/Security/ManageApiToken/ManageApiTokenAuthenticatorTest.php index f08deac2b..2638df9fc 100644 --- a/libs/api-bundle/tests/Security/ManageApiToken/ManageApiTokenAuthenticatorTest.php +++ b/libs/api-bundle/tests/Security/ManageApiToken/ManageApiTokenAuthenticatorTest.php @@ -11,6 +11,7 @@ use Keboola\ApiBundle\Security\ManageApiToken\ManageApiTokenAuthenticator; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Exception\AccessDeniedException; class ManageApiTokenAuthenticatorTest extends TestCase @@ -315,4 +316,27 @@ public static function provideAuthorizeTokenExceptionsData(): Generator 'Authentication token must not be super admin', ]; } + + public function testExtractTokenFromHeader(): void + { + $authenticator = new ManageApiTokenAuthenticator( + $this->createMock(ManageApiClientFactory::class), + ); + + $request = Request::create('https://keboola.com'); + $request->headers->set('X-KBC-ManageApiToken', 'my-manage-token'); + + self::assertSame('my-manage-token', $authenticator->extractToken($request)); + } + + public function testExtractTokenReturnsNullWhenNoHeader(): void + { + $authenticator = new ManageApiTokenAuthenticator( + $this->createMock(ManageApiClientFactory::class), + ); + + $request = Request::create('https://keboola.com'); + + self::assertNull($authenticator->extractToken($request)); + } } diff --git a/libs/api-bundle/tests/Security/StorageApiToken/StorageApiTokenAuthenticatorTest.php b/libs/api-bundle/tests/Security/StorageApiToken/StorageApiTokenAuthenticatorTest.php index 0b304d623..1051f0593 100644 --- a/libs/api-bundle/tests/Security/StorageApiToken/StorageApiTokenAuthenticatorTest.php +++ b/libs/api-bundle/tests/Security/StorageApiToken/StorageApiTokenAuthenticatorTest.php @@ -110,4 +110,69 @@ public static function provideExceptionData(): Generator 'expectedExceptionCode' => 500, ]; } + + public function testExtractTokenFromPrimaryHeader(): void + { + $authenticator = new StorageApiTokenAuthenticator( + $this->createMock(StorageClientRequestFactory::class), + new RequestStack(), + ); + + $request = Request::create('https://keboola.com'); + $request->headers->set('X-StorageApi-Token', 'my-token'); + + self::assertSame('my-token', $authenticator->extractToken($request)); + } + + public function testExtractTokenFromBearerHeader(): void + { + $authenticator = new StorageApiTokenAuthenticator( + $this->createMock(StorageClientRequestFactory::class), + new RequestStack(), + ); + + $request = Request::create('https://keboola.com'); + $request->headers->set('Authorization', 'Bearer my-bearer-token'); + + self::assertSame('my-bearer-token', $authenticator->extractToken($request)); + } + + public function testExtractTokenFromAuthorizationHeaderWithoutBearer(): void + { + $authenticator = new StorageApiTokenAuthenticator( + $this->createMock(StorageClientRequestFactory::class), + new RequestStack(), + ); + + $request = Request::create('https://keboola.com'); + $request->headers->set('Authorization', 'some-token-without-bearer'); + + self::assertSame('some-token-without-bearer', $authenticator->extractToken($request)); + } + + public function testExtractTokenPrefersAuthorizationHeader(): void + { + $authenticator = new StorageApiTokenAuthenticator( + $this->createMock(StorageClientRequestFactory::class), + new RequestStack(), + ); + + $request = Request::create('https://keboola.com'); + $request->headers->set('X-StorageApi-Token', 'storage-token'); + $request->headers->set('Authorization', 'Bearer bearer-token'); + + self::assertSame('bearer-token', $authenticator->extractToken($request)); + } + + public function testExtractTokenReturnsNullWhenNoHeader(): void + { + $authenticator = new StorageApiTokenAuthenticator( + $this->createMock(StorageClientRequestFactory::class), + new RequestStack(), + ); + + $request = Request::create('https://keboola.com'); + + self::assertNull($authenticator->extractToken($request)); + } }