close

Symfony JSON Web Tokens Authentication with Guard

go

With the popularity and growth of single-page applications and mobile applications, developers nowadays create many more APIs. The most basic (and one of the most important problems to handle in this case) is authentication. The traditional way of doing this is through session-based authentication. The client authenticates with its credentials and the server return session ID can then be stored in a cookie. Then, on every request, the user is authenticated by this cookie. However, there are some downsides to this method in the case of APIs:

  • CORS(Cross-origin resource sharing): cookies and CORS don’t work well across different domains/devices. We can run into problems with forbidden requests.
  • Scalability: the session needs to be stored somewhere on the server. If we have a distributed system, it will limit our ability to scale.
  • Depending on a web framework: when we use server-based authentication and we use multiple frameworks/CMS, it is hard to share session data between them.

I would like to present a different approach to this problem; authentication using JWT.

What are JSON Web Tokens?

I won’t talk about Web Tokens itself because I think the idea is greatly explained on the official Web Tokens website - I encourage you to read about them here.

Symfony and JWT

In order to use JWT in a Symfony project we are going to use lexik/jwt-authentication-bundle. First install it using composer:

$ composer require "lexik/jwt-authentication-bundle"

Then register the bundle:

// source app/AppKernel.php
public function registerBundles()
{
    return array(
        // ...
        new Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle(),
    );
}

In the next step, we need to create SSH keys that will be used to generate tokens. In order to perform this step you need to have OpenSSL installed. We place keys inside app/var/jwt but you can place them wherever you want (just remember to keep them secure and exclude them from version control).

$ mkdir -p app/var/jwt
$ openssl genrsa -out app/var/jwt/private.pem -aes256 4096
$ openssl rsa -pubout -in app/var/jwt/private.pem -out app/var/jwt/public.pem

Configure parameters.yml

// source app/config/parameters.yml

jwt_private_key_path: '%kernel.root_dir%/var/jwt/private.pem'   # ssh private key path
jwt_public_key_path:  '%kernel.root_dir%/var/jwt/public.pem'    # ssh public key path
jwt_key_pass_phrase:  'test'                                      # ssh key pass phrase if present
jwt_token_ttl:        86400

Configure config.yml

// source app/config/config.yml

lexik_jwt_authentication:
    private_key_path: %jwt_private_key_path%
    public_key_path:  %jwt_public_key_path%
    pass_phrase:      %jwt_key_pass_phrase%
    token_ttl:        %jwt_token_ttl%

That's basically all that needs to be configured in order to make tokens work. In the next step we are going to use Guard to create a custom authentication system.

Custom authentication system with Guard

In this example I am using Symfony 3 but Guard has been available since Symfony version 2.8. Guard is a really great Symfony feature, it makes creating custom authentication systems very easy. I am assuming here that you already know the basics of Symfony and have some way of handling users.
First, create the endpoint that will return tokens after entering valid credentials:

/**
 * @Route(path="api/token-authentication", name="token_authentication")
 */
public function tokenAuthentication(Request $request)
{
    $username = $request->request->get('username');
    $password = $request->request->get('password');

    $user = $this->getDoctrine()->getRepository('AppBundle:User')
        ->findOneBy(['username' => $username]);

    if(!$user) {
        throw $this->createNotFoundException();
    }

    // password check
    if(!$this->get('security.password_encoder')->isPasswordValid($user, $password)) {
        throw $this->createAccessDeniedException();
    }

    // Use LexikJWTAuthenticationBundle to create JWT token that hold only information about user name
    $token = $this->get('lexik_jwt_authentication.encoder')
        ->encode(['username' => $user->getUsername()]);

    // Return genereted tocken
    return new JsonResponse(['token' => $token]);
}

Then create test endpoint that needs to be secured:

/**
 * @Route(path="/secure-resource", name="secure_resource")
 */
public function secureResource(){
    $data = [
        test => 'test',
        test2 => 'test2'
    ];

    return new JsonResponse($data);
}

Now, let’s see if token authentication endpoint works correctly. I am using Postman extension for Chrome to create test requests:

screen_shot_2016-04-04_at_15.27.35.png

When I enter the correct username/password I get 'valid token', which we will use later.
Now, create a new directory that will hold our custom Guard authentication file and call it 'Security'. Then, create a new class inside that directory called JwtAuthenticator.php. That class needs to extend AbstractGuardAuthenticator and implement the following methods:

<?php

namespace AppBundle\Security;

use Doctrine\ORM\EntityManager;
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoder;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\AuthorizationHeaderTokenExtractor;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;

class JwtAuthenticator extends AbstractGuardAuthenticator
{
    private $em;
    private $jwtEncoder;

    public function __construct(EntityManager $em, JWTEncoder $jwtEncoder)
    {
        $this->em = $em;
        $this->jwtEncoder = $jwtEncoder;
    }

    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new JsonResponse('Auth header required', 401);
    }

    public function getCredentials(Request $request)
    {

        if(!$request->headers->has('Authorization')) {
            return;
        }

        $extractor = new AuthorizationHeaderTokenExtractor(
            'Bearer',
            'Authorization'
        );

        $token = $extractor->extract($request);

        if(!$token) {
            return;
        }

        return $token;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $data = $this->jwtEncoder->decode($credentials);

        if(!$data){
            return;
        }

        $username = $data['username'];

        $user = $this->em->getRepository('AppBundle:User')
            ->findOneBy(['username' => $username]);

        if(!$user){
            return;
        }

        return $user;
    }


    public function checkCredentials($credentials, UserInterface $user)
    {
        return true;
    }
    
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        return new JsonResponse([
            'message' => $exception->getMessage()
        ], 401);
    }
    
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        return;
    }
    
    public function supportsRememberMe()
    {
        return false;
    }

}

Let’s go briefly through each of those methods:
start() is called when an anonymous request accesses a resource that requires authentication. In the case of API we just need to return 401
getCredentials() - this checks if Authorization header and token are set.
getUser() - here we decode token, get the username that was stored inside and we load the whole user object. If token was modified we won’t get a valid username
checkCredentials() - we can just return true because we already know that a valid token was sent
onAuthenticationFailure() - called when authentication executed but failed
onAuthenticationSuccess() - let the request continue to controller
supportsRememberMe()  -  no need for API
Now we need to register this class as a service:

// source app/config/services.yml
services:

    app.jwt_token_authenticator:
        class: AppBundle\Security\JwtAuthenticator
        arguments: ['@doctrine.orm.entity_manager', '@lexik_jwt_authentication.encoder']

And then enable another firewall inside your security configuration, and add some basic access control rules:

// source app/config/security.yml

    firewalls:
        # Custom authentication firewall for all request thats starts from /api
        api:
            pattern: ^/api/(?!token)
            guard:
                authenticators:
                    - app.jwt_token_authenticator

        # Here you handle regular form authentication
        main:
            anonymous: ~
            guard:
                authenticators:
                    - app.form_login_authenticator

    access_control:
        - { path: /api/token-authentication, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api, roles: [ROLE_USER, ROLE_API_USER] }

        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/(css|js), roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/(_wdt|_profiler), roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, roles: ROLE_USER }

Now we can test to see if everything is working correctly. Open Postman and perform a GET request to secured resource path. Set the following header:
Header: Authorization
Value: Bearer {token that was generated from token-authentication request}

screen_shot_2016-04-04_at_16.11.27.png

We should receive a valid JSON response.

Conclusion

This is a basic overview of JSON Web Tokens but, thanks to Symfony, implementing this functionality is quite easy. JSON Web Tokens works across all popular programming languages and are gaining in popularity.

Author:

Andrzej Kogut
Andrzej
  • symfony
  • authentication

Get in Touch