Updated AuthenticateAction to generate and return a JWT

This commit is contained in:
Alejandro Celaya
2016-08-07 19:13:40 +02:00
parent a60080b1ce
commit 9573e9f4ef
6 changed files with 63 additions and 18 deletions

View File

@@ -1,6 +1,7 @@
<?php <?php
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory; use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Shlinkio\Shlink\Rest\Action; use Shlinkio\Shlink\Rest\Action;
use Shlinkio\Shlink\Rest\Authentication\JWTService;
use Shlinkio\Shlink\Rest\Middleware; use Shlinkio\Shlink\Rest\Middleware;
use Shlinkio\Shlink\Rest\Service; use Shlinkio\Shlink\Rest\Service;
use Zend\ServiceManager\Factory\InvokableFactory; use Zend\ServiceManager\Factory\InvokableFactory;
@@ -9,6 +10,7 @@ return [
'dependencies' => [ 'dependencies' => [
'factories' => [ 'factories' => [
JWTService::class => AnnotatedFactory::class,
Service\RestTokenService::class => AnnotatedFactory::class, Service\RestTokenService::class => AnnotatedFactory::class,
Service\ApiKeyService::class => AnnotatedFactory::class, Service\ApiKeyService::class => AnnotatedFactory::class,

View File

@@ -5,6 +5,8 @@ use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Firebase\JWT\JWT; use Firebase\JWT\JWT;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; 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\ApiKeyService;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
@@ -21,18 +23,27 @@ class AuthenticateAction extends AbstractRestAction
* @var ApiKeyService|ApiKeyServiceInterface * @var ApiKeyService|ApiKeyServiceInterface
*/ */
private $apiKeyService; private $apiKeyService;
/**
* @var JWTServiceInterface
*/
private $jwtService;
/** /**
* AuthenticateAction constructor. * AuthenticateAction constructor.
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService * @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
* @param JWTServiceInterface|JWTService $jwtService
* @param TranslatorInterface $translator * @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->translator = $translator;
$this->apiKeyService = $apiKeyService; $this->apiKeyService = $apiKeyService;
$this->jwtService = $jwtService;
} }
/** /**
@@ -54,15 +65,16 @@ class AuthenticateAction extends AbstractRestAction
} }
// Authenticate using provided API key // Authenticate using provided API key
if (! $this->apiKeyService->check($authData['apiKey'])) { $apiKey = $this->apiKeyService->getByKey($authData['apiKey']);
if (! $apiKey->isValid()) {
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::INVALID_API_KEY_ERROR, 'error' => RestUtils::INVALID_API_KEY_ERROR,
'message' => $this->translator->translate('Provided API key does not exist or is invalid.'), 'message' => $this->translator->translate('Provided API key does not exist or is invalid.'),
], 401); ], 401);
} }
// TODO Generate a JSON Web Token that will be used for authorization in next requests // Generate a JSON Web Token that will be used for authorization in next requests
$token = $this->jwtService->create($apiKey);
return new JsonResponse(['token' => '']); return new JsonResponse(['token' => $token]);
} }
} }

View File

@@ -1,6 +1,7 @@
<?php <?php
namespace Shlinkio\Shlink\Rest\Authentication; namespace Shlinkio\Shlink\Rest\Authentication;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Firebase\JWT\JWT; use Firebase\JWT\JWT;
use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -16,6 +17,8 @@ class JWTService implements JWTServiceInterface
/** /**
* JWTService constructor. * JWTService constructor.
* @param AppOptions $appOptions * @param AppOptions $appOptions
*
* @Inject({AppOptions::class})
*/ */
public function __construct(AppOptions $appOptions) public function __construct(AppOptions $appOptions)
{ {

View File

@@ -52,9 +52,7 @@ class ApiKeyService implements ApiKeyServiceInterface
public function check($key) public function check($key)
{ {
/** @var ApiKey $apiKey */ /** @var ApiKey $apiKey */
$apiKey = $this->em->getRepository(ApiKey::class)->findOneBy([ $apiKey = $this->getByKey($key);
'key' => $key,
]);
if (! isset($apiKey)) { if (! isset($apiKey)) {
return false; return false;
} }
@@ -71,9 +69,7 @@ class ApiKeyService implements ApiKeyServiceInterface
public function disable($key) public function disable($key)
{ {
/** @var ApiKey $apiKey */ /** @var ApiKey $apiKey */
$apiKey = $this->em->getRepository(ApiKey::class)->findOneBy([ $apiKey = $this->getByKey($key);
'key' => $key,
]);
if (! isset($apiKey)) { if (! isset($apiKey)) {
throw new InvalidArgumentException(sprintf('API key "%s" does not exist and can\'t be disabled', $key)); 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] : []; $conditions = $enabledOnly ? ['enabled' => true] : [];
return $this->em->getRepository(ApiKey::class)->findBy($conditions); 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,
]);
}
} }

View File

@@ -36,4 +36,12 @@ interface ApiKeyServiceInterface
* @return ApiKey[] * @return ApiKey[]
*/ */
public function listKeys($enabledOnly = false); 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);
} }

View File

@@ -4,6 +4,8 @@ namespace ShlinkioTest\Shlink\Rest\Action;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Rest\Action\AuthenticateAction; 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 Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
@@ -19,11 +21,20 @@ class AuthenticateActionTest extends TestCase
* @var ObjectProphecy * @var ObjectProphecy
*/ */
protected $apiKeyService; protected $apiKeyService;
/**
* @var ObjectProphecy
*/
protected $jwtService;
public function setUp() public function setUp()
{ {
$this->apiKeyService = $this->prophesize(ApiKeyService::class); $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() public function properApiKeyReturnsTokenInResponse()
{ {
$this->apiKeyService->check('foo')->willReturn(true) $this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->setId(5))
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withParsedBody([ $request = ServerRequestFactory::fromGlobals()->withParsedBody([
'apiKey' => 'foo', 'apiKey' => 'foo',
@@ -58,8 +69,8 @@ class AuthenticateActionTest extends TestCase
*/ */
public function invalidApiKeyReturnsErrorResponse() public function invalidApiKeyReturnsErrorResponse()
{ {
$this->apiKeyService->check('foo')->willReturn(false) $this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->setEnabled(false))
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withParsedBody([ $request = ServerRequestFactory::fromGlobals()->withParsedBody([
'apiKey' => 'foo', 'apiKey' => 'foo',