diff --git a/config/packages/security.yaml b/config/packages/security.yaml index a192b691..986d8ed2 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -16,6 +16,17 @@ security: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false + api: + request_matcher: App\Security\ApiRequestMatcher + user_checker: App\Security\UserChecker + anonymous: false + stateless: true + remember_me: false + provider: chain_provider + guard: + authenticators: + - App\Security\TokenAuthenticator + secured_area: kimai_saml: ~ kimai_ldap: ~ @@ -26,7 +37,7 @@ security: guard: authenticators: - - App\Security\TokenAuthenticator + - App\Security\ApiAuthenticator remember_me: secret: '%kernel.secret%' diff --git a/src/Security/ApiAuthenticator.php b/src/Security/ApiAuthenticator.php new file mode 100644 index 00000000..377a132f --- /dev/null +++ b/src/Security/ApiAuthenticator.php @@ -0,0 +1,120 @@ +authenticator = $authenticator; + } + + /** + * @param Request $request + * @return bool + */ + public function supports(Request $request) + { + // API docs can only be access, when the user is logged in + if (strpos($request->getRequestUri(), '/api/doc') !== false) { + return false; + } + + // only try to use this authenticator, when the URL contains the /api/ path + if (strpos($request->getRequestUri(), '/api/') !== false) { + // javascript requests can set a header to disable this authenticator and use the existing session + return !$request->headers->has(self::HEADER_JAVASCRIPT); + } + + return false; + } + + /** + * @param Request $request + * @return array|bool + */ + public function getCredentials(Request $request) + { + return $this->authenticator->getCredentials($request); + } + + /** + * @param array $credentials + * @param UserProviderInterface $userProvider + * @return null|UserInterface + */ + public function getUser($credentials, UserProviderInterface $userProvider) + { + return $this->authenticator->getUser($credentials, $userProvider); + } + + /** + * @param array $credentials + * @param UserInterface $user + * @return bool + */ + public function checkCredentials($credentials, UserInterface $user) + { + return $this->authenticator->checkCredentials($credentials, $user); + } + + /** + * @param Request $request + * @param TokenInterface $token + * @param string $providerKey + * @return null|Response + */ + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + return $this->authenticator->onAuthenticationSuccess($request, $token, $providerKey); + } + + /** + * @param Request $request + * @param AuthenticationException $exception + * @return null|JsonResponse|Response + */ + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + return $this->authenticator->onAuthenticationFailure($request, $exception); + } + + /** + * @param Request $request + * @param AuthenticationException|null $authException + * @return JsonResponse|Response + */ + public function start(Request $request, AuthenticationException $authException = null) + { + return $this->authenticator->start($request, $authException); + } + + /** + * @return bool + */ + public function supportsRememberMe() + { + return false; + } +} diff --git a/src/Security/ApiRequestMatcher.php b/src/Security/ApiRequestMatcher.php new file mode 100644 index 00000000..ba685d99 --- /dev/null +++ b/src/Security/ApiRequestMatcher.php @@ -0,0 +1,29 @@ +getRequestUri(), '/api/doc') !== false) { + return false; + } + + if (!preg_match('{^/api/}', rawurldecode($request->getPathInfo()))) { + return false; + } + + return $request->headers->has(TokenAuthenticator::HEADER_USERNAME) && $request->headers->has(TokenAuthenticator::HEADER_TOKEN); + } +} diff --git a/src/Security/TokenAuthenticator.php b/src/Security/TokenAuthenticator.php index 0854132d..be046bcc 100644 --- a/src/Security/TokenAuthenticator.php +++ b/src/Security/TokenAuthenticator.php @@ -24,7 +24,6 @@ class TokenAuthenticator extends AbstractGuardAuthenticator { public const HEADER_USERNAME = 'X-AUTH-USER'; public const HEADER_TOKEN = 'X-AUTH-TOKEN'; - public const HEADER_JAVASCRIPT = 'X-AUTH-SESSION'; private $encoderFactory; @@ -47,7 +46,7 @@ class TokenAuthenticator extends AbstractGuardAuthenticator // only try to use this authenticator, when the URL contains the /api/ path if (strpos($request->getRequestUri(), '/api/') !== false) { // javascript requests can set a header to disable this authenticator and use the existing session - return !$request->headers->has(self::HEADER_JAVASCRIPT); + return $request->headers->has(self::HEADER_USERNAME) && $request->headers->has(self::HEADER_TOKEN); } return false; diff --git a/tests/Security/ApiAuthenticatorTest.php b/tests/Security/ApiAuthenticatorTest.php new file mode 100644 index 00000000..1564af12 --- /dev/null +++ b/tests/Security/ApiAuthenticatorTest.php @@ -0,0 +1,62 @@ +createMock(EncoderFactoryInterface::class); + $sut = new TokenAuthenticator($factory); + + self::assertFalse($sut->supportsRememberMe()); + } + + public function testSupports() + { + $factory = $this->createMock(EncoderFactoryInterface::class); + $sut = new TokenAuthenticator($factory); + + $request = new Request([], [], [], [], [], ['REQUEST_URI' => 'dfghj/api/doc/dfghj']); + self::assertFalse($sut->supports($request)); + + $request = new Request([], [], [], [], [], ['REQUEST_URI' => '/api/fooo']); + self::assertFalse($sut->supports($request)); + + $request = new Request([], [], [], [], [], ['REQUEST_URI' => '/api/fooo', 'HTTP_X-AUTH-SESSION' => true]); + self::assertFalse($sut->supports($request)); + + $request = new Request([], [], [], [], [], ['REQUEST_URI' => '/api/fooo', 'HTTP_X-AUTH-USER' => 'foo', 'HTTP_X-AUTH-TOKEN' => 'bar']); + self::assertTrue($sut->supports($request)); + } + + public function testGetCredentials() + { + $factory = $this->createMock(EncoderFactoryInterface::class); + $sut = new TokenAuthenticator($factory); + + $request = new Request([], [], [], [], [], ['REQUEST_URI' => '/api/fooo', 'HTTP_X-AUTH-SESSION' => true]); + self::assertEquals(['user' => null, 'token' => null], $sut->getCredentials($request)); + + $request = new Request([], [], [], [], [], ['REQUEST_URI' => '/api/fooo', 'HTTP_X-AUTH-USER' => 'foo']); + self::assertEquals(['user' => 'foo', 'token' => null], $sut->getCredentials($request)); + + $request = new Request([], [], [], [], [], ['REQUEST_URI' => '/api/fooo', 'HTTP_X-AUTH-USER' => 'foo', 'HTTP_X-AUTH-TOKEN' => 'bar']); + self::assertEquals(['user' => 'foo', 'token' => 'bar'], $sut->getCredentials($request)); + } +} diff --git a/tests/Security/TokenAuthenticatorTest.php b/tests/Security/TokenAuthenticatorTest.php index e4bd452f..43829094 100644 --- a/tests/Security/TokenAuthenticatorTest.php +++ b/tests/Security/TokenAuthenticatorTest.php @@ -36,10 +36,13 @@ class TokenAuthenticatorTest extends TestCase self::assertFalse($sut->supports($request)); $request = new Request([], [], [], [], [], ['REQUEST_URI' => '/api/fooo']); - self::assertTrue($sut->supports($request)); + self::assertFalse($sut->supports($request)); $request = new Request([], [], [], [], [], ['REQUEST_URI' => '/api/fooo', 'HTTP_X-AUTH-SESSION' => true]); self::assertFalse($sut->supports($request)); + + $request = new Request([], [], [], [], [], ['REQUEST_URI' => '/api/fooo', 'HTTP_X-AUTH-USER' => 'foo', 'HTTP_X-AUTH-TOKEN' => 'bar']); + self::assertTrue($sut->supports($request)); } public function testGetCredentials()