phplaravelwebsocketwebrtcratchet

How to access Laravel Auth in Ratchet


I found a post on Laravel.io on how to load Laravel sessions into Ratchet which is outdated and uses Laravel 5.4 so I've altered a few things to get this to work with Laravel 8.x

public function onOpen(ConnectionInterface $conn)
{
    // Attach connection
    $this->clients->attach($conn);

    // Create a new session handler for this client
    $session = (new SessionManager(App::getInstance()))->driver();

    // Get the cookies
    $cookiesRaw = $conn->httpRequest->getHeader('Cookie');
    $cookies = [];

    if(count($cookiesRaw)) {
        $cookies = Header::parse($cookiesRaw)[0]; // Array of cookies
    }

    // Get the laravel's one - todo: try catch
    $sessionId = Crypt::decrypt(urldecode($cookies[Config::get('session.cookie')]), false);

    var_dump($sessionId);

    // Set the session id to the session handler
    $session->setId($sessionId);

    // Bind the session handler to the client connection
    $conn->session = $session;

    var_dump($conn->session->getId());
}

I then altered the send message too because I am receiving unexpected results.

public function onMessage(ConnectionInterface $conn, MessageInterface $msg)
{
    $conn->session->start();
    $sessionId = $conn->session->getId();

    var_dump($sessionId);

    if(!is_null(($decoded = json_decode(base64_decode($msg), true))) && array_diff(['message'], array_keys($decoded)))
        return;

    var_dump($decoded['message']);

    return;
}

I test this with JS front-end like so:

class WebRTC
{
    socket;
    constants;
    timerId;

    constructor(protocol, fqdns, port) {
        this.constants = {
            protocol: protocol,
            fqdns: fqdns,
            port: port
        };

        this.listenChanges();
    }

    listenChanges() {
        this.socket = new WebSocket(`${this.constants.protocol}://${this.constants.fqdns}:${this.constants.port}`);

        this.socket.onmessage = e => {
            console.log(atob(e.data));
        };

        this.socket.onerror = () => {
            this.socket.close();
        };

        this.socket.onopen = () => {
            console.info('Connected to WebRTC Chat Server...');

            this.socket.send(btoa(JSON.stringify({
                message: '{{ session()->getId() }}' // Expected session
            })));

            clearInterval(this.timerId);

            this.socket.onclose = () => {
                this.timerId = setInterval(() => {
                    this.listenChanges();
                }, 1000);
            };
        };
    }
}

new WebRTC('ws', '127.0.0.1', '8080');

& When the connection opens, I sent the session()->getId() which is the expected session I need. However, my output in the CLI is:

onOpen() : $sessionId

string(81) "b0e41cf0d856bdfc8427e1fdde62d5a154519f9c|MLXa9H2BbnQmySt2hRB360UANxLGHyz6iRMxGcoG"

onOpen() : $conn->session->getId()

string(40) "qyaDOQjNFlbrbjvvKRE1m5sN0dsGqqAsoMfkeqyU"

onMessage(): $conn->session->getId()

string(40) "qyaDOQjNFlbrbjvvKRE1m5sN0dsGqqAsoMfkeqyU"

JS blade formatted actual session that is sent as a message

string(40) "MLXa9H2BbnQmySt2hRB360UANxLGHyz6iRMxGcoG"

Here, my expected onMessage() method receive the dependency injected $conn (ConnectionInterface) with the ->session->getId() of the actual session()->getId() so I can make Auth::user() work.

Any ideas on what I'm doing wrong? I tried the var_dump($conn->session->get(Auth::getName())); as the Laravel.Io says to do but it returns null on the var_dump and my user is logged in.

This should then give me access to use User::find() or Auth::user().


Solution

  • When you decrypt the session cookie with Crypt::decrypt(..., false), the resulting string isn't just the session ID. It's a pipe-separated string containing the payload (the session ID) and a Message Authentication Code (MAC) for security, looking something like this: sessionId|macHash.

    Your code was passing this entire string to $session->setId(). The session handler saw it as an invalid ID and correctly generated a new, random one. The fix is to simply split that decrypted string and grab the first part.

    It's best practice to handle authentication directly in the onOpen method.

    <?php
    
    namespace App\Websockets;
    
    use App\Models\User;
    use Illuminate\Support\Facades\App;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\Facades\Config;
    use Illuminate\Support\Facades\Crypt;
    use Illuminate\Session\SessionManager;
    use Ratchet\ConnectionInterface;
    use Ratchet\MessageComponentInterface;
    use Ratchet\RFC6455\Messaging\MessageInterface;
    use SplObjectStorage;
    use GuzzleHttp\Psr7\Header;
    
    class WebSocketHandler implements MessageComponentInterface
    {
        protected $clients;
    
        public function __construct()
        {
            $this->clients = new SplObjectStorage;
        }
    
        public function onOpen(ConnectionInterface $conn)
        {
            $this->clients->attach($conn);
    
            try {
                $session = (new SessionManager(App::getInstance()))->driver();
                
                $rawCookies = $conn->httpRequest->getHeader('Cookie');
                if (empty($rawCookies)) {
                    throw new \Exception("No cookies found.");
                }
                $cookies = Header::parse($rawCookies)[0];
    
                $laravelCookie = urldecode($cookies[Config::get('session.cookie')]);
    
                $decryptedPayload = Crypt::decrypt($laravelCookie, false);
    
                $sessionId = explode('|', $decryptedPayload)[0];
    
                $session->setId($sessionId);
                $session->start();
                
                $userId = $session->get(Auth::getName());
                if (!$userId) {
                    throw new \Exception("User not authenticated.");
                }
    
                $user = User::find($userId);
                if (!$user) {
                    throw new \Exception("User not found.");
                }
    
                $conn->user = $user;
                echo "User {$conn->user->id} ({$conn->user->name}) connected.\n";
    
            } catch (\Exception $e) {
                echo "Connection rejected: {$e->getMessage()}\n";
                $conn->close();
            }
        }
    
        public function onMessage(ConnectionInterface $from, MessageInterface $msg)
        {
            if (!isset($from->user)) {
                $from->close();
                return;
            }
    
            echo "Message from {$from->user->name}: {$msg}\n";
    
            foreach ($this->clients as $client) {
                if ($from !== $client && isset($client->user)) {
                    $client->send("{$from->user->name}: {$msg}");
                }
            }
        }
    
        public function onClose(ConnectionInterface $conn)
        {
            $this->clients->detach($conn);
            $userName = isset($conn->user) ? $conn->user->name : 'unauthenticated user';
            echo "Connection for {$userName} has disconnected.\n";
        }
    
        public function onError(ConnectionInterface $conn, \Exception $e)
        {
            echo "An error has occurred: {$e->getMessage()}\n";
            $conn->close();
        }
    }