Security - Extending Symfony2 Web Application Framework (2014)

Extending Symfony2 Web Application Framework (2014)

Chapter 4. Security

Security is a very broad topic, and in general, it means restricting access to resources depending on who tries to access them. This chapter will not be going into the theory but will be a hands-on approach on how you can customize the security layer of Symfony to meet your needs.

Security is usually split into two parts:

· Authentication: This identifies who is trying to access our app and is a prerequisite to authorization

· Authorization: It decides if a user has the right to access specific parts of the app/data

In other words, authentication answers the question "Who are you?" (Luke SkyWalker) and authorization decides what you are allowed to do (for example, Use the force: yes; Pilot the Death Star: no).

We'll first go through both the topics in order, and then see a practical application of these techniques to protect an API against CSRF attacks.

Authentication

There are many ways to authenticate a user. The most common pattern nowadays is through the username and password, but we also have the third-party sites' authentication (Facebook login, Twitter login, GitHub, and so on), which sometimes uses OAuth or their custom method. LDAP is also a popular option in the enterprise.

Symfony's documentation already contains everything you need to know about creating a custom authentication. However, it is hard to understand why you are doing things in a particular way when following the official guide. This part guides you through the same process, while detailing the reasons why things are done in such a way, and how each part connects with each other.

Simple OAuth with GitHub

In this part, we'll add authentication through GitHub's API; GitHub implements OAuth. How this works in practice is that your app will contain a link to send users to a GitHub page asking them if they want to allow your app to connect to their GitHub account (only if they haven't yet) and then redirect them to a given URL. From this URL, we need to retrieve information about the user and log them in. We'll make a simple controller do this first and ensure things are working correctly. As we need to communicate to the GitHub servers over HTTP, we included the Guzzle library (http://guzzle.readthedocs.org/en/latest/) that helps deal with HTTP communication.

Tip

If you are unfamiliar with OAuth, you might want to learn about the basics (http://en.wikipedia.org/wiki/OAuth) before diving into this chapter so that you get a better understanding of how the process is happening.

Before you start, you need to create an app on GitHub, which will give you a client_id and a secret_token. Then, we will create our simple controller as follows:

/**

* @Route("/github")

*/

public function ghloginAction(Request $request)

{

$client = new \Guzzle\Http\Client(

'https://github.com/login/oauth/access_token');

$req = $client->post('', null, [

'client_id' => 'your app client_id',

'client_secret' => 'your app secret_token',

'code' => $request->query->get('code')

])->setHeader('Accept', 'application/json');

$res = $req->send()->json();

$token = $res['access_token'];

$client = new \Guzzle\Http\Client(

'https://api.github.com');

$req = $client->get('/user');

$req->getQuery()->set('access_token', $token);

$username = $req->send()->json()['login'];

return new Response($username);

}

Then, if you point the URL to https://github.com/login/oauth/authorize?client_id=<client_id>&redirect_uri=http://your-project.local/github], you will be on GitHub and will be asked if you want to allow this application (your project) to use your GitHub account. After you allow it, you are redirected to http://your-project.local/github.

Note

You only allow the app once. After that, GitHub will automatically redirect you to the right page.

When GitHub redirects you, it adds a code query string to the URL so that it actually looks like http://your-project.local/github?code=<some code>.

With that code, we ask GitHub for an access_token token specific to this user. This token now allows us to browse GitHub's API as if we were that user. We request the special URL https://api.github.com/user, which returns the current user information (username, ID, and so on).

If everything worked correctly, you will see your GitHub username on the screen. Great! Now, we need to hook this process inside Symfony's security layer. Now that we understood the basic principle inside, let's make it work with the actual Symfony authentication mechanisms, starting with Symfony's firewall.

The firewall

Firewalls in Symfony are configured so that they know which parts of the application are free to visit and which require a user to be authenticated (defined by a URL pattern). The firewall only cares about authentication. Whenever a request arrives to a URL, the firewall checks if this URL can be visited by anonymous users (in which case, the request flows through). If the URL requires authenticated users, the request either flows through (the user is already authenticated), or the firewall interrupts it and initiates the authentication process.

To authenticate users, you declare a special URL in Symfony's firewall. This URL does not map to a controller. The firewall catches it, finds which class is listening for it, and asks it to authenticate the user. Our firewall configuration now looks like the following code snippet:

firewalls:

main:

pattern: ^/.*

form_login:

provider: fos_userbundle

csrf_provider: form.csrf_provider

github:

check_path: /github_login

logout: true

anonymous: true

The /github_login part, although not mapped to a controller, is quite important here. We will use it as the redirect_url parameter when we go to log in from GitHub. If you start to have multiple OAuth providers, you can then clearly separate them to implement a login for each of them.

At the same time, we need to declare this route in routing.yml, but again, it does not need to be tied to a controller:

# routing.yml

github_login:

pattern: /github_login

Next, we need to create an authentication listener that will listen on this special URL and tell Symfony about it. Symfony provides an abstract class for AuthenticationListener, which means we won't have to implement all the methods. All we have to do is implement the attemptAuthentication method. For this, we'll reuse the code that is placed in the controller:

namespace Khepin\BookBundle\Security\Github;

use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener;

use Khepin\BookBundle\Security\Github\GithubUserToken;

use Symfony\Component\HttpFoundation\Request;

class AuthenticationListener extends AbstractAuthenticationListener

{

protected function attemptAuthentication(Request $request)

{

$client = new \Guzzle\Http\Client(

'https://github.com/login/oauth/access_token');

$req = $client->post('', null, [

'client_id' => 'xxx',

'client_secret' => 'xxx',

'code' => $request->query->get('code')

])->setHeader('Accept', 'application/json');

$res = $req->send()->json();

$access_token = $res['access_token'];

$client = new \Guzzle\Http\Client('https://api.github.com');

$req = $client->get('/user');

$req->getQuery()

->set('access_token', $access_token);

$email = $req->send()->json()['email'];

$token = new GithubUserToken();

$token->setCredentials($email)

return $this->authenticationManager

->authenticate($token);

}

}

One more class again! The code is exactly what we had before, except that this time, instead of returning the response, we return a token. The token now only holds the user's credentials. In this case, we have retrieved the user's e-mail address and set it in the token.

By using GitHub or any other third party, we no longer need a password. We trust that when GitHub says user@example.com is trying to connect, it has already verified this. We can then create a simplified Token class that only contains the e-mail and no password.

The token itself is fairly simple:

namespace Khepin\BookBundle\Security\Github;

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

class GithubUserToken extends AbstractToken

{

private $credentials;

public function setCredentials($email)

{

$this->credentials = $email;

}

public function getCredentials()

{

return $this->credentials;

}

}

Note

We use the user's e-mail because if we find the same user e-mail from GitHub and Twitter logins, we know it is actually the same user. But finding the same username doesn't mean much; it could be two different people who registered the same name for different services.

The security factory

We already wrote two new classes and a bit of configuration, and yet, if you try to load your application right now, all you will see is an error stating that "GitHub" is not a recognized option for the firewall. So we need to keep working on this for a bit longer before we can see anything. That's why we tried things within a controller first so that we can immediately see what worked and what didn't.

So far, we have defined the following options:

· The token

· The authentication listener

Now, we need to tell the firewall how to make any use of these. The class responsible for tying these together is SecurityFactory.

Let's take a look at how things work for the security component. In the following diagram, we can see that Factory brings together the AuthenticationListener and UserProvider classes and makes the firewall aware of them:

The security factory

In the following diagram, we see that any incoming request is first stopped at the Firewall level. The firewall finds a suitable authentication listener for this request, which creates a non-authenticated token with all the relevant information in order to authenticate the user later. This token is then passed on to the User Provider block, which attempts to find a user based on the given credentials.

The security factory

To define our security factory, we extend it from the abstract security factory, thus avoiding the burden of reimplementing everything. This is shown in the following code:

namespace Khepin\BookBundle\Security\Github\SecurityFactory;

use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory;

use Symfony\Component\DependencyInjection\ContainerBuilder;

use Symfony\Component\DependencyInjection\DefinitionDecorator;

use Symfony\Component\DependencyInjection\Reference;

class SecuirtyFactory extends AbstractFactory

{

public function createAuthProvider(

ContainerBuilder $container, $id, $config,$userProviderId)

{

$providerId ='khepin.github.authentication_provider.'.$id;

$definition = $container->setDefinition(

$providerId, new DefinitionDecorator(

'khepin.github.authentication_provider')

);

if (isset($config['provider']))

{

$definition->addArgument(new Reference($userProviderId));

}

return $providerId;

}

public function getPosition()

{

return 'pre_auth';

}

public function getKey()

{

return 'github';

}

protected function getListenerId()

{

return 'khepin.github.authentication_listener';

}

}

The getKey method returns the name under which you will be able to use the security factory in the firewall. The createAuthProvider part receives the builder for the dependency injection container and can add and modify service definitions. Here, a new authentication provider is created, and we pass the user_provider parameter as an argument to its constructor.

The preceding class is then passed onto your Bundle class, the one that Symfony generates at the root of each bundle, to be added to the configuration, which is shown in the following code snippet. We will see more about what it means to add configuration directly through the Bundle class in Chapter 6, Sharing Your Extensions.

namespace Khepin\BookBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

use Khepin\BookBundle\Security\Github\SecurityFactory;

class KhepinBookBundle extends Bundle

{

public function build(ContainerBuilder $container)

{

parent::build($container);

$extension = $container->getExtension('security');

$extension->addSecurityListenerFactory(

new SecurityFactory()

);

}

}

This part, at least, is pretty straightforward to understand.

There is one last class that must be implemented before we can finish our configuration and use our login, the AuthenticationProvider class, which is given in the following code snippet:

namespace Khepin\BookBundle\Security\Github;

use Symfony\Component\Security\Core\Authentication

\Provider\AuthenticationProviderInterface;

use Symfony\Component\Security\Core\Authentication

\Token\TokenInterface;

use Khepin\BookBundle\Security\Github\GithubUserToken;

class AuthenticationProvider implementsAuthenticationProviderInterface

{

private $user_provider;

public function __construct($user_provider)

{

$this->user_provider = $user_provider;

}

public function supports(TokenInterface $token)

{

return $token instanceof GithubUserToken;

}

public function authenticate(TokenInterface $token)

{

$email = $token->getCredentials();

$user = $this->user_provider->loadOrCreate($username);

// Log the user in

$new_token = new GithubUserToken($user->getRoles());

$new_token->setUser($user);

$new_token->setAuthenticated(true);

return $new_token;

}

}

It receives a user provider that is used to either load or create the user. This is because a login through GitHub or any third-party site can be as much a login as it can be a registration. So, if the user is not found, it must be created. Letting Symfony know that the user is now authenticated means two things on the token, which are as follows:

· The Token::isAuthenticated line is true

· The token contains some roles defining what the user is or isn't allowed to do within the application

The services configuration is as follows:

khepin.github.authentication_listener:

class: Khepin\BookBundle\Security\Github\AuthenticationListener

parent: security.authentication.listener.abstract

abstract: true

public: false

khepin.github.authentication_provider:

class: Khepin\BookBundle\Security\Github\AuthenticationProvider

public: false

There are two interesting aspects we didn't see before. They are as follows:

· parent: This service definition inherits from another service definition, so anything that is not specified directly here will come from the parent.

· abstract: This service itself cannot be implemented. The security component is responsible for taking this abstract service definition and creating actual concrete services from it.

Our security file now also looks like the following code snippet:

providers:

fos_userbundle:

id: fos_user.user_provider.username

firewalls:

main:

pattern: ^/

form_login:

provider: fos_userbundle

csrf_provider: form.csrf_provider

github:

provider: fos_userbundle

check_path: /github_login

logout: true

anonymous: true

As FOSUserBundle is a very popular way of dealing with users in Symfony, we are reusing their user provider. This would work fine if your users were already registered with the same username they have on GitHub and if we were using the username to identify users. However, we need to use the e-mails to ensure consistent and secure logins through multiple third-party providers.

Note

The UserProvider class is one of the components of Symfony's security component, so you don't need the one provided by FOSUserBundle. It is used in this example for convenience and shows how you can integrate your new authentication with it.

We can then create our own user provider using the following code snippet:

class UserProvider implements UserProviderInterface

{

public function __construct($user_manager)

{

$this->user_manager = $user_manager;

}

public function supportsClass($class)

{

return $this->user_manager->supportsClass($class);

}

public function loadUserByUsername($email)

{

$user = $this->user_manager->findUserByEmail($email);

if(empty($user)){

$user = $this->user_manager->createUser();

$user->setEnabled(true);

$user->setPassword('');

$user->setEmail($email);

$user->setUsername($email);

}

$this->user_manager->updateUser($user);

return $user;

}

public function loadOrCreateUser($email)

{

return $this->loadUserByUsername($email);

}

public function refreshUser(UserInterface $user)

{

if (!$this->supportsClass(get_class($user)) ||!$user->getEmail())

{

throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.',get_class($user)));

}

return $this->loadUserByUsername($user->getEmail());

}

}

The preceding UserProvider class is then defined as a service and is set up as a provider in the security configuration. This is done using the following lines of code:

# config.yml

khepin.github.user_provider:

class: Khepin\BookBundle\Security\Github\UserProvider

arguments: [@fos_user.user_manager]

# security.yml

providers:

fos_userbundle:

id: fos_user.user_provider.username

github_provider:

id: khepin.github.user_provider

firewalls:

main:

pattern: ^/

form_login:

provider: fos_userbundle

csrf_provider: form.csrf_provider

github:

provider: github_provider

check_path: /github_login

logout: true

anonymous: true

Authentication is not the easiest part to understand in Symfony, but it is structured in a way that allows for many customizations. After this part, you should have a better understanding of how things are working and be able to create your own authentication method if you need it.

Authorization

It is a common thing in any application to restrict access to different parts of an application depending on who the user is. In Symfony, this can be done in many places, such as through annotations on the controller (or some equivalent configuration), via Access Control Lists (ACL), and through voters.

Controller annotations are role-based, which is fine for a lot of cases, but won't be adapted when we want to exercise fine-grained controls. At that point, you either have to create many more roles to express all of the permissions of a user or start using ACLs. ACLs provide much more fine-grained control, but they are very inexpressive. A user's rights on a given object or page are stored in the database as just that; these rights are called granular permissions. These permissions have to be granted and revoked one by one in your code; so, if you decide one day to completely change the logic of how some users are allowed to do something and others are not, you will have to go over all of these single permissions again and update them.

Voters in Symfony allow you to express your permissions as business logic rules. Some famous websites (think stackoverflow, for example) rely on this type of logic a lot. A user with a reputation of less than 100 cannot edit a question, a user with a reputation of 1000 or more can close a question, and so on. Luckily, in Symfony, it doesn't really matter how you express your authorization logic; the way to check a user's rights to perform an action or access a resource is always done in the same way through SecurityContext; for example, consider the following code lines:

$context->isGranted('ROLE_ADMIN');

$context->isGranted('EDIT', $object);

Our project so far was to create meetups that a user can join. As we're a small website for now, we don't plan to grow international operations yet. So, we'll only allow users from a given country to create new meetups. Anyone can create a meetup as long as they are from the right country.

Note

In real life, it is very difficult to know which country a user is actually from. The IP address checks can be circumvented by using VPN services, and everything else coming in the HTTP request to your server can be set up by anyone with basic knowledge of HTTP. You shouldn't base any important security decisions on that information.

Voters

Let's create a simple Voter class that will let a user create a meetup depending on their country. The Voter class implements the three methods of VoterInterface, which are as follows:

· supportsAttribute: This method will return true if the attribute is MEETUP_CREATE, and false otherwise. This means our voter is only allowed to vote for this. It will not be called when the security component is checking for something else such as ROLE_ADMIN, for example. It's important to set it correctly to avoid conflicts between different voters.

· supportsClass: This method will return true all the time as we won't be passed an actual object to check if the user has rights on this specific object.

· vote: This method will return the result of our vote.

Note

As you will see, it is your responsibility to call the supports* methods; the AccessDecisionManager method will not do it for you.

namespace Khepin\BookBundle\Security\Voters;

use Symfony\Component\HttpFoundation\RequestStack;

use Symfony\Component\Security\Core\Authorization

\Voter\VoterInterface;

use Symfony\Component\Security\Core\Authentication

\Token\TokenInterface;

class CountryVoter implements VoterInterface

{

protected $country_code;

public function __construct($service_container)

{

$this->country_code = $service_container

->get('user_locator')->getCountryCode();

}

public function supportsAttribute($attribute)

{

return $attribute === 'MEETUP_CREATE';

}

public function supportsClass($class)

{

return true;

}

public function vote(TokenInterface $token, $object, array $attributes)

{

if ( !$this->supportsClass(get_class($object)) ||

!$this->supportsAttribute($attributes[0])

) {

return VoterInterface::ACCESS_ABSTAIN;

}

if ($this->country_code === 'CN') {

return VoterInterface::ACCESS_GRANTED;

}

return VoterInterface::ACCESS_DENIED;

}

}

The vote method of a voter can return one of the following three results:

· ACCESS_GRANTED: The user is allowed access

· ACCESS_DENIED: The user is denied access

· ACCESS_ABSTAIN: This voter does not take part in the current vote

We define this voter as a service and tag it as a security voter using the following lines of code:

security.access.country_voter:

class: Khepin\BookBundle\Security\Voters\CountryVoter

public: false

arguments: [@service_container]

tags:

- { name: security.voter }

If you haven't done it before, it is now time to use the AccessDecisionManager method in the security configuration using the following code lines:

security:

access_decision_manager:

strategy: unanimous

As shown in the preceding code lines, AccessDecisionManager takes a few possible arguments, which are described as follows:

· strategy: This can have one of the following values:

· unanimous: If any voter votes ACCESS_DENIED, then access is denied

· affirmative: If any voter votes ACCESS_GRANTED, then access is granted

· consensus: This counts the number of ACCESS_DENIED and ACCESS_GRANTED permissions and decides based on the majority of votes

· allow_if_all_abstain: This checks whether or not to grant access when all voters returned ACCESS_ABSTAIN

· allow_if_equal_granted_denied: In the consensus strategy, when the number of ACCESS_GRANTED and ACCESS_DENIED is equal, this checks whether access should be granted or not

The last step to make this work is to configure the controller to deny access to anyone who isn't allowed to create a meetup:

/**

* @Security("is_granted('MEETUP_CREATE')")

* ... other annotations ...

*/

public function newAction()

{

// ...

}

The logic we implemented here would be painful to manage through roles or ACL. With these, whenever you want to add a new country, you would have to find all users in that country and update their roles or ACL. You would also need to update all of the users' entries in the ACL whenever they change country and so forth.

Voters can also be used for more specific object decisions. If our meetups had to be reviewed and then published or approved by someone else, we would need specific permission checks for this. However, maybe a user that has already successfully organized at least five meetups can now be trusted to publish them on their own. These would work exactly the same way as what we just saw as they are rules independent from the meetup itself.

A different case would happen if we decide that a user can only update a meetup if the following conditions are met:

· They were the ones who created the meetup.

· The meetup has not been joined by anyone yet. This would avoid bad surprises for users who joined a meetup.

First, let's see how the AccessDecisionManager strategies work if we modify our edit controller to include the following code snippet:

if (!$this->get('security.context')

->isGranted('EDIT', $entity)) {

throw new UnauthorizedHttpException(

'No edit allowed at this time'

);

}

Trying to access the edit page, we get an unauthorized response. This happened because all our voters abstained from voting and we didn't set allow_if_all_abstain to true. Try switching it to see the effect, then set it back to false before we continue.

Since the voter has already passed the security token while voting, we don't need to inject it while defining the service; hence, our service definition is extremely simple:

security.access.meetup_voter:

class: Khepin\BookBundle\Security\Voters\MeetupVoter

public: false

tags:

- { name: security.voter }

The voter class now becomes as follows:

namespace Khepin\BookBundle\Security\Voters;

use Symfony\Component\HttpFoundation\RequestStack;

use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

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

class MeetupVoter implements VoterInterface

{

public function supportsAttribute($attribute)

{

return $attribute === 'EDIT';

}

public function supportsClass($class)

{

return $class === 'Khepin\BookBundle\Entity\Event';

}

public function vote(TokenInterface $token, $object,array $attributes)

{

if (!$this->supportsClass(get_class($object)) ||!$this->supportsAttribute($attributes[0]))

{

return VoterInterface::ACCESS_ABSTAIN;

}

if (

$this->meetupHasNoAttendees($object) &&$this->isMeetupCreator($token->getUser(), $object))

{

return VoterInterface::ACCESS_GRANTED;

}

return VoterInterface::ACCESS_DENIED;

}

protected function meetupHasNoAttendees($meetup)

{

return $meetup->getAttendees()->count() === 0;

}

protected function isMeetupCreator($user, $meetup)

{

return $user->getUserId() === $meetup->getUserId();

}

}

Any user is now allowed to edit a meetup if and only if they are the organizer of that meetup and the meetup does not have any attendees yet. These complex decision rules would be impossible to express through roles. They could be expressed with ACLs and with a lot of care. They could be repeated over different controllers too. With voters, you have a simple way to use access rules that are very expressive and simple to use.

We saw that to secure our controller action, all we had to do was to add an @Security annotation. Annotations are a very common way of configuring things in Symfony, and we have already encountered them in the book (defining controllers in Chapter 1, Services and Listeners), but never written our own. The @Security annotation is also interesting because it does more than just provide some configuration information about a method or a class; it modifies the workflow of the application, adding a security check before the method is executed.

Annotations

Let's take advantage of this possibility in our app. An event organizer should be able to contact the event attendees and view their phone numbers in case of last minute changes to the event. Therefore, we should only allow users that have registered their phone number in their profile to join an event.

Our action to join an event should be decorated with an annotation as follows:

/**

* @Route("/events/{event_id}/join")

* @Template()

* @ValidateUser("join_event")

*/

public function joinAction($event_id) {

// ...

}

Here, join_event is the name of the validation group, which is defined in the user class as follows:

/**

* @ORM\Column(type="string", length=255, name="phone")

* @Assert\NotBlank(groups={"join_event"})

*/

protected $phone;

Defining an annotation

Annotations are defined through annotation classes. These classes don't need to inherit or implement any specific interface, but they need to be annotated with @Annotation.

An annotation will receive an array as a constructor parameter. This array contains all the information that was passed to the annotation. Consider that your annotation is as follows:

/**

* @Log("custom_logger", level="debug")

*/

Then, the array you would receive in the constructor would be:

[ 'value' => 'custom_logger', 'level' => 'debug']

Whenever you need to read an annotation, you need an annotation reader. Of course, this service is readily available for you in Symfony, and all that you have to do in a service where you need to read annotations is to inject that annotation reader.

Let's define our annotation class as follows:

namespace Khepin\BookBundle\Security\Annotation;

/**

* @Annotation

*/

class ValidateUser

{

private $validation_group;

public function __construct(array $parameters)

{

$this->validation_group = $parameters['value'];

}

public function getValidationGroup()

{

return $this->validation_group;

}

}

The annotation is a simple value object containing the information that was passed to it, nothing more.

Let's try to read the annotation first to better understand how they work with regards to the reader by directly using it inside of our controller:

/**

* @Route("/events/{event_id}/join")

* @Template()

* @ValidateUser("join_event")

*/

public function joinAction($event_id)

{

$reader = $this->get('annotation_reader');

$method = new \ReflectionMethod(

get_class($this), 'joinAction');

$annotation_name = 'Khepin\BookBundle\Security\Annotation\ValidateUser';

$annotation = $reader->getMethodAnnotation(

$method, $annotation_name);

// ... Your normal code

}

We see that through our reader service, and by knowing only the name of the class and the method, we can read the annotation and get back an instance of our annotation class.

Note

Here, we create \ReflectionMethod directly because we already know the exact method we want to read an annotation for. You would probably, in most interesting cases, have to create a class named \ReflectionClass, and then loop over all defined methods to see which ones have the annotation you are looking for.

In the same way, you can read annotations for methods, properties, and the class itself, using the following code:

// Reading a class annotation

$reader->getClassAnnotation(

new \ReflectionClass('MyClass'),

'\My\Annotation'

);

// Reading a property annotation

$reader->getPropertyAnnotation(

new \ReflectionProperty(

'UserClass',

'phone_number'

),

'\My\Annotation'

);

The preceding code works well for reading a single annotation if you know which annotation you are looking for. For these cases, it is important to always use the fully qualified class name, including the namespace; otherwise, Doctrine's annotation reader will not be able to match the annotation class to the one you are trying to load.

For cases when you need to load all annotations and see which ones are defined, you can use get*Annotations() instead of the singular method. In this case, you would receive an array of all of the available annotations:

$annotation = $reader->getMethodAnnotations(

new \ReflectionMethod(get_class($this), 'joinAction'));

=>

{

[0]=> object(Sensio\Bundle\FrameworkExtraBundle\Configuration\Route),

[1]=> object(Sensio\Bundle\FrameworkExtraBundle\Configuration\Template),

[2]=> object(Khepin\BookBundle\Security\Annotation\ValidateUser)

}

Note

When adding annotations to entities or documents managed through Doctrine, you should not rely on get_class. Instead, use \Doctrine\Common\Util\ClassUtils::getClass because Doctrine will generate proxy classes for your entities, and in some cases, you will be trying to read the annotations on the proxy class instead of the class you are actually interested in. ClassUtils avoids this by returning the real class of an object instead of the proxy.

When a bundle is using annotations, it is creating a service in which the annotation reader is injected and then reads the annotation whenever needed. Even SensioFrameworkExtraBundle, which brings us the @Route and @Template annotations that we use on ourjoinAction method, does it the same way. By listening to the kernel.controller event before the controller is called, a service can read the required annotations and modify the behavior as needed.

Note

The annotation reader in Symfony will cache your annotations after they are read. Because PHP doesn't have support for annotations, they are created by adding comments. Parsing these comments on each request would be extremely slow. Make sure you use Symfony's annotation_reader service, and don't instantiate your own as it is already configured to speed things up and cache all read annotations.

Securing controllers with custom annotations

We now have all the building blocks in order to secure our actions, and we'll define a listener to the kernel.controller event:

security.access.valid_user:

class: Khepin\BookBundle\Security\ValidUserListener

arguments: [@annotation_reader, @router, @session,

@security.context, @validator]

tags:

- { name: kernel.event_listener,

event: kernel.controller,

method: onKernelController}

Our listener takes quite a few arguments. They are as follows:

· annotation_reader: This will allow us to read the arguments on each controller

· router: This will let us redirect the user to their profile page if their profile is not complete

· session: This is to add a "flash" message telling the user why they were redirected and what they have to do

· security.context: This is to retrieve the user

· validator: This is to validate the user

The controller event allows us to retrieve the controller in the form of an array:

{

[0] => object('\My\Controller'),

[1] => 'myAction'

}

This is everything we need in order to read our annotation. Now, change the controller as follows:

class ValidUserListener

{

private $reader;

private $router;

private $session;

private $sc;

private $validator;

private $annotation_name = 'Khepin\BookBundle\Security\Annotation\ValidateUser';

public function __construct(Reader $reader, Router $router,Session $session, SecurityContext $sc,Validator $validator)

{

$this->reader = $reader;

$this->router = $router;

$this->session = $session;

$this->sc = $sc;

$this->validator = $validator;

}

public function onKernelController($event)

{

// Get class and method name to read the annotation

$class_name = get_class($event->getController()[0]);

$method_name = $event->getController()[1];

$method = new \ReflectionMethod(

$class_name, $method_name);

// Read the annotation

$annotation = $this->reader->getMethodAnnotation($method,$this->annotation_name);

// If our controller doesn't have a "ValidateUser"

// annotation, we don't do anything

if (!is_null($annotation)) {

// Retrieve the validation group from the

// annotation, and try to validate the user

$validation_group = $annotation->getValidationGroup();

$user = $this->sc->getToken()->getUser();

$errors = $this->validator->validate($user,

$validation_group);

if (count($errors)) {

// If the user is not valid, change the

// controller to redirect the user

$event->setController(function()

{

$this->session->getFlashBag()->add(

'warning', 'You must fill in yourphone number before joining ameetup.');

$url = $this->router->generate('fos_user_profile_edit');

return new RedirectResponse($url);

});

}

}

}

}

When we change the controller, we define an anonymous function instead of the array. All that is required is to pass a callable, so you could also pass in a static method, another callable array, and so on.

If you have a user defined that does not have a phone number, whenever they try to view the page to join a meetup, they are redirected to their profile page with a message saying they should update their phone number. If the phone number is present, then they see the page as requested.

Note

Here, this is secure because viewing the form to join a meetup and submitting the form are both in the same action. If you were to separate them, then both calls would need to be secure as well.

Securing an API – an example

It is becoming a common practice to only have an API on your web server and not generate the page's HTML on the server but through JavaScript in a user's browser.

However, it is also common for developers to still use standard sessions and logins when the API is only there to serve their own website at first. This can lead to issues regarding security. Whenever you create a form to be displayed in Symfony via Twig, it contains a CSRF token. This token is here to help us ensure that not only is the request coming from this user's browser (cookies do that) but also from your actual webpage and not a malicious tab in the user's browser.

With an API, your forms are going to be generated entirely in the frontend. So, they cannot include a CSRF token. Furthermore, whenever an attacker submits a request to our server through a user's browser, all the cookies will be sent together, allowing the attacker to control the user's account. However, because of the same origin policy in browsers, an attacker's script cannot see what the cookies are for our website. So a technique to still defend ourselves is to double-submit the cookies, once normally, which we don't control, and once through a custom header.

An attacker will not be able to reproduce this, and for us, through the JavaScript that we are using, it is very easy to include this duplicated header on every request.

Since we are only checking for permissions and access, we create the simplest possible controller:

/**

* @Route("/api/status")

*/

public function apiAction()

{

return new Response('The API works great!');

}

Now, for any request to a URL starting with /api/, we want to make sure that our cookie exists twice. In the following code snippet, we will use events in a way similar to what we did with annotations earlier, but this time, we'll use the kernel.request event as it happens earlier. Also, in this case, we don't need information about the controller.

security.access.api:

class: Khepin\BookBundle\Security\ApiCustomCookieListener

tags:

- { name: kernel.event_listener,

event: kernel.request,

method: onKernelRequest }

This listener will receive the request through the event and only compare two headers of this request, so it will not require any argument. The listener is also very easy to implement:

namespace Khepin\BookBundle\Security;

use Symfony\Component\HttpFoundation\Response;

class ApiCustomCookieListener

{

public function onKernelRequest($event)

{

// We only secure urls in our API

if (strpos(

$event->getRequest()->getPathInfo(),

'/api/'

) !== 0

) {

return;

}

$cookie = $event->getRequest()->headers

->get('cookie');

$double = $event->getRequest()->headers

->get('X-Doubled-Cookie');

if ($cookie !== $double) {

$event->setResponse(new Response('', 400));

}

}

}

With just these few lines, we have enabled the CSRF protection on an API with a cost that is a lot less than that of using CSRF tokens as compared to forms, as these need to be random and encrypted values.

Summary

Security is a huge topic and a source of endless debate. This chapter showed you how to craft authentication and authorization mechanisms in Symfony, but it's important to understand that security does not stop there. Depending on the level of security required by your application, you should always do your research on how to best make it safe for you and your users.

Although creating your own authentication method is a bit complex in Symfony, it's done in a way that is highly modular and customizable. For that reason, most authentication schemes you might encounter will already have an existing third-party bundle that you could use, relieving you of the implementation effort.

We also saw how roles, ACLs, and voters can be used independently or together to give various authorizations to different users. Roles, combined with voters, allow for a powerful and expressive way to control access.

In the next chapter, we will take a break from Symfony to talk about Doctrine. Doctrine is not the only persistence layer that can be used, but it is Symfony's default choice and offers a lot in terms of extensibility.