35
votes

I am trying to test a scenario, that on the one hand, anonymous users should immediately get a disconnect from a Websocket connection and on the other hand, authenticated users should stay in the websocket connection. The first case is easy testable by using the code down under. The authentication process is not working.

For session storage, I am using Cookie authentication in combination with a database: Symfony PDO Session Storage. It's all working fine, but when it comes to testing the described behaviour by using authentication, I don't know how to authenticate the user in a test. As a client, I am using Pawl asynchronous Websocket client. This looks the following:

\Ratchet\Client\connect('ws://127.0.0.1:8080')->then(function($conn) {
    $conn->on('message', function($msg) use ($conn) {
        echo "Received: {$msg}\n";
    });

    $conn->send('Hello World!');
}, function ($e) {
    echo "Could not connect: {$e->getMessage()}\n";
});

I know that as a third parameter, I can pass header information to the "connect" method, but I cannot find a way so that the client is connected and the cookie is passed correctly during the ws handshake. I thought of something like:

  1. Authenticate a client by creating an authentication token
  2. I create a new entry in the session table in database with serialized user
  3. I pass the created cookie as a third argument to the connect method

This is the theory I thought that would work, but the user always stays anonym on websocket side. Here the code to the theory so far:

// ...
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class WebsocketTest extends WebTestCase
{

    static $closed;

    protected function setUp()
    {
      self::$closed = null;
    }


    public function testWebsocketConnection()
    {
      $loop = Factory::create();
      $connector = new Connector($loop);

      // This user exists in database user tbl
      $symfClient = $this->createSession("[email protected]");

      $connector('ws://127.0.0.1:80', [], ['Origin' => 'http://127.0.0.1', 'Cookie' => 
                 $symfClient->getContainer()->get('session')->getName() . '=' 
                . $symfClient->getContainer()->get('session')->getId()])
        ->then(function(WebSocket $conn) use($loop){

            $conn->on('close', function($code = null, $reason = null) use($loop) {
                self::$closed = true;
                $loop->stop();
            });
            self::$closed = false;

        }, function(\Exception $e) use ($loop) {
            $this->fail("Websocket connection failed");
            $loop->stop();
        });

      $loop->run();

      // Check, that user stayed logged
      $this->assertFalse(self::$closed);
    }

    private function createSession($email)
    {
      $client = static::createClient();
      $container = $client->getContainer();

      $session = $container->get('session');
      $session->set('logged', true);

      $userManager = $container->get('fos_user.user_manager');
      $em = $container->get('doctrine.orm.entity_manager');
      $loginManager = $container->get('fos_user.security.login_manager');
      $firewallName = 'main';

      $user = $userManager->findUserByEmail($email);

      $loginManager->loginUser($firewallName, $user);

      // save the login token into the session and put it in a cookie
      $container->get('session')->set('_security_' . $firewallName,
        serialize($container->get('security.token_storage')->getToken()));
      $container->get('session')->save();
      $client->getCookieJar()->set(new Cookie($session->getName(), $session->getId()));


      // Create session in database
      $pdo = new PDOSessionStorage();
      $pdo->setSessId($session->getId());
      $pdo->setSessTime(time());
      $pdo->setSessData(serialize($container->get('security.token_storage')->getToken()));
      $pdo->setSessLifetime(1440);

      $em->persist($pdo);
      $em->flush();

      return $client;
  }

}

As config_test.yml, I configured the session the following way:

session:
    storage_id:     session.storage.mock_file
    handler_id:     session.handler.pdo

For server side websocket implementation, I am using Ratchet, which is being wrapped by the following Symfony bundle: Gos Websocket Bundle

How to authenticate the user when testing websockets? On websocket server, the user is always something like "anon-15468850625756b3b424c94871115670", but when I test manually, he gets connected correct.

Additional question (secondary): How to test the subscription to topics? (pubsub) There are no blog entries or anything else about this on the internet.

Update: No one ever functional tested their websockets? Is this unimportant, useless or why can't anyone help on that important topic?

1
Are you only looking for a way to pass on the cookie, or do you want to immediately send for example a sessionId/userId?mitchken
I've updated the question with the code i've written. The problem is, that on Ratchet side, the user stays anonym in the test. Maybe I'm passing the cookie wrong. The session id is transported in a cookie.user3746259
'Cookie' => $symfClient->getContainer()->get('session')->getId() . '=' . $symfClient->getContainer()->get('session')->getId() sure this is correct? shouldn't the first rather be the cookie name?bwoebi
@user3746259 Okay, I cannot quite guess what's going wrong there, so, first step, look into the websocket code of ratchet directly, what are the raw headers received? Does it match your expectations? … Debug your application until you find the source. It can be a tedious task, but at the end of it, you typically know why it fails.bwoebi
@user3746259 Looks like no-one has had the issue maybe. Only solution is hardcore debugging there, I'm sorry ;o)bwoebi

1 Answers

3
votes

You have a cart before the horse situation here. When you set a cookie on a client connection that cookie is then only sent on subsequent requests (websockets or XHR, GET, POST, etc) provided the cookie restrictions (httpOnly, secure, domain, path, etc) match.

Any cookies available are sent during the initial handshake of the websocket connection. Setting a cookie on an open connection will set the cookie on the client but since the socket is already an open connection and established (post handshake) the server will be blind to those cookies for the duration of that connection.

Some people have had success setting the cookie during the handshake. However, that requires the server and client socket implementations supporting this behavior and passing credentials as get parameters (bad practice).

So I think your only real options are:

  • handle authentication through XHR or other request before opening a websocket
  • use the websocket for authentication, but then on successful login:
    • set your auth cookie
    • close the existing socket
    • initiate a new socket from the client (which will then carry your auth cookie)
  • forget cookies entirely and handle an authentication exchange on the server based on the request/resource ID for the open connection.

If you choose the last option you could still set the cookie and look for the cookie to restore connections on reconnects.