diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index 8f6dbbfb..e0cc13f7 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -1,6 +1,7 @@ [ 'factories' => [ + JWTService::class => AnnotatedFactory::class, Service\RestTokenService::class => AnnotatedFactory::class, Service\ApiKeyService::class => AnnotatedFactory::class, diff --git a/module/Rest/src/Action/AuthenticateAction.php b/module/Rest/src/Action/AuthenticateAction.php index 093e935f..020a0fb5 100644 --- a/module/Rest/src/Action/AuthenticateAction.php +++ b/module/Rest/src/Action/AuthenticateAction.php @@ -5,6 +5,8 @@ use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Firebase\JWT\JWT; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; +use Shlinkio\Shlink\Rest\Authentication\JWTService; +use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface; use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; use Shlinkio\Shlink\Rest\Util\RestUtils; @@ -21,18 +23,27 @@ class AuthenticateAction extends AbstractRestAction * @var ApiKeyService|ApiKeyServiceInterface */ private $apiKeyService; + /** + * @var JWTServiceInterface + */ + private $jwtService; /** * AuthenticateAction constructor. * @param ApiKeyServiceInterface|ApiKeyService $apiKeyService + * @param JWTServiceInterface|JWTService $jwtService * @param TranslatorInterface $translator * - * @Inject({ApiKeyService::class, "translator"}) + * @Inject({ApiKeyService::class, JWTService::class, "translator"}) */ - public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator) - { + public function __construct( + ApiKeyServiceInterface $apiKeyService, + JWTServiceInterface $jwtService, + TranslatorInterface $translator + ) { $this->translator = $translator; $this->apiKeyService = $apiKeyService; + $this->jwtService = $jwtService; } /** @@ -54,15 +65,16 @@ class AuthenticateAction extends AbstractRestAction } // Authenticate using provided API key - if (! $this->apiKeyService->check($authData['apiKey'])) { + $apiKey = $this->apiKeyService->getByKey($authData['apiKey']); + if (! $apiKey->isValid()) { return new JsonResponse([ 'error' => RestUtils::INVALID_API_KEY_ERROR, 'message' => $this->translator->translate('Provided API key does not exist or is invalid.'), ], 401); } - // TODO Generate a JSON Web Token that will be used for authorization in next requests - - return new JsonResponse(['token' => '']); + // Generate a JSON Web Token that will be used for authorization in next requests + $token = $this->jwtService->create($apiKey); + return new JsonResponse(['token' => $token]); } } diff --git a/module/Rest/src/Authentication/JWTService.php b/module/Rest/src/Authentication/JWTService.php index bc1647c2..ee252606 100644 --- a/module/Rest/src/Authentication/JWTService.php +++ b/module/Rest/src/Authentication/JWTService.php @@ -1,6 +1,7 @@ em->getRepository(ApiKey::class)->findOneBy([ - 'key' => $key, - ]); + $apiKey = $this->getByKey($key); if (! isset($apiKey)) { return false; } @@ -71,9 +69,7 @@ class ApiKeyService implements ApiKeyServiceInterface public function disable($key) { /** @var ApiKey $apiKey */ - $apiKey = $this->em->getRepository(ApiKey::class)->findOneBy([ - 'key' => $key, - ]); + $apiKey = $this->getByKey($key); if (! isset($apiKey)) { throw new InvalidArgumentException(sprintf('API key "%s" does not exist and can\'t be disabled', $key)); } @@ -94,4 +90,17 @@ class ApiKeyService implements ApiKeyServiceInterface $conditions = $enabledOnly ? ['enabled' => true] : []; return $this->em->getRepository(ApiKey::class)->findBy($conditions); } + + /** + * Tries to find one API key by its key string + * + * @param string $key + * @return ApiKey|null + */ + public function getByKey($key) + { + return $this->em->getRepository(ApiKey::class)->findOneBy([ + 'key' => $key, + ]); + } } diff --git a/module/Rest/src/Service/ApiKeyServiceInterface.php b/module/Rest/src/Service/ApiKeyServiceInterface.php index 84c856ce..e1b8ce53 100644 --- a/module/Rest/src/Service/ApiKeyServiceInterface.php +++ b/module/Rest/src/Service/ApiKeyServiceInterface.php @@ -36,4 +36,12 @@ interface ApiKeyServiceInterface * @return ApiKey[] */ public function listKeys($enabledOnly = false); + + /** + * Tries to find one API key by its key string + * + * @param string $key + * @return ApiKey|null + */ + public function getByKey($key); } diff --git a/module/Rest/test/Action/AuthenticateActionTest.php b/module/Rest/test/Action/AuthenticateActionTest.php index 57522852..240b60a6 100644 --- a/module/Rest/test/Action/AuthenticateActionTest.php +++ b/module/Rest/test/Action/AuthenticateActionTest.php @@ -4,6 +4,8 @@ namespace ShlinkioTest\Shlink\Rest\Action; use PHPUnit_Framework_TestCase as TestCase; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Rest\Action\AuthenticateAction; +use Shlinkio\Shlink\Rest\Authentication\JWTService; +use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Zend\Diactoros\Response; use Zend\Diactoros\ServerRequestFactory; @@ -19,11 +21,20 @@ class AuthenticateActionTest extends TestCase * @var ObjectProphecy */ protected $apiKeyService; + /** + * @var ObjectProphecy + */ + protected $jwtService; public function setUp() { $this->apiKeyService = $this->prophesize(ApiKeyService::class); - $this->action = new AuthenticateAction($this->apiKeyService->reveal(), Translator::factory([])); + $this->jwtService = $this->prophesize(JWTService::class); + $this->action = new AuthenticateAction( + $this->apiKeyService->reveal(), + $this->jwtService->reveal(), + Translator::factory([]) + ); } /** @@ -40,8 +51,8 @@ class AuthenticateActionTest extends TestCase */ public function properApiKeyReturnsTokenInResponse() { - $this->apiKeyService->check('foo')->willReturn(true) - ->shouldBeCalledTimes(1); + $this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->setId(5)) + ->shouldBeCalledTimes(1); $request = ServerRequestFactory::fromGlobals()->withParsedBody([ 'apiKey' => 'foo', @@ -58,8 +69,8 @@ class AuthenticateActionTest extends TestCase */ public function invalidApiKeyReturnsErrorResponse() { - $this->apiKeyService->check('foo')->willReturn(false) - ->shouldBeCalledTimes(1); + $this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->setEnabled(false)) + ->shouldBeCalledTimes(1); $request = ServerRequestFactory::fromGlobals()->withParsedBody([ 'apiKey' => 'foo',