Øystein Amundsen
Øystein Amundsen

Reputation: 4203

Building realtime app using Laravel and Latchet websocket

I'm building a closed app (users need to authenticate in order to use it). I'm having trouble in identifying the currently authenticated user from my Latchet session. Since apache does not support long-lived connections, I host Latchet on a separate server instance. This means that my users receive two session_id's. One for each connection. I want to be able to identify the current user for both connections.

My client code is a SPA based on AngularJS. For client WS, I'm using the Autobahn.ws WAMP v1 implementation. The ab framework specifies methods for authentication: http://autobahn.ws/js/reference_wampv1.html#session-authentication, but how exactly do I go about doing this?

Do I save the username and password on the client and retransmit these once login is performed (which by the way is separate from the rest of my SPA)? If so, won't this be a security concearn?

And what will receive the auth request server side? I cannot find any examples of this...

Please help?

P.S. I do not have reputation enough to create the tag "Latchet", so I'm using Ratchet (which Latchet is built on) instead.

Upvotes: 0

Views: 2438

Answers (3)

Øystein Amundsen
Øystein Amundsen

Reputation: 4203

Ok, Greg was kind enough to provide a full example of the client implementation on this, so I wont do anything more on that. It works with just a few tweaks and modifications to almost any use-case I can think of. I will mark his answer as the correct one. But his input only covered the theory of the backend implementation, so I will try to fill in the blanks here for postparity.

I have to point out though, that the solution here is not complete as it does not give me a shared session between my SPA/REST connection and my WS connection.

I discovered that the authentication request transmitted by autobahn is in fact a variant of RPC and for some reason has hardcoded topic names curiously resembling regular url's:

- 'http://api.wamp.ws/procedure#authreq' - for auth requests
- 'http://api.wamp.ws/procedure#auth'    - for signed auth client responses

I needed to create two more routes in my Laravel routes.php

// WS CRA routes
Latchet::topic('http://api.wamp.ws/procedure#authreq', 'app\\socket\\AuthReqController');
Latchet::topic('http://api.wamp.ws/procedure#auth',    'app\\socket\\AuthReqController');

Now a Latchet controller has 4 methods: subscribe, publish, call and unsubscribe. Since both the authreq and the auth calls made by autobahn are RPC calls, they are handled by the call method on the controller.

The solution first proposed by oberstet and then backed up by Greg, describes a temporary auth key and secret being generated upon request and held temporarily just long enough to be validated by the WS CRA procedure. I've therefore created a REST endpoint which generates a persisted key value pair. The endpoint is not included here, as I am sure that this is trivial.

class AuthReqController extends BaseTopic {
    public function subscribe ($connection, $topic) {    }

    public function publish ($connection, $topic, $message, array $exclude, array $eligible) {    }

    public function unsubscribe ($connection, $topic) {    }

    public function call ($connection, $id, $topic, array $params) {
        switch ($topic) {
            case 'http://api.wamp.ws/procedure#authreq':
                return $this->getAuthenticationRequest($connection, $id, $topic, $params);
            case 'http://api.wamp.ws/procedure#auth':
                return $this->processAuthSignature($connection, $id, $topic, $params);
        }
    }

    /**
     * Process the authentication request
     */
    private function getAuthenticationRequest ($connection, $id, $topic, $params) {
        $auth_key = $params[0]; // A generated temporary auth key
        $tmpUser  = $this->getTempUser($auth_key); // Get the key value pair as persisted from the temporary store.
        if ($tmpUser) {
            $info = [
                'authkey'   => $tmpUser->username,
                'secret'    => $tmpUser->secret,
                'timestamp' => time()
            ];
            $connection->callResult($id, $info); 
        } else {
            $connection->callError($id, $topic, array('User not found'));
        }
        return true;
    }

    /**
     * Process the final step in the authentication
     */
    private function processAuthSignature ($connection, $id, $topic, $params) {
        // This should do something smart to validate this response.

        // The session should be ours right now. So store the Auth::user()
        $connection->user = Auth::user(); // A null object is stored.
        $connection->callResult($id, array('msg' => 'connected'));
    }

    private function getTempUser($auth_key) {
        return TempAuth::findOrFail($auth_key);
    }
}

Now somewhere in here I've gone wrong. Cause if I were supposed to inherit the ajax session my app holds, I would be able to call Auth::user() from any of my other WS Latchet based controllers and automatically be presented with the currently logged in user. But this is not the case. So if somebody see what I'm doing wrong, give me a shout. Please!

Since I'm unable to get the shared session, I'm currently cheating by transmitting the real username as a RPC call instead of performing a full CRA.

Upvotes: 0

Greg
Greg

Reputation: 6759

Create an angularjs service called AuthenticationService, inject where needed and call it with:

AuthenticationService.check('login_name', 'password');

This code exists in a file called authentication.js. It assumes that autobahn is already included. I did have to edit this code heavily removing all the extra crap I had in it,it may have a syntax error or two, but the idea is there.

angular.module(
  'top.authentication',
  ['top']
)

.factory('AuthenticationService', [ '$rootScope', function($rootScope) {
    return {
        check: function(aname, apwd) {
              console.log("here in the check function");
              $rootScope.loginInfo = { channel: aname, secret: apwd };
              var wsuri = 'wss://' + '192.168.1.11' + ':9000/';
              $rootScope.loginInfo.wsuri = wsuri;
              ab.connect(wsuri,
                  function(session) {
                      $rootScope.loginInfo.session = session;
                      console.log("connected to " + wsuri);
                      onConnect(session);
                  },
                  function(code,reason) {
                      $rootScope.loginInfo.session = null;
                      if ( code == ab.CONNECTION_UNSUPPORTED) {
                          console.log(reason);
                      } else {
                          console.log('failed');
                          $rootScope.isLoggedIn = 'false';
                      }
                  }
              );

              function onConnect(sess) {
                  console.log('onConnect');
                  var wi = $rootScope.loginInfo;
                  sess.authreq(wi.channel).then(
                    function(challenge) {
                        console.log("onConnect().then()");
                        var secret = ab.deriveKey(wi.secret,JSON.parse(challenge).authextra);
                        var signature = sess.authsign(challenge, secret);
                        sess.auth(signature).then(onAuth, ab.log);
                    },ab.log
                  );
              }

              function onAuth(permission) {
                  $rootScope.isLoggedIn = 'true';
                  console.log("authentication complete");
                  // do whatever you need when you are logged in..
              }
        }
    };
    }])

then you need code (as you point out) on the server side. I assume your server side web socket is php coding. I can't help with that, haven't coded in php for over a year. In my case, I use python, I include the autobahn gear, then subclass WampCraServerProtocol, and replace a few of the methods (onSessionOpen, getAuthPermissions, getAuthSecret, onAuthenticated and onClose) As you can envision, these are the 'other side' of the angular code knocking at the door. I don't think autobahn supports php, so, you will have to program the server side of the authentication yourself.

Anyway, my backend works much more like what @oberstat describes. I establish authentication via old school https, create a session cookie, then do an ajax requesting a 'ticket' (which is a temporary name/password which i associate with the web authenticated session). It is a one use name/password and must be used in a few seconds or it disappears. The point being I don't have to keep the user's credentials around, i already have the cookie/session which i can create tickets that can be used. this has a neat side affect as well, my ajax session becomes related to my web socket session, a query on either is attributed to the same session in the backend.

-g

Upvotes: 3

oberstet
oberstet

Reputation: 22011

I can give you a couple of hints regarding WAMP-CRA, which is the authentication mechnism this is referring:

WAMP-CRA does not send passwords over the wire. It works by a challenge-response scheme. The client and server have a shared secret. To authenticate a client, the server will send a challenge (something random) that the client needs to sign - using the secret. And only the signature is sent back. The client might store the secret in browser local storage. It's never sent.

In a variant of above, the signing of the challenge the server sends is not directly signed within the client, but the client might let the signature be created from an Ajax request. This is useful when the client was authenticated using other means already (e.g. classical cookie based), and the signing can then be done in the classical web app that was authenticating.

Upvotes: 1

Related Questions