Tag Archive: Authorization


I’m currently building a backend API for a site which requires requests to be signed with an access token header. My problem is, the default @Security annotation feature doesn’t fit with our requirements, because we don’t actually have any users on our site! We actually consume our clients SOAP service, which is where all the important data is stored.

Currently, in a typical controller action, I have the following code:

/**
 *@Rest\Put("/auth/change-password")
 */
public function changePasswordAction(Request $request)
{
 $tokenId = $paramFetcher->get('token');
 $tokenSvc = $this->get('Our\SuperbBundle\Service\TokenService');

 // Check the token exists and is valid
 // throws exception if not found or expired
 $tokenSvc->findToken($tokenId); 
 // actual change password logic here
}

We are going to have several controllers with multiple calls, so we don’t want to add this in every single controller action, so we will create our own custom annotation.

<?php

namespace Our\CustomerZoneBundle\Annotation;

/**
 * Class SecureToken
 * @package Our\CustomerZoneBundle\Annotation
 * @Annotation
 */
class SecureToken
{
}

The next thing we do is create an Annotation listener, which I’ll explain below:

<?php

namespace Our\CustomerZoneBundle\EventListener;

use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Util\ClassUtils;
use Our\CustomerZoneBundle\Annotation\SecureToken;
use Our\CustomerZoneBundle\Model\Exception\TokenException;
use Our\CustomerZoneBundle\Service\TokenService;
use ReflectionClass;
use ReflectionObject;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

class SecureTokenAnnotationListener
{
    /** @var AnnotationReader $reader */
    protected $reader;

    /** @var RequestStack $requestStack */
    protected $requestStack;

    /** @var TokenService $tokenService */
    protected $tokenService;

    /**
     * SecureTokenAnnotationListener constructor.
     * @param AnnotationReader $reader
     * @param RequestStack $stack
     * @param TokenService $tokenService
     */
    public function __construct(AnnotationReader $reader, RequestStack $stack, TokenService $tokenService)
    {
        $this->reader = $reader;
        $this->requestStack = $stack;
        $this->tokenService = $tokenService;
    }

    /**
     * @param FilterControllerEvent $event
     */
    public function onKernelController(FilterControllerEvent $event)
    {
        $controller = $event->getController();
        /*
         * $controller passed can be either a class or a Closure.
         * This is not usual in Symfony2 but it may happen.
         * If it is a class, it comes in array format
         *
         */
        if (!is_array($controller)) {
            return;
        }

        /** @var Controller $controllerObject */
        list($controllerObject, $methodName) = $controller;

        $request = $this->requestStack->getCurrentRequest();
        $cookies = $request->cookies;
        $mkzCookie = $cookies->get('mkz');

        // Override the response only if the annotation is used for method or class
        if ($this->hasSecureTokenAnnotation($controllerObject, $methodName)) {
            try {
                if (!$mkzCookie) {
                    throw new TokenException(TokenException::ERROR_NOT_FOUND, 404);
                }
                $this->tokenService->findToken($mkzCookie);
            } catch (TokenException $e) {
                throw new AccessDeniedHttpException($e->getMessage(), $e);
            }
        }
    }

    /**
     * @param Controller $controllerObject
     * @param string $methodName
     * @return bool
     */
    private function hasSecureTokenAnnotation(Controller $controllerObject, string $methodName) : bool
    {
        $tokenAnnotation = SecureToken::class;

        $hasAnnotation = false;

        // Get class annotation
        // Using ClassUtils::getClass in case the controller is an proxy
        $classAnnotation = $this->reader->getClassAnnotation(
            new ReflectionClass(ClassUtils::getClass($controllerObject)), $tokenAnnotation
        );

        if ($classAnnotation) {
            $hasAnnotation = true;
        }

        // Get method annotation
        $controllerReflectionObject = new ReflectionObject($controllerObject);
        $reflectionMethod = $controllerReflectionObject->getMethod($methodName);
        $methodAnnotation = $this->reader->getMethodAnnotation($reflectionMethod, $tokenAnnotation);

        if ($methodAnnotation) {
            $hasAnnotation = true;
        }

        return $hasAnnotation;
    }
}

Our event listener uses the annotation reader class, and takes the request object. When the event triggers, the onKernelController() method is called, where we get the Access Token cookie from the request. Then it checks for the @SecureToken annotation, which can either cover an entire class, or can be set in individual method docblocks.

Next, we hook up the service autowiring in the config services.yml:

our.event_listener.secure_token:
    class: Our\CustomerZoneBundle\EventListener\SecureTokenAnnotationListener
    autowire: true
    tags:
        - { name: kernel.event_listener, event: kernel.controller }

Symfony autowiring is pretty cool. the autowire true key allows Symfony to take care of object instantiation, so we don’t even need to fetch the annotation reader or request for the constructor!

Finally, we add our annotation to our method, and remove our check from inside the method:

/**
 * @Rest\Put("/auth/change-password")
 * @SecureToken
 */
public function changePasswordAction(Request $request)
{
    // actual change password logic here
}

Now using whatever HTTP client you like (POSTman 😉 !) when we add our Access Token cookie header, the check happens automatically! Removing the cookie now gives us a 403 response! Success! Have fun! 😀

Advertisements

It’s something to do with using PHP FPM / Fast CGI, and auth headers being disable for that.

So we add this to the directory entry of the .htaccess:

SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1

You now have your missing header back!