Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP Websocket authenticate user in a test (pass session cookie)

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?

like image 437
user3746259 Avatar asked May 31 '16 13:05

user3746259


1 Answers

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.

like image 130
xcsrz Avatar answered Nov 13 '22 12:11

xcsrz