Integrating Symfony Authentication for ThruwayBundle

The last post I wrote gave instructions on how to push updates realtime with Symfony using the ThruwayBundle.

If you implemented those instructions, you probably thought to yourself that there really isn’t a whole lot of security. If someone could guess your event names, they would be able connect and subscribe to them because the router has no authentication or authorization implemented.

This post is going to give you the basic structure of authentication for your router, allowing us to grant access to the router only to those that we want to have it.

Symfony Users

We are going to start with our project from the last post and add Symfony in-memory user provider. We will then use these users to create a JWT token that will be the used to authenticate users with the Thruway WAMP router.

Again, with this post I am not trying to show how to use Symfony so I will not be explaining all of the steps. You should be able to extend the principles here into your project whether you are using in-memory, fos-user, or some other user system.

Add the users to security.yml:

    providers:
        in_memory:
            memory:
                users:
                    ryan:
                        password: ryanpass
                        roles: 'ROLE_USER'
                    admin:
                        password: kitten
                        roles: 'ROLE_ADMIN'

The Token

We will be using JWT tokens to have the router authenticate the WAMP connection. For this we will need a library to generate JWTs:

composer require firebase/php-jwt

Add a parameter that will store our JWT secret key (under parameters in config.yml):

jwt_key: MyJwtKey

For the client to retrieve the JWT, we will create a Symfony route that returns the token:

In DefaultController.php, add the method:

    /**
     * @Route("/get_token", name="get_token")
     */
    public function getToken() {
        $key = $this->get('service_container')->getParameter("jwt_key");
        $token = [
            "authid" => $this->getUser()->getUsername(),
            "exp" => time() + 86400 // expires a day from now
        ];

        $jwt = \JWT::encode($token, $key);

        return new Response($jwt);
    }

Let’s secure this so that only admin users can access this route. Add http_basic to the main: firewall section (in security.yml):

http_basic: ~

This will let us authenticate with basic auth when we try and get the token.

We’ll need tell Symfony that you have to be admin to get to the token route. Add this section to the security section of security.yml:

    access_control:
        - { path: ^/get_token, roles: ROLE_ADMIN }

    encoders:
            Symfony\Component\Security\Core\User\User: plaintext

Go ahead and clear cache and try to get the token: http://127.0.0.1:8000/get_token.

If everything is configured correctly, you should be prompted for a username and password which is admin/kitten and then be presented with the JWT (which is a bunch of letters and numbers and stuff).

Using the JWT for Router Authentication

To have the router require authentication, we will need to configure a few things and add an authorization provider for Thruway. For simplicity, we are going to reuse the JWT Authentication provider similar to the one that I wrote about a few posts back for Thruway.

It is actually possible to write an auth provider using a Symfony controller that registers appropriate calls to handle it, but that is more complex and we are just getting started with ThruwayBundle.

Create JwtAuthProvider.php in src/AppBundle/Security:

jwtKey = $jwtKey;
        parent::__construct($authRealms);
    }

    public function getMethodName() {
        return 'jwt';
    }

    public function processAuthenticate($signature, $extra = null)
    {
        try {
            $jwt = \JWT::decode($signature, $this->jwtKey, ['HS256']);

            return ["SUCCESS", (object)["authid" => $jwt->authid]];
        } catch (\Exception $e) {
            return ["FAILURE"];
        }
    }
}

Configure the service provider to build this auth provider into the router when it starts.

Enable the authentication manager in config.yml under the router section of voryx_thruway:

        authentication: true

Now in services.yml, configure the JwtAuthProvider:

    jwt_auth_provider:
        class: AppBundle\Security\JwtAuthProvider
        arguments:
            - ["@=parameter('voryx_thruway')['realm']"]
            - "%jwt_key%"
        tags:
            - { name: thruway.internal_client }

The thruway.internal_client tag tells the bundle to add this auth provider as an internal client to the router.

Now that the router will be requiring authentication, we will have to change the way our events are published. If we left things as they are now, the client that publishes the events would not be allowed to connect. We need to make it so that that client does not have to authenticate. We can do this simply by specifying a trusted_url for that client to connect to. In config.yml add the parameter to the voryx_thruway section:

    trusted_url: 'ws://127.0.0.1:8081' 

It is important to secure access to the trusted_url as all WAMP connections to this will completely bypass all security. We are using localhost right now, so there isn’t much to worry about with that right now.

The Browser Client

We now have to configure the browser to use JWT to log in.

Add a jwt variable and replace the connection declaration in the livestatus.html file:

    var jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoaWQiOiJzb21ldXNlciIsImlzcyI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwiYXVkIjoiaHR0cDpcL1wvZXhhbXBsZS5jb20iLCJpYXQiOjE0MzQ5NDk2ODEsImV4cCI6MTQzNTAzNjA4MX0.fnHTCtfqVZWBeh4iKoqwZnqSK4sDWuG7Tr5MAwgQtJc";

    var connection = new autobahn.Connection({
        url: 'ws://127.0.0.1:8080/',
        realm: 'realm1',
        authmethods: ["jwt"],
        onchallenge: function () {
            return jwt;
        }
    });

You should replace the JWT with the JWT you got above.

Notice that we are now telling the router that we are using the jwt authmethod and we have provided an onchallenge handler that will return the jwt.

At this point you can clear your cache and refresh your browser window. Everything should work as before. Open the console and try creating or updating some UserStatuses. You should see events in the console window.

There is still the issue of loading the key into the browser in the first place. We can’t just statically have a key in the html file as that would defeat the whole purpose. A good way to deal with this is to generate the javascript with a template so you can insert the key into the script. There are a few different ways you could do this, for now, just copy the livestatus.html file to src/AppBundle/Resources/views/livestatus.html.twig. Then replace the jwt line:

var jwt = "{{ jwt }}";

In your DefaultController.php, replace the get_token route with:

    /**
     * @Route("/livestatus", name="livestatus")
     * @Template("@App/livestatus.html.twig")
     */
    public function getLiveStatus() {
        $key = $this->get('service_container')->getParameter("jwt_key");
        $token = [
            "authid" => $this->getUser()->getUsername(),
            "exp" => time() + 86400 // expires a day from now
        ];

        $jwt = \JWT::encode($token, $key);

        return [ "jwt" => $jwt ];
    }

Make sure you update security.yml to set permissions on this route as it has changed names:

        - { path: ^/livestatus, roles: ROLE_ADMIN }

You can now navigate to http://127.0.0.1:8000/livestatus. Because you were already logged in, you will probably not see the login page. Try it in an incognito window if you want to see it make you log in again.

This is pretty good – you now have your entity updates being published from a traditional web application in realtime to a browser-based client that is also secured with a token and using your traditional web site authentication.

Leave a Reply