Search code examples
symfonyfosuserbundle

Combine a stateful application with stateless JWT


We have a FOSUserBundle login system authenticating via LDAP and the fr3d LDAP bundle. It behaves like a normal multiple page application using sessions. We also have several RESTful endpoints using the FOSRestbundle and normal sessions for authentication. However, we need to share a few end points with an external application.

We managed to implement JWT using the Lexik bundle. It returns a token just fine. However, I don't know the best way to let a user using our login form to get this token so their request can pass it along in the header or session. My question is how to allow a user to login to our application in a stateful manner, but also receive the JWT and pass it to the server on ajax requests. This way I can allow external clients to connect directly to the API. Below is my symfony2 security configuration, security.yml:

security:

    #erase_credentials: false

    encoders:
        FOS\UserBundle\Model\UserInterface: bcrypt

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        chain_provider:
            chain:
                providers: [my_user_provider, fr3d_ldapbundle]

        in_memory:
            memory:
                users:
                    admin: { password: secret, roles: 'ROLE_ADMIN' }

        my_user_provider:
            id: app.custom_user_provider

        fos_userbundle:
            id: fos_user.user_provider.username

        fr3d_ldapbundle:
            id: fr3d_ldap.security.user.provider

    access_control:
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, role: IS_AUTHENTICATED_FULLY }
        - { path: ^/api, role: IS_AUTHENTICATED_FULLY }
        - { path: ^/api/login, role: IS_AUTHENTICATED_ANONYMOUSLY }

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        api_login:
            pattern: ^/api/login
            fr3d_ldap:  ~
            provider: chain_provider
            anonymous: true
            stateless: false
            form_login:
                check_path: /api/login_check
                username_parameter: username
                password_parameter: password
                require_previous_session: false
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure

        api:
            pattern:   ^/api
            provider: chain_provider
            stateless: false 
            lexik_jwt:
                throw_exceptions: true
                create_entry_point: true

        main:
            pattern: ^/
            fr3d_ldap:  ~
            form_login:
                # provider: fos_userbundle
                provider: chain_provider
                always_use_default_target_path: true
                default_target_path: /
                csrf_provider: security.csrf.token_manager
            logout: true
            anonymous: true
            switch_user: { role: ROLE_LIMS-BIOINFO}

EDIT:

Based on Kévin's answer I decided to implement a custom Twig extension to get the token for the logged in user on each page load:

AppBundle/Extension/JsonWebToken.php:

<?php 

namespace AppBundle\Extension;

use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class JsonWebToken extends \Twig_Extension
{
    /**
     * @var ContainerInterface
     */
    private $container;

    /**
     * @var JWTManagerInterface
     */
    private $jwt;

    public function __construct(ContainerInterface $container, JWTManagerInterface $jwt)
    {
        $this->container = $container;
        $this->jwt = $jwt;
    }

    public function getName()
    {
        return 'json_web_token';
    }

    public function getFunctions()
    {
        return [
            'json_web_token' => new \Twig_Function_Method($this, 'getToken')
        ];
    }

    public function getToken()
    {
        $user = $this->container->get('security.token_storage')->getToken()->getUser();
        $token = $this->jwt->create($user);

        return $token;
    }
}

app/config/services.yml:

app.twig_jwt:
    class: AppBundle\Extension\JsonWebToken
    arguments: ["@service_container", "@lexik_jwt_authentication.jwt_manager"]
    tags:
        - { name: twig.extension }

app/Resources/views/layout.html.twig

<script>window.jsonWebToken = '{{ json_web_token() }}';</script>

app/Resources/modules/layout/app.js:

 var jsonWebToken = window.jsonWebToken;
    $.ajaxSetup({
        beforeSend: function (xhr) {
            xhr.setRequestHeader("Authorization","Bearer " + jsonWebToken);        
        } 
    });  

So far this seems to be working well. It let's my external API users and internal application users share the same authentication methods.


Solution

  • As the JWT token must be stored client-side (and not in a cookie to prevent CSRF attacks), you can use the create method of the lexik_jwt_authentication.jwt_manager service provided by LexikJWTAuthenticationBundle to generate a token after the login, then inject this token in a <script> tag in the generated HTML.