Hello everyone!

We have been investing plenty of personal time and energy for many years to share our knowledge with you all. However, we now need your help to keep this blog running. All you have to do is just click one of the adverts on the site, otherwise it will sadly be taken down due to hosting etc. costs. Thank you.

In this example we are going to create a Symfony API and secure it with OAuth1. User will have a "Consumer Key" and "Consumer Secret" in order to consume our API.


Prerequisites



Restart Apache service after the changes above.


Endpoints



parameters.yml


parameters:
oauth.version: '1.0'
oauth.signature_method: HMAC-SHA1
oauth.lifetime: 300
oauth.cache_prefix: oauth_cache_

routing.yml


customer:
resource: "@CustomerBundle/Controller/"
type: annotation
prefix: /v1

security.yml


security:

role_hierarchy:
ROLE_API_USER: ROLE_USER
ROLE_ADMIN_USER: ROLE_USER

providers:
user:
id: customer.security.provider.user

firewalls:
oauth_secured:
pattern: ^/
stateless: true
oauth: true

CustomerController


namespace CustomerBundle\Controller;

use CustomerBundle\Security\TokenUser;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("/customers", service="customer.controller.customer")
*/
class CustomerController
{
private $tokenUser;

public function __construct(TokenUser $tokenUser)
{
$this->tokenUser = $tokenUser;
}

/**
* @param Request $request
* @param int $id
*
* @Method({"GET"})
* @Route("/{id}", requirements={"id"="\d+"})
* @Security("has_role('ROLE_API_USER')")
*
* @return Response
*/
public function getOneAction(Request $request, $id)
{
echo $request->getContent().PHP_EOL;
echo $id.PHP_EOL;

return new Response('GET:'.$this->tokenUser->get()->getApiKey());
}

/**
* @param Request $request
*
* @Method({"POST"})
* @Route("")
* @Security("has_role('ROLE_ADMIN_USER')")
*
* @return Response
*/
public function createOneAction(Request $request)
{
echo $request->getContentType().PHP_EOL;
echo $request->getContent().PHP_EOL;

return new Response('POST:'.$this->tokenUser->get()->getApiKey());
}
}

services:
customer.controller.customer:
class: CustomerBundle\Controller\CustomerController
arguments:
- "@customer.security.token_user"

User


namespace CustomerBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
* @ORM\Entity(repositoryClass="CustomerBundle\Repository\UserRepository")
* @ORM\Table(
* name="user",
* uniqueConstraints={
* @ORM\UniqueConstraint(name="unq_api_key", columns={"api_key"})
* }
* )
*/
class User implements UserInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(name="id", type="integer")
*/
private $id;

/**
* @var string
*
* @ORM\Column(name="api_key", type="string", length=36)
*/
private $apiKey;

/**
* @var string
*
* @ORM\Column(name="api_secret", type="string", length=40)
*/
private $apiSecret;

/**
* @var array
*
* @ORM\Column(name="roles", type="array")
*/
private $roles;

public function __construct($apiKey, array $roles)
{
$this->apiKey = $apiKey;
$this->apiSecret = sha1(microtime());
$this->roles = $roles;
}

public function getId()
{
return $this->id;
}

public function getApiKey()
{
return $this->apiKey;
}

public function getApiSecret()
{
return $this->apiSecret;
}

public function getRoles()
{
return $this->roles;
}

public function getUsername()
{
}

public function getPassword()
{
}

public function getSalt()
{
}

public function eraseCredentials()
{
}
}

UserRepository


namespace CustomerBundle\Repository;

use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;

class UserRepository extends EntityRepository
{
public function findOneByApiKey($apiKey)
{
return $this->createQueryBuilder('u')
->where('u.apiKey = :apiKey')
->setParameter('apiKey', $apiKey)
->getQuery()
->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true)
->getOneOrNullResult(Query::HYDRATE_SIMPLEOBJECT);
}
}

services:
customer.repository.user:
class: CustomerBundle\Repository\UserRepository
factory: [ "@doctrine.orm.entity_manager", getRepository ]
arguments:
- CustomerBundle\Entity\User

OauthFactory


namespace CustomerBundle\Security;

use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;

class OauthFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'customer.security.provider.'.$id;
$container
->setDefinition($providerId, new ChildDefinition(OauthProvider::class))
->replaceArgument(0, new Reference($userProvider));

$listenerId = 'customer.security.listener.'.$id;
$container
->setDefinition($listenerId, new ChildDefinition(OauthListener::class));

return [$providerId, $listenerId, $defaultEntryPoint];
}

public function getPosition()
{
return 'pre_auth';
}

public function getKey()
{
return 'oauth';
}

public function addConfiguration(NodeDefinition $node)
{
}
}

OauthListener


namespace CustomerBundle\Security;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;

class OauthListener implements ListenerInterface
{
private $tokenStorage;
private $authenticationManager;
private $logger;

public function __construct(
TokenStorageInterface $tokenStorage,
AuthenticationManagerInterface $authenticationManager,
LoggerInterface $logger
) {
$this->tokenStorage = $tokenStorage;
$this->authenticationManager = $authenticationManager;
$this->logger = $logger;
}

public function handle(GetResponseEvent $event)
{
try {
$token = $this->getToken($event->getRequest());
$authToken = $this->authenticationManager->authenticate($token);
$this->tokenStorage->setToken($authToken);
} catch (AuthenticationException $e) {
$this->logger->info(sprintf('[Oauth] %s', $e->getMessage()));

throw new UnauthorizedHttpException('OAuth', 'Authentication failed.');
}

return;
}

private function getToken(Request $request)
{
$oauthRegex = '/OAuth oauth_consumer_key="([^"]+)",oauth_signature_method="([^"]+)",oauth_timestamp="([^"]+)",oauth_nonce="([^"]+)",oauth_version="([^"]+)",oauth_signature="([^"]+)"/';
if (
!$request->headers->has('authorization') ||
preg_match($oauthRegex, stripslashes($request->headers->get('authorization')), $matches) !== 1
) {
throw new AuthenticationException('Invalid token pattern.');
}

$oauthCredential = (new OauthCredential())
->setOauthConsumerKey($matches[1])
->setOauthSignature($matches[6])
->setOauthSignatureMethod($matches[2])
->setOauthVersion($matches[5])
->setOauthTimestamp($matches[3])
->setOauthNonce($matches[4])
->setMethod($request->getMethod())
->setBaseUrl(explode('?', $request->getUri())[0])
->setQueryStringParameters($request->query->all())
->setRequestBodyParameters($request->getContent());

return new OauthToken($oauthCredential);
}
}

OauthCredential


namespace CustomerBundle\Security;

class OauthCredential
{
private $oauthConsumerKey;
private $oauthSignature;
private $oauthSignatureMethod;
private $oauthVersion;
private $oauthTimestamp;
private $oauthNonce;
private $method;
private $baseUrl;
private $queryStringParameters;
private $requestBodyParameters;

public function setOauthConsumerKey($oauthConsumerKey)
{
$this->oauthConsumerKey = rawurldecode($oauthConsumerKey);

return $this;
}

public function getOauthConsumerKey()
{
return $this->oauthConsumerKey;
}

public function setOauthSignature($oauthSignature)
{
$this->oauthSignature = rawurldecode($oauthSignature);

return $this;
}

public function getOauthSignature()
{
return $this->oauthSignature;
}

public function setOauthSignatureMethod($oauthSignatureMethod)
{
$this->oauthSignatureMethod = strtoupper(rawurldecode($oauthSignatureMethod));

return $this;
}

public function getOauthSignatureMethod()
{
return $this->oauthSignatureMethod;
}

public function setOauthVersion($oauthVersion)
{
$this->oauthVersion = rawurldecode($oauthVersion);

return $this;
}

public function getOauthVersion()
{
return $this->oauthVersion;
}

public function setOauthTimestamp($oauthTimestamp)
{
$this->oauthTimestamp = $oauthTimestamp;

return $this;
}

public function getOauthTimestamp()
{
return $this->oauthTimestamp;
}

public function setOauthNonce($oauthNonce)
{
$this->oauthNonce = $oauthNonce;

return $this;
}

public function getOauthNonce()
{
return $this->oauthNonce;
}

public function setMethod($method)
{
$this->method = strtoupper($method);

return $this;
}

public function getMethod()
{
return $this->method;
}

public function setBaseUrl($baseUrl)
{
$this->baseUrl = $baseUrl;

return $this;
}

public function getBaseUrl()
{
return $this->baseUrl;
}

public function setQueryStringParameters(array $queryStringParameters)
{
$this->queryStringParameters = $queryStringParameters;

return $this;
}

public function getQueryStringParameters()
{
return $this->queryStringParameters;
}

public function setRequestBodyParameters($requestBodyParameters)
{
$this->requestBodyParameters = $requestBodyParameters;

return $this;
}

public function getRequestBodyParameters()
{
return $this->requestBodyParameters;
}

public function createSignatureParameters()
{
$oauthParameters = [
'oauth_consumer_key' => $this->getOauthConsumerKey(),
'oauth_signature_method' => $this->getOauthSignatureMethod(),
'oauth_timestamp' => $this->getOauthTimestamp(),
'oauth_nonce' => $this->getOauthNonce(),
'oauth_version' => $this->getOauthVersion(),
];

$queryStringParameters = $this->getQueryStringParameters();
parse_str($this->getRequestBodyParameters(), $requestBodyParameters);

$parameters = $oauthParameters + $queryStringParameters + $requestBodyParameters;
$parameters = array_filter($parameters);
ksort($parameters);

return $parameters;
}
}

OauthToken


namespace CustomerBundle\Security;

use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;

class OauthToken extends AbstractToken
{
private $credential;

public function __construct(OauthCredential $credential, array $roles = [])
{
parent::__construct($roles);

$this->credential = $credential;
$this->setUser($credential->getOauthConsumerKey());
}

public function getCredentials()
{
return $this->credential;
}
}

OauthProvider


namespace CustomerBundle\Security;

use OAuth;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class OauthProvider implements AuthenticationProviderInterface
{
private $userProvider;
private $cachePool;
private $version;
private $signatureMethod;
private $lifetime;
private $cachePrefix;

public function __construct(
UserProviderInterface $userProvider,
CacheItemPoolInterface $cachePool,
$version,
$signatureMethod,
$lifetime,
$cachePrefix
) {
$this->userProvider = $userProvider;
$this->cachePool = $cachePool;
$this->version = $version;
$this->signatureMethod = $signatureMethod;
$this->lifetime = $lifetime;
$this->cachePrefix = $cachePrefix;
}

public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByUsername($token->getUsername());

if (!$user instanceof UserInterface) {
throw new AuthenticationException('User not found.');
}

if (!$this->validateCredentials($token->getCredentials(), $user)) {
throw new AuthenticationException('Invalid credentials.');
}

$oauthToken = new OauthToken($token->getCredentials(), $user->getRoles());
$oauthToken->setUser($user);

return $oauthToken;
}

public function supports(TokenInterface $token)
{
return $token instanceof OauthToken;
}

private function validateCredentials(OauthCredential $credential, UserInterface $user)
{
if ($credential->getOauthSignatureMethod() !== $this->signatureMethod) {
throw new AuthenticationException('Invalid signature method.');
}

if ($credential->getOauthVersion() !== $this->version) {
throw new AuthenticationException('Invalid version.');
}

if ($credential->getOauthTimestamp() > time()) {
throw new AuthenticationException('Timestamp is in future.');
}

if (time() - $credential->getOauthTimestamp() > $this->lifetime) {
throw new AuthenticationException('Timestamp is old.');
}

$cacheItem = $this->cachePool->getItem($this->cachePrefix.md5($credential->getOauthNonce()));
if ($cacheItem->isHit()) {
throw new AuthenticationException('Nonce is already used.');
}
$cacheItem->set(null)->expiresAfter($this->lifetime);
$this->cachePool->save($cacheItem);

return hash_equals($credential->getOauthSignature(), $this->getSignature($credential, $user));
}

private function getSignature(OauthCredential $credential, UserInterface $user)
{
$oauth = new OAuth($user->getApiKey(), $user->getApiSecret(), $this->signatureMethod);
$oauth->setVersion($credential->getOauthVersion());
$oauth->setTimestamp($credential->getOauthTimestamp());
$oauth->setNonce($credential->getOauthNonce());

return $oauth->generateSignature(
$credential->getMethod(),
$credential->getBaseUrl(),
$credential->createSignatureParameters()
);
}
}

UserProvider


namespace CustomerBundle\Security;

use CustomerBundle\Repository\UserRepository;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class UserProvider implements UserProviderInterface
{
private $userRepository;

public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}

public function loadUserByUsername($apiKey)
{
return $this->userRepository->findOneByApiKey($apiKey);
}

public function refreshUser(UserInterface $user)
{
throw new UnsupportedUserException();
}

public function supportsClass($class)
{
return User::class === $class;
}
}

TokenUser


namespace CustomerBundle\Security;

use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Translation\Exception\NotFoundResourceException;

class TokenUser
{
private $tokenStorage;

public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}

public function get()
{
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof UserInterface) {
throw new NotFoundResourceException('User not found.');
}

return $user;
}
}

services:
customer.security.provider.oauth_secured:
class: CustomerBundle\Security\OauthProvider
arguments:
- '@customer.security.provider.user'
- '@cache.app'
- '%oauth.version%'
- '%oauth.signature_method%'
- '%oauth.lifetime%'
- '%oauth.cache_prefix%'

customer.security.listener.oauth_secured:
class: CustomerBundle\Security\OauthListener
arguments:
- '@security.token_storage'
- '@security.authentication.manager'
- '@logger'

customer.security.provider.user:
class: CustomerBundle\Security\UserProvider
arguments:
- "@customer.repository.user"

customer.security.token_user:
class: CustomerBundle\Security\TokenUser
arguments:
- "@security.token_storage"

CustomerBundle


namespace CustomerBundle;

use CustomerBundle\Security\OauthFactory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class CustomerBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);

$extension = $container->getExtension('security');
$extension->addSecurityListenerFactory(new OauthFactory());
}
}

Database


CREATE TABLE user (
id INTEGER NOT NULL,
api_key VARCHAR (36) NOT NULL,
api_secret VARCHAR (40) NOT NULL,
roles CLOB NOT NULL,
PRIMARY KEY (
id
)
);

SELECT * FROM user;
#|api_key |api_secret |roles
1|api12345-449c-4e49-87ff-82db2da75715|eeb1eab56765f7a09d4865464329ae5837243fbf|a:2:{i:0;s:9:"ROLE_USER";i:1;s:13:"ROLE_API_USER";}
2|admin123-449c-4e49-87ff-82db2da75715|197fcd31b92273fa80dc56e12ceedb9fd974841d|a:3:{i:0;s:9:"ROLE_USER";i:1;s:13:"ROLE_API_USER";i:2;s:15:"ROLE_ADMIN_USER";}

Tests


In the case of authentication failure HTTP/1.1 401 Unauthorized error is returned and example errors below are logged.


[2018-02-17 16:15:16] app.INFO: [Oauth] Invalid signature method. [] []
[2018-02-17 16:15:29] app.INFO: [Oauth] Invalid version. [] []
[2018-02-17 16:16:33] app.INFO: [Oauth] Timestamp is in future. [] []
[2018-02-17 16:16:51] app.INFO: [Oauth] Timestamp is old. [] []
[2018-02-17 16:17:06] app.INFO: [Oauth] Nonce is already used. [] []
[2018-02-17 16:17:51] app.INFO: [Oauth] User not found. [] []
[2018-02-17 16:18:40] app.INFO: [Oauth] Invalid token pattern. [] []

Postman


All you have to do is, provide Consumer Key and Consumer Secret data in "Authorization" section for "OAuth 1" option. Postman does the rest for you. See images below. You can obtain cUrl codes from the "Code" button on the right hand side.



curl -X GET \
http://192.168.99.10:8081/app_test.php/v1/customers/77 \
-H 'authorization: OAuth oauth_consumer_key=\"api12345-449c-4e49-87ff-82db2da75715\",oauth_signature_method=\"HMAC-SHA1\",oauth_timestamp=\"1518899627\",oauth_nonce=\"VgcvCy\",oauth_version=\"1.0\",oauth_signature=\"SHT5ot378TlhWD4SwRhHS7RzdIU%3D\"'


curl -X POST \
'http://192.168.99.10:8081/app_test.php/v1/customers?url_1=1&url_2=2&url_3=' \
-H 'authorization: OAuth oauth_consumer_key=\"admin123-449c-4e49-87ff-82db2da75715\",oauth_signature_method=\"HMAC-SHA1\",oauth_timestamp=\"1518900374\",oauth_nonce=\"ZOWuZB\",oauth_version=\"1.0\",oauth_signature=\"6HgMgwSmEu6Z6m%2B%2BXe6odi6JagU%3D\"' \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'bod_1=1&bod_2=2&bod_3='