Development Group
BLOG

Software Development

Back to Blog

Modern web app development on PHP. Security

July 2, 2018

This is the last article from the series of posts dedicated to shed a light on modern web application development using PHP and Symfony web framework.

In this article we’ll try to restrict the access to our API and in order to do that we have to regard additional capabilities, provided by the Symfony framework.

Goal

Our goal will be the implementation of the mechanism dedicated to restrict the access to the specific method of the controller. That will be the simplest example of the authorization mechanism.

Let’s try to recall the material from the previous article. As such we already have the controller, that returns the anticipated response to the HTTP request.

Once we skip the business logic - as it isn’t of the importance for our example - the controller looks like:

/**
* @Route("", name="api-users")
* @Method({"GET"})
* @param Request $request
* @return JsonResponse
*/
public function listAction(Request $request)
{   
$array = [];
return new JsonResponse($array);
}

Above the method there is the comments block, all the comments have to be familiar to us. We’ve already discussed that comments of such kind are annotations and because of that they define the additional logic or restrictions - enhance the contract of the controller method.

Let’s introduce our own annotation @GrantAll(). The idea of it is to restrict the invocation of the controller method only if that controller method had the mentioned annotation.

Thus, let’s add the annotation to the comments block:

/**
* @Route("", name="api-users")
* @GrantAll()
* @Method({"GET"})
* @param Request $request
* @return JsonResponse
*/

As for now, the annotation that we have specified is simply the comment and it doesn’t restrict or allows anything yet.

But let’s proceed and make that annotation work. So, we would like to allow controller’s method invocation only if there is the @GrantAll() annotation declared.

How do we go about that?

Symfony has the EventDispatcher component that implements the Mediator pattern and is the central point, where the event can be published by one component and leads to the receiving of that event by other component.

Evenmore, the dispatcher allows for other components to handle the standard Symfony events - and thus to register the specific class / method to listen and handle those.

As the result, we create the class / method with the custom logic and ask dispatcher to call it once the event is emitted.

The comprehensive list of events, emitted by Symfony, can be found in documentation. However, we are more interesting in such event as kernel.controller.  

This event occurs before the controller invocation, any controller, always. Thus, we have to create the Handler for kernel.controller event, then that handler has to check, whether the method of our controller has the corresponding annotation indeed and based on that to take a decision, whether to allow or deny the access.

At first, let’s create the definition of out annotation:

<?php
namespace AppBundle\Annotation;
/**
* @Annotation
*/classGrantAll
{
}

Nothing complex here, the annotation @Annotation tells that current class is annotation to other. It’s worth to say, that as soon as there is the parameter that is to be defined in annotation, then the declaration of the class above should be a bit more complicated.

Then we have to create the handler, dedicated to check, whether the target method has the corresponding annotation:

./src/AppBundle/EventListener/AuthListener.php

<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpFoundation\JsonResponse;
use AppBundle\Annotation\GrantAll as AnnotationGrantAll;

class AuthListener
{
   private $annotationReader;
   public function __construct($annotationReader)
   {
       $this->annotationReader = $annotationReader;
   }

   /**  
   * @param FilterControllerEvent $event
   * @throws \Exception
   */    

public function onKernelController(FilterControllerEvent $event)

   {
       $controllerInfo = $event->getController();
       $object = new ReflectionObject($controllerInfo[0]);
       $method = $object->getMethod($controllerInfo[1]);
       $annotations = $this->annotationReader->getMethodAnnotations($method);

       foreach($annotations as $annotation){
           if ($annotation instanceof AnnotationGrantAll) {
               return;
           }
       }

       $event->setController(function ()
       {
           return new JsonResponse('Forbidden', 403);
       });   
 }
}

We’ve created the AuthListener handler with onKernelController method. Despite the very simple implementation, the class already implements the authorization functionality by listening the required event and reacting to it.

Let’s take a look at the method logic and later we’ll see, how that method becomes to be a handler of the specific event.

So, what does here happens?

We can see that our method has the reference to the controller through the event parameter, passed to it.

Using reflection we get the access to the method annotations and check then, whether the required annotation is declared at method signature.

If the annotation is provided the handler job is done and control is returned back, thus controller’s method is called. If there is no such annotation, then we return the HTTP 403 as the result of method call.

Let’s see, how it looks like in browser:

That simple!

And now let’s understand, what’s the way our method becomes a handler of the required event. We declare that there is a demand for our method to be called before the call of every method of every controller.

We should be already familiar with /app/config folder and the application config files inside of it. For our purposes we are interesting in services block in services.yml config file.

Let’s add couple lines to that file:

app.listener.authorization:
  class: AppBundle\EventListener\AuthListener
  arguments:
      - "@annotation_reader"
  tags:       - {name: kernel.event_listener, event: kernel.controller, method: onKernelController, priority: 1 }

The very first line is the arbitrary identifier of our handler inside the config file.

The next line describes the full path to our handler, that is required to listen to the events.

arguments is where we specify, what object is going to be passed as the parameter to the class constructor. @annotation_reader - is the identifier, that is used by Symfony to find the annotation reader service. That is the service provided by ORM Doctrine and it is used to work with all the annotations within the application scope.

And the last one - tags. Here we declare, which events to listen to, what us the priority and the actual method name, that is going to be called.

These few lines will lead the method onKernelController to be executed before every controller method.

The example above can be extended drastically. For example, the annotation can be based on a parameter and that parameter may have the list of the roles / permissions (business related), which define, what permissions user is required to have to call the specific controller method.

And in such a case it’s a proper implementation, when there is a call to DB performed to check, whether the user has the required permissions or not.

Summary

Today we have seen the capability to create and use custom annotations in order to implement the authorization. What’s more important, we’ve seen that the custom annotation isn’t something complex to implement.

The applicability of annotations is huge and isn’t limited with the authorization only, but is rather limited with our imagination.

We hope that the series of our articles has shed a light on todays application development using PHP and will help you in your decision making process.