cult3

Authenticating with Pusher in a Laravel application

Nov 23, 2015

Table of contents:

  1. How does Pusher’s authentication work?
  2. Creating the Authenticators
  3. Creating the Authentication Manager class
  4. Authenticating in the Controller
  5. Conclusion

Last week we looked at setting up Pusher in a Laravel application.

This involved creating a Client to make HTTP requests to Pusher, and a Storage containaer to store the currently active users.

In today’s tutorial we’re going to be dealing with the authentication aspect of setting up Pusher.

How does Pusher’s authentication work?

In order to provide the required functionality, we need to set up both private and presence channels. I won’t go into detail as to what are the differences between private and presence channels as you can get a better description in the Pusher Documentation.

When a user logs into the application, requests will be sent to the API to subscribe to the channels. We need to be able to accept these requests, check that the user has permission, and then either allow or deny access.

You can read more about this in the Pusher Documentation under Authenticating users.

Creating the Authenticators

So hopefully the above section was clear, but just to recap, we need to provide a way to authenticate for both private and presence channels. The authentication process will basically be that the user does actually belong to the account they will be joining.

As we will need to implementations for both private and presence channels, we can first define an interface:

interface Authenticator
{
    /**
     * Authenticate the request
     *
     * @return
     */
    public function authenticate();
}

Next we can define the PresenceAuthenticator:

class PresenceAuthenticator implements Authenticator
{
    /**
     * @param Client $client
     * @param User $user
     * @param string $account
     * @param string $channel
     * @param string $socket
     * @return void
     */
    public function __construct(
        Client $client,
        User $user,
        $account,
        $channel,
        $socket
    ) {
        $this->client = $client;
        $this->user = $user;
        $this->account = $account;
        $this->channel = $channel;
        $this->socket = $socket;
    }
}

First I will inject an object that implements the Client interface from last week’s tutorial, as well as the User in question and the $account, $channel and $socket, which I will be accepting from the HTTP request.

The User has already been authenticated with the API at this point, so we don’t need to do any additional checking.

Next we can implement the authenticate() method:

/**
 * Authenticate the request
 *
 * @return
 */
public function authenticate()
{
    // Check that the user has access to the account

    $data = [/* */];

    return $this->client->authenticate($this->channel, $this->socket, json_encode($data));
}

In this method you should do any checking you need to do in order to authenticate the request. In this example, I would be checking to see if the user had access to the account. An easy way to do that would be to use guards as we saw in Implementing Business Rules as Guards.

Next we can create an array of data to send, for example the user’s first and last name.

And finally we can pass the request to the Client.

The PrivateAuthenticator is basically exactly the same but we don’t need to pass any additional data to the Client:

class PrivateAuthenticator implements Authenticator
{
    /**
     * @var Client
     */
    private $client;

    /**
     * @var User
     */
    private $user;

    /**
     * @var string
     */
    private $account;

    /**
     * @var string
     */
    private $channel;

    /**
     * @var string
     */
    private $socket;

    /**
     * @param Client $client
     * @param User $user
     * @param string $account
     * @param string $channel
     * @param string $socket
     * @return void
     */
    public function __construct(
        Client $client,
        User $user,
        $account,
        $channel,
        $socket
    ) {
        $this->client = $client;
        $this->user = $user;
        $this->account = $account;
        $this->channel = $channel;
        $this->socket = $socket;
    }

    /**
     * Authenticate the request
     *
     * @return
     */
    public function authenticate()
    {
        // Check that the user has access to the account

        return $this->client->authenticate($this->channel, $this->socket);
    }
}

The tests for these classes are really aimed at ensuring that the user belongs to the account, so I’ll leave that up to you.

Creating the Authentication Manager class

Now that we have the two implementations for authenticating the two types of authentication request, we can wrap them up in a service class to make them easier to use.

First I will create the Manager class and inject an object that implements the Client interface from last week:

class Manager
{
    /**
     * @var Client
     */
    private $client;

    /**
     * @param Client $client
     * @return void
     */
    public function __construct(Client $client)
    {
        $this->client = $client;
    }
}

Next I will register the two authentication classes as a class property of the Manager class:

/**
 * @var array
 */
private $types = [
    'private' => PrivateAuthenticator::class,
    'presence' => PresenceAuthenticator::class
];

Alternatively you could inject this through the constructor, but it’s never going to change so I’m fine with simply doing this.

Finally I will define an auth() method as the public API:

/**
 * Authenticate a Pusher request
 *
 * @param User $user
 * @param string $account
 * @param string $channel
 * @param string $socket
 * @return
 */
public function auth(User $user, $account, $channel, $socket)
{
    foreach ($this->types as $type => $authenticator) {
        if (starts_with($channel, $type)) {
            return new $authenticator($client, $user, $account, $channel, $socket);
        }
    }

    throw new InvalidChannelAuthenticator('invalid_channel', $channel);
}

This will accept the User as well as the $account, $channel, and $socket from the HTTP request.

Next we can cycle through each of the authentication types. By default the channel name will start with either private or presence so we can easily match against this using Laravel’s starts_with() helper function.

Finally we can return a new instance of the relevant Authenticator implementation.

If the request includes a rogue channel name, the request will be aborted with an Exception. This Exception will bubble up to the surface and be automatically returned as the correct HTTP response as we saw in Dealing with Exceptions in a Laravel API application.

The tests for this class look like this:

class ManagerTest extends \TestCase
{
    use DatabaseMigrations;

    /** @test */
    public function should_throw_exception_on_invalid_channel_name()
    {
        $this->setExpectedException(InvalidChannelAuthenticator::class);

        $client = m::mock(Client::class);
        $user = factory(User::class)->create();

        (new Manager($client))->auth($user, ", 'invalid', ");
    }

    /** @test */
    public function should_create_private_authenticator()
    {
        $client = m::mock(Client::class);
        $user = factory(User::class)->create();
        $channel = "private-member-cca23388-6866-47e5-a3d2-f85d301b0e34";

        $auth = (new Manager($client))->auth($user, ", $channel', ");
        $this->assertInstanceOf(PrivateAuthenticator::class, $auth);
    }

    /** @test */
    public function should_create_presence_authenticator()
    {
        $client = m::mock(Client::class);
        $user = factory(User::class)->create();
        $channel = "presence-account-3984b36e-e491-408d-85e4-215babd43b47";

        $auth = (new Manager($client))->auth($user, ", $channel, ");
        $this->assertInstanceOf(PresenceAuthenticator::class, $auth);
    }
}

Finally as I’ve shown a few times over the last couple of weeks, we can add a Facade to make this service class feel more at home in a Laravel application:

class Pusher extends Facade
{
    /**
     * Get the registered name of the component
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return "pusher";
    }
}

Authenticating in the Controller

Now that we have everything in place to accept and authenticate Pusher requests, we can add the Controller method which will be accepting the HTTP request:

/**
 * Authenticate a Pusher request
 *
 * @param Request $request
 * @return JsonResponse
 */
public function auth(Request $request)
{
    $user = Context::get('User')->model();

    $socket = $request->input('socket_id');
    $channel = $request->input('channel_name');
    $account = $request->input('account_id');

    $response = Pusher::auth($user, $account, $channel, $socket)->authenticate();

    return response($response, 200)->header('Content-Type', 'application/json');
}

First, we can grab the user from the context. We looked at how to do this in Managing Context in a Laravel application and Setting the Context in a Laravel Application.

Next, we grab the $socket, $channel, and the $account from the Request.

Next, we can use the Pusher service to grab the correct Authenticator instance and then capture the response from Pusher and then return it from the method.

Finally, we will also require a route to send the request:

class PusherRoutes
{
    /**
     * Define the routes
     *
     * @param Registrar $router
     * @return void
     */
    public function map(Registrar $router)
    {
        $router->post("auth/pusher", [
            "as" => "pusher.auth",
            "uses" => "PusherController@auth",
        ]);
    }
}

Conclusion

In today’s tutorial we’ve looked at authenticating with Pusher.

The hardest bit in my opinion is understanding the difference between private and presence channels!

Today we’ve looked at a technique for using multiple implementations without having to do any conditional checking.

In theory, if Pusher were to create another type of channel, all we would need to do would be to create a new class that implements the Authenticator interface.

We have also abstracted the instantiation of the authenticator class behind a Manger.

This will automatically deal with returning the right implementation, and it will deal with invalid requests by throwing an Exception.

Next week will be the final instalment as we look at Pusher events as well as sending data to the client during the normal execution of the application.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.