Refactor api bundle to use security to handle authentication #164
This commit is contained in:
parent
2b9499861b
commit
0d41b46ee9
13 changed files with 485 additions and 24 deletions
9
catalyst/api-bundle/CatalystAPIBundle.php
Normal file
9
catalyst/api-bundle/CatalystAPIBundle.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Catalyst\APIBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class CatalystAPIBundle extends Bundle
|
||||
{
|
||||
}
|
||||
138
catalyst/api-bundle/Entity/User.php
Normal file
138
catalyst/api-bundle/Entity/User.php
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace Catalyst\APIBundle\Entity;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="api_user")
|
||||
*/
|
||||
class User implements UserInterface
|
||||
{
|
||||
// api key
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="string", length=32)
|
||||
*/
|
||||
protected $api_key;
|
||||
|
||||
// secret key
|
||||
/**
|
||||
* @ORM\Column(type="string", length=32)
|
||||
*/
|
||||
protected $secret_key;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=80)
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
// date created
|
||||
/**
|
||||
* @ORM\Column(type="datetime")
|
||||
*/
|
||||
protected $date_create;
|
||||
|
||||
// roles
|
||||
// TODO: make this db loaded
|
||||
protected $roles;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// generate keys
|
||||
$this->setAPIKey($this->generateAPIKey())
|
||||
->setSecretKey($this->generateSecretKey());
|
||||
|
||||
// set date created
|
||||
$this->date_create = new DateTime();
|
||||
}
|
||||
|
||||
public function getID()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setAPIKey($api_key)
|
||||
{
|
||||
$this->api_key = $api_key;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAPIKey()
|
||||
{
|
||||
return $this->api_key;
|
||||
}
|
||||
|
||||
public function setSecretKey($key)
|
||||
{
|
||||
$this->secret_key = $key;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSecretKey()
|
||||
{
|
||||
return $this->secret_key;
|
||||
}
|
||||
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getRoles()
|
||||
{
|
||||
return ['ROLE_API'];
|
||||
}
|
||||
|
||||
public function getDateCreate()
|
||||
{
|
||||
return $this->date_create;
|
||||
}
|
||||
|
||||
public function getPassword()
|
||||
{
|
||||
// we don't need this for API
|
||||
return 'notneeded';
|
||||
}
|
||||
|
||||
public function getSalt()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getUsername()
|
||||
{
|
||||
// since it's an api, the api key IS the username
|
||||
return $this->api_key;
|
||||
}
|
||||
|
||||
public function eraseCredentials()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public function generateAPIKey()
|
||||
{
|
||||
return $this->generateKey('api');
|
||||
}
|
||||
|
||||
public function generateSecretKey()
|
||||
{
|
||||
return $this->generateKey('secret');
|
||||
}
|
||||
|
||||
protected function generateKey($prefix = '')
|
||||
{
|
||||
return md5(uniqid($prefix, true));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3,19 +3,29 @@
|
|||
namespace Catalyst\APIBundle\EventSubscriber;
|
||||
|
||||
use Catalyst\APIBundle\Controller\APIController;
|
||||
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class TokenSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
const HEADER_API_KEY = 'X-Catalyst-API-Key';
|
||||
const HEADER_SIGNATURE = 'X-Catalyst-Signature';
|
||||
const HEADER_API_KEY = 'X-Cata-API-Key';
|
||||
const HEADER_SIGNATURE = 'X-Cata-Signature';
|
||||
const HEADER_DATE = 'X-Cata-Date';
|
||||
|
||||
const MODE_HEADER = 'header';
|
||||
const MODE_QUERY_STRING = 'query';
|
||||
const DATE_FORMAT = 'D, d M Y H:i:s T';
|
||||
|
||||
const QUERY_API_KEY = 'api_key';
|
||||
const QUERY_SIGNATURE = 'sig';
|
||||
|
||||
// 30 minute time limit
|
||||
const TIME_LIMIT = 1800;
|
||||
|
||||
protected $em;
|
||||
|
||||
|
|
@ -24,6 +34,33 @@ class TokenSubscriber implements EventSubscriberInterface
|
|||
$this-> em = $em;
|
||||
}
|
||||
|
||||
protected function getSecretKey($api_key)
|
||||
{
|
||||
return 'sldkfjlksdjflksdjflksdjflsjf';
|
||||
}
|
||||
|
||||
protected function validateSignature($req, $hdate_string, $secret_key, $sig)
|
||||
{
|
||||
// get needed params for generation
|
||||
$method = $req->getRealMethod();
|
||||
$uri = $req->getRequestUri();
|
||||
|
||||
$elements = [$method, $uri, $hdate_string, $secret_key];
|
||||
$sig_source = implode('|', $elements);
|
||||
|
||||
error_log($sig_source);
|
||||
|
||||
// generate signature
|
||||
$raw_sig = hash_hmac('sha1', $sig_source, $secret_key, true);
|
||||
$enc_sig = base64_encode($raw_sig);
|
||||
|
||||
error_log($enc_sig);
|
||||
|
||||
if ($enc_sig != trim($sig))
|
||||
throw new AccessDeniedHttpException('Invalid signature.');
|
||||
|
||||
}
|
||||
|
||||
public function onKernelController(FilterControllerEvent $event)
|
||||
{
|
||||
$controller = $event->getController();
|
||||
|
|
@ -37,25 +74,41 @@ class TokenSubscriber implements EventSubscriberInterface
|
|||
if (!($controller[0] instanceof APIController))
|
||||
return;
|
||||
|
||||
// TODO: check if we have a mode setup
|
||||
|
||||
// TODO: if no mode specified default to header
|
||||
|
||||
$req = $event->getRequest();
|
||||
|
||||
// check date from headers
|
||||
$headers = $req->headers->all();
|
||||
$hdate_string = $req->headers->get(self::HEADER_DATE);
|
||||
if ($hdate_string == null)
|
||||
throw new AccessDeniedHttpException('No date specified.');
|
||||
|
||||
$hdate = DateTime::createFromFormat(self::DATE_FORMAT, $hdate_string);
|
||||
if ($hdate == null)
|
||||
throw new AccessDeniedHttpException('Invalid date specified.');
|
||||
|
||||
// get number of seconds difference
|
||||
$date_now = new DateTime();
|
||||
$date_diff = abs($date_now->getTimestamp() - $hdate->getTimestamp());
|
||||
|
||||
// time difference is too much
|
||||
if ($date_diff > self::TIME_LIMIT)
|
||||
throw new AccessDeniedHttpException('Clock synchronization error.');
|
||||
|
||||
// api key header
|
||||
$api_key = $req->headers->get(self::HEADER_API_KEY);
|
||||
if ($api_key == null)
|
||||
throw new AccessDeniedHttpException('No api key sent.');
|
||||
|
||||
// TODO: check valid api key
|
||||
// check valid api key
|
||||
$secret_key = $this->getSecretKey($api_key);
|
||||
|
||||
// signature header
|
||||
$sig = $req->header->get(self::HEADER_SIGNATURE);
|
||||
$sig = $req->headers->get(self::HEADER_SIGNATURE);
|
||||
if ($sig == null)
|
||||
throw new AccessDeniedHttpException('No signature sent.');
|
||||
|
||||
// TODO: check valid signature
|
||||
// check valid signature
|
||||
$this->validateSignature($req, $hdate_string, $secret_key, $sig);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Catalyst\APIBundle\Library;
|
||||
|
||||
class Response
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
9
catalyst/api-bundle/Response/Response.php
Normal file
9
catalyst/api-bundle/Response/Response.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Catalyst\APIBundle\Response;
|
||||
|
||||
class Response
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
162
catalyst/api-bundle/Security/APIKeyAuthenticator.php
Normal file
162
catalyst/api-bundle/Security/APIKeyAuthenticator.php
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
namespace Catalyst\APIBundle\Security;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
use DateTime;
|
||||
|
||||
class APIKeyAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
|
||||
{
|
||||
const HEADER_API_KEY = 'X-Cata-API-Key';
|
||||
const HEADER_SIGNATURE = 'X-Cata-Signature';
|
||||
const HEADER_DATE = 'X-Cata-Date';
|
||||
|
||||
const DATE_FORMAT = 'D, d M Y H:i:s T';
|
||||
|
||||
// 30 minute time limit
|
||||
const TIME_LIMIT = 1800;
|
||||
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
protected function getSecretKey($api_key)
|
||||
{
|
||||
return 'sldkfjlksdjflksdjflksdjflsjf';
|
||||
}
|
||||
|
||||
protected function validateSignature($req, $hdate_string, $secret_key, $sig)
|
||||
{
|
||||
// get needed params for generation
|
||||
$method = $req->getRealMethod();
|
||||
$uri = $req->getRequestUri();
|
||||
|
||||
$elements = [$method, $uri, $hdate_string, $secret_key];
|
||||
$sig_source = implode('|', $elements);
|
||||
|
||||
error_log($sig_source);
|
||||
|
||||
// generate signature
|
||||
$raw_sig = hash_hmac('sha1', $sig_source, $secret_key, true);
|
||||
$enc_sig = base64_encode($raw_sig);
|
||||
|
||||
error_log($enc_sig);
|
||||
|
||||
if ($enc_sig != trim($sig))
|
||||
throw new BadCredentialsException('Invalid signature.');
|
||||
|
||||
}
|
||||
|
||||
public function createToken(Request $req, $provider_key)
|
||||
{
|
||||
// api key header
|
||||
$api_key = $req->headers->get(self::HEADER_API_KEY);
|
||||
if ($api_key == null)
|
||||
throw new BadCredentialsException('No API key sent.');
|
||||
|
||||
// check date from headers
|
||||
$hdate_string = $req->headers->get(self::HEADER_DATE);
|
||||
if ($hdate_string == null)
|
||||
throw new BadCredentialsException('No date specified.');
|
||||
|
||||
$hdate = DateTime::createFromFormat(self::DATE_FORMAT, $hdate_string);
|
||||
if ($hdate == null)
|
||||
throw new BadCredentialsException('Invalid date specified.');
|
||||
|
||||
// get number of seconds difference
|
||||
$date_now = new DateTime();
|
||||
$date_diff = abs($date_now->getTimestamp() - $hdate->getTimestamp());
|
||||
|
||||
// time difference is too much
|
||||
if ($date_diff > self::TIME_LIMIT)
|
||||
throw new BadCredentialsException('Clock synchronization error.');
|
||||
|
||||
// check valid api key
|
||||
$secret_key = $this->getSecretKey($api_key);
|
||||
|
||||
// signature header
|
||||
$sig = $req->headers->get(self::HEADER_SIGNATURE);
|
||||
if ($sig == null)
|
||||
throw new BadCredentialsException('No signature sent.');
|
||||
|
||||
// check valid signature
|
||||
$this->validateSignature($req, $hdate_string, $secret_key, $sig);
|
||||
|
||||
return new PreAuthenticatedToken(
|
||||
'anonymous',
|
||||
$api_key,
|
||||
$provider_key
|
||||
);
|
||||
}
|
||||
|
||||
public function supportsToken(TokenInterface $token, $provider_key)
|
||||
{
|
||||
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $provider_key;
|
||||
}
|
||||
|
||||
public function authenticateToken(TokenInterface $token, UserProviderInterface $user_provider, $provider_key)
|
||||
{
|
||||
if (!$user_provider instanceof APIKeyUserProvider)
|
||||
{
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf(
|
||||
'The user provider must be an instance of APIKeyUserProvider (%s was given).',
|
||||
get_class($user_provider)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$api_key = $token->getCredentials();
|
||||
$user = $user_provider->getUserByAPIKey($api_key);
|
||||
|
||||
/*
|
||||
$username = $user_provider->getUsernameForAPIKey($api_key);
|
||||
|
||||
if (!$username)
|
||||
{
|
||||
// CAUTION: this message will be returned to the client
|
||||
// (so don't put any un-trusted messages / error strings here)
|
||||
throw new CustomUserMessageAuthenticationException(
|
||||
sprintf('API Key "%s" does not exist.', $api_key)
|
||||
);
|
||||
}
|
||||
*/
|
||||
if (!$user)
|
||||
throw new CustomUserMessageAuthenticationException('Invalid API Key');
|
||||
|
||||
// $user = $user_provider->loadUserByUsername($username);
|
||||
|
||||
return new PreAuthenticatedToken(
|
||||
$user,
|
||||
$api_key,
|
||||
$provider_key,
|
||||
$user->getRoles()
|
||||
);
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $req, AuthenticationException $exception)
|
||||
{
|
||||
$data = [
|
||||
'success' => false,
|
||||
'error' => [
|
||||
'message' => $exception->getMessage(),
|
||||
],
|
||||
];
|
||||
return new JsonResponse($data, 401);
|
||||
}
|
||||
}
|
||||
62
catalyst/api-bundle/Security/APIKeyUserProvider.php
Normal file
62
catalyst/api-bundle/Security/APIKeyUserProvider.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace Catalyst\APIBundle\Security;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
use Catalyst\APIBundle\Entity\User;
|
||||
|
||||
class APIKeyUserProvider implements UserProviderInterface
|
||||
{
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function getUserByAPIKey($api_key)
|
||||
{
|
||||
$user = $this->em->getRepository(User::class)->find($api_key);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function getUsernameForAPIKey($apiKey)
|
||||
{
|
||||
// Look up the username based on the token in the database, via
|
||||
// an API call, or do something entirely different
|
||||
$username = 'test';
|
||||
|
||||
return $username;
|
||||
}
|
||||
|
||||
public function loadUserByUsername($username)
|
||||
{
|
||||
return new User(
|
||||
$username,
|
||||
null,
|
||||
// the roles for the user - you may choose to determine
|
||||
// these dynamically somehow based on the user
|
||||
array('ROLE_API')
|
||||
);
|
||||
}
|
||||
|
||||
public function refreshUser(UserInterface $user)
|
||||
{
|
||||
// this is used for storing authentication in the session
|
||||
// but in this example, the token is sent in each request,
|
||||
// so authentication can be stateless. Throwing this exception
|
||||
// is proper to make things stateless
|
||||
throw new UnsupportedUserException();
|
||||
}
|
||||
|
||||
public function supportsClass($class)
|
||||
{
|
||||
return User::class === $class;
|
||||
}
|
||||
}
|
||||
18
catalyst/api-bundle/Service/AccessDeniedHandler.php
Normal file
18
catalyst/api-bundle/Service/AccessDeniedHandler.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Catalyst\APIBundle\Service;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
|
||||
|
||||
class AccessDeniedHandler implements AccessDeniedHandlerInterface
|
||||
{
|
||||
public function handle(Request $req, AccessDeniedException $exception)
|
||||
{
|
||||
$content = $exception->getMessage();
|
||||
|
||||
return new Response($content, 403);
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,6 @@
|
|||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/",
|
||||
"Catalyst\\": "catalyst-libs/",
|
||||
"Catalyst\\APIBundle\\": "catalyst/api-bundle/"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,5 +11,7 @@ return [
|
|||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
|
||||
Catalyst\APIBundle\CatalystAPIBundle::class => ['all' => true],
|
||||
// DataDog\AuditBundle\DataDogAuditBundle::class => ['all' => true],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ security:
|
|||
algorithm: bcrypt
|
||||
cost: 12
|
||||
providers:
|
||||
user:
|
||||
user_provider:
|
||||
entity:
|
||||
class: App\Entity\User
|
||||
property: username
|
||||
api_key_user_provider:
|
||||
id: Catalyst\APIBundle\Security\APIKeyUserProvider
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
|
|
@ -29,9 +31,13 @@ security:
|
|||
|
||||
warranty_api:
|
||||
pattern: ^\/capi\/
|
||||
security: false
|
||||
stateless: true
|
||||
simple_preauth:
|
||||
authenticator: Catalyst\APIBundle\Security\APIKeyAuthenticator
|
||||
provider: api_key_user_provider
|
||||
|
||||
main:
|
||||
provider: user_provider
|
||||
form_login:
|
||||
login_path: login
|
||||
check_path: login
|
||||
|
|
|
|||
|
|
@ -80,4 +80,11 @@ services:
|
|||
arguments:
|
||||
$em: "@doctrine.orm.entity_manager"
|
||||
tags: ['kernel.event_subscriber']
|
||||
|
||||
|
||||
Catalyst\APIBundle\Security\APIKeyUserProvider:
|
||||
arguments:
|
||||
$em: "@doctrine.orm.entity_manager"
|
||||
|
||||
Catalyst\APIBundle\Security\APIKeyAuthenticator:
|
||||
arguments:
|
||||
$em: "@doctrine.orm.entity_manager"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use App\Entity\VehicleManufacturer;
|
|||
use Doctrine\ORM\Query;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
use App\Menu\Generator as MenuGenerator;
|
||||
|
|
@ -19,5 +20,9 @@ class APITestController extends BaseController implements APIController
|
|||
{
|
||||
public function test()
|
||||
{
|
||||
$data = [
|
||||
'success' => true,
|
||||
];
|
||||
return new JsonResponse($data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue