Created rest route to perform authentication

This commit is contained in:
Alejandro Celaya 2016-07-04 14:45:18 +02:00
parent 56b2bd3d56
commit dfc5bfd0f2
9 changed files with 263 additions and 0 deletions

View file

@ -13,6 +13,12 @@ return [
],
// Rest
[
'name' => 'rest-authenticate',
'path' => '/rest/authenticate',
'middleware' => Rest\AuthenticateMiddleware::class,
'allowed_methods' => ['POST'],
],
[
'name' => 'rest-create-shortcode',
'path' => '/rest/short-codes',

View file

@ -38,6 +38,7 @@ return [
Service\UrlShortener::class => AnnotatedFactory::class,
Service\VisitsTracker::class => AnnotatedFactory::class,
Service\ShortUrlService::class => AnnotatedFactory::class,
Service\RestTokenService::class => AnnotatedFactory::class,
Cache::class => CacheFactory::class,
// Cli commands
@ -45,6 +46,7 @@ return [
// Middleware
Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\AuthenticateMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\CreateShortcodeMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\ResolveUrlMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\GetVisitsMiddleware::class => AnnotatedFactory::class,

View file

@ -1,6 +1,7 @@
<?php
namespace Acelaya\UrlShortener\Entity;
use Acelaya\UrlShortener\Util\StringUtilsTrait;
use Doctrine\ORM\Mapping as ORM;
/**
@ -13,6 +14,8 @@ use Doctrine\ORM\Mapping as ORM;
*/
class RestToken extends AbstractEntity
{
use StringUtilsTrait;
/**
* The default interval is 20 minutes
*/
@ -32,6 +35,7 @@ class RestToken extends AbstractEntity
public function __construct()
{
$this->updateExpiration();
$this->setRandomTokenKey();
}
/**
@ -86,4 +90,13 @@ class RestToken extends AbstractEntity
{
return $this->setExpirationDate((new \DateTime())->add(new \DateInterval(self::DEFAULT_INTERVAL)));
}
/**
* Sets a random unique token key for this RestToken
* @return RestToken
*/
public function setRandomTokenKey()
{
return $this->setToken($this->generateV4Uuid());
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace Acelaya\UrlShortener\Exception;
class AuthenticationException extends \RuntimeException implements ExceptionInterface
{
public static function fromCredentials($username, $password)
{
return new self(sprintf('Invalid credentials. Username -> "%s". Password -> "%s"', $username, $password));
}
}

View file

@ -0,0 +1,77 @@
<?php
namespace Acelaya\UrlShortener\Middleware\Rest;
use Acelaya\UrlShortener\Exception\AuthenticationException;
use Acelaya\UrlShortener\Service\RestTokenService;
use Acelaya\UrlShortener\Service\RestTokenServiceInterface;
use Acelaya\UrlShortener\Util\RestUtils;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Stratigility\MiddlewareInterface;
class AuthenticateMiddleware implements MiddlewareInterface
{
/**
* @var RestTokenServiceInterface
*/
private $restTokenService;
/**
* AuthenticateMiddleware constructor.
* @param RestTokenServiceInterface|RestTokenService $restTokenService
*
* @Inject({RestTokenService::class})
*/
public function __construct(RestTokenServiceInterface $restTokenService)
{
$this->restTokenService = $restTokenService;
}
/**
* Process an incoming request and/or response.
*
* Accepts a server-side request and a response instance, and does
* something with them.
*
* If the response is not complete and/or further processing would not
* interfere with the work done in the middleware, or if the middleware
* wants to delegate to another process, it can use the `$out` callable
* if present.
*
* If the middleware does not return a value, execution of the current
* request is considered complete, and the response instance provided will
* be considered the response to return.
*
* Alternately, the middleware may return a response instance.
*
* Often, middleware will `return $out();`, with the assumption that a
* later middleware will return a response.
*
* @param Request $request
* @param Response $response
* @param null|callable $out
* @return null|Response
*/
public function __invoke(Request $request, Response $response, callable $out = null)
{
$authData = $request->getParsedBody();
if (! isset($authData['username'], $authData['password'])) {
return new JsonResponse([
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
'message' => 'You have to provide both "username" and "password"'
], 400);
}
try {
$token = $this->restTokenService->createToken($authData['username'], $authData['password']);
return new JsonResponse(['token' => $token->getToken()]);
} catch (AuthenticationException $e) {
return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => 'Invalid username and/or password',
], 401);
}
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Acelaya\UrlShortener\Service;
use Acelaya\UrlShortener\Entity\RestToken;
use Acelaya\UrlShortener\Exception\AuthenticationException;
use Acelaya\UrlShortener\Exception\InvalidArgumentException;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Doctrine\ORM\EntityManagerInterface;
class RestTokenService implements RestTokenServiceInterface
{
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var array
*/
private $restConfig;
/**
* ShortUrlService constructor.
* @param EntityManagerInterface $em
*
* @param array $restConfig
* @Inject({"em", "config.rest"})
*/
public function __construct(EntityManagerInterface $em, array $restConfig)
{
$this->em = $em;
$this->restConfig = $restConfig;
}
/**
* @param string $token
* @return RestToken
* @throws InvalidArgumentException
*/
public function getByToken($token)
{
$restToken = $this->em->getRepository(RestToken::class)->findOneBy([
'token' => $token,
]);
if (! isset($restToken)) {
throw new InvalidArgumentException(sprintf('RestToken not found for token "%s"', $token));
}
return $restToken;
}
/**
* Creates and returns a new RestToken if username and password are correct
* @param $username
* @param $password
* @return RestToken
* @throws AuthenticationException
*/
public function createToken($username, $password)
{
$this->processCredentials($username, $password);
$restToken = new RestToken();
$this->em->persist($restToken);
$this->em->flush();
return $restToken;
}
/**
* @param string $username
* @param string $password
*/
protected function processCredentials($username, $password)
{
$configUsername = strtolower(trim($this->restConfig['username']));
$providedUsername = strtolower(trim($username));
$configPassword = trim($this->restConfig['password']);
$providedPassword = trim($password);
if ($configUsername === $providedUsername && $configPassword === $providedPassword) {
return;
}
// If credentials are not correct, throw exception
throw AuthenticationException::fromCredentials($providedUsername, $providedPassword);
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace Acelaya\UrlShortener\Service;
use Acelaya\UrlShortener\Entity\RestToken;
use Acelaya\UrlShortener\Exception\AuthenticationException;
use Acelaya\UrlShortener\Exception\InvalidArgumentException;
interface RestTokenServiceInterface
{
/**
* @param string $token
* @return RestToken
* @throws InvalidArgumentException
*/
public function getByToken($token);
/**
* Creates and returns a new RestToken if username and password are correct
* @param $username
* @param $password
* @return RestToken
* @throws AuthenticationException
*/
public function createToken($username, $password);
}

View file

@ -8,6 +8,7 @@ class RestUtils
const INVALID_SHORTCODE_ERROR = 'INVALID_SHORTCODE';
const INVALID_URL_ERROR = 'INVALID_URL';
const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT';
const INVALID_CREDENTIALS = 'INVALID_CREDENTIALS';
const UNKNOWN_ERROR = 'UNKNOWN_ERROR';
public static function getRestErrorCodeFromException(Exception\ExceptionInterface $e)
@ -19,6 +20,8 @@ class RestUtils
return self::INVALID_URL_ERROR;
case $e instanceof Exception\InvalidArgumentException:
return self::INVALID_ARGUMENT_ERROR;
case $e instanceof Exception\AuthenticationException:
return self::INVALID_CREDENTIALS;
default:
return self::UNKNOWN_ERROR;
}

View file

@ -0,0 +1,40 @@
<?php
namespace Acelaya\UrlShortener\Util;
trait StringUtilsTrait
{
protected function generateRandomString($length = 10)
{
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
protected function generateV4Uuid()
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
// 32 bits for "time_low"
mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
// 16 bits for "time_mid"
mt_rand(0, 0xffff),
// 16 bits for "time_hi_and_version",
// four most significant bits holds version number 4
mt_rand(0, 0x0fff) | 0x4000,
// 16 bits, 8 bits for "clk_seq_hi_res",
// 8 bits for "clk_seq_low",
// two most significant bits holds zero and one for variant DCE1.1
mt_rand(0, 0x3fff) | 0x8000,
// 48 bits for "node"
mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0xffff)
);
}
}