GeniusesOfSymfony / WebSocketBundle

:part_alternation_mark: Websocket server for Symfony applications (powered by Ratchet), includes a Autobahn.JS based JavaScript client
MIT License
608 stars 140 forks source link

JWT auth #225

Open ndoulgeridis opened 7 years ago

ndoulgeridis commented 7 years ago

Hello, is it possible to AUTH the user on socket connect with a JWT token? Something like socketio-jwt does.

I use lexik/LexikJWTAuthenticationBundle for JWT generation and security handling.

jjsaunier commented 7 years ago

Yes you can, you have to bring your own authenticationProvider, here is the default provided by this bundle : https://github.com/GeniusesOfSymfony/WebSocketBundle/blob/master/Client/Auth/WebsocketAuthenticationProvider.php (using shared session)

So you can create a jwt implementation as well.

jokari4242 commented 7 years ago

Hi;

I'm looking to use this functionnality but i don't know how i can do for create new authentificationProvider.. If you can help me it will be wonderfull! Actually :

I'm using FOSUserBundle, LexikJWTAuthenticationBundle.

When i'm on website, when i'm connection the websocket everything works good. User are authentificated :

[2017-03-10 06:30:18] websocket.INFO: xxx@xxx.xx connected {"connection_id":5029,"session_id":"191374500458c28e4a506de469327892","storage_id":5029}
Executed once every 5 seconds
[2017-03-10 06:30:21] websocket.DEBUG: GET CLIENT 5029

When i'm logged by the api (json web token), i have this :

[2017-03-10 06:30:31] websocket.INFO: anon-38388862858c28e5721e11572010159 disconnected {"connection_id":5106,"session_id":"38388862858c28e5721e11572010159","storage_id":5106,"username":"anon-38388862858c28e5721e11572010159"}

I have try to add api firewall in this config, but it's the same.

gos_web_socket:
    client:
        firewall: [main, api] #can be an array of firewalls
        session_handler: "@session.handler.pdo.service"
    server:
        port: "%gos_web_socket_port%"   #The port the socket server will listen on
        host: "%gos_web_socket_host%"   #The host ip to bind to
        router:
            resources:
                - "@jokari4242ChatBundle/Resources/config/routing.yml"
        origin_check: true
    topics:
        - "@chat_service"

Thanks for your help,

Antoine

rsaenen commented 7 years ago

Hi, I already have a custom auth provider, but how to link this with gosWebSocket ?

@jokari4242 If I find a solution, I will explain it on this issue.

alozytskyi commented 7 years ago

You would have to override gos_websocket.websocket_authentification.provider service in compiler pass completely to do that as it seems to be rather not replaceable by simple means of config.

stipic commented 5 years ago

@rsaenen can you show us how you solve this problem

rsaenen commented 5 years ago

@rsaenen can you show us how you solve this problem

Sorry I don't remember and I switched to node

astrocodespace commented 3 years ago

Hey guys, I was looking for solving of this case, and finally created my own implementation of JWT Auth with this bundle. I store my JWT Token in cookie and I can access it from Request object inside Connection.

I've created WebSocketJWTAuthProvider class which looks like:

<?php declare(strict_types=1);

namespace App\Shared\Providers;

use Gos\Bundle\WebSocketBundle\Client\Auth\WebsocketAuthenticationProviderInterface;
use Gos\Bundle\WebSocketBundle\Client\ClientStorageInterface;
use Illuminate\Support\Collection;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken;
use Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUserProvider;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProvider\JWSProviderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Ratchet\ConnectionInterface;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

final class WebSocketJWTAuthProvider implements WebsocketAuthenticationProviderInterface, LoggerAwareInterface
{
    use LoggerAwareTrait;

    /**
     * @var ClientStorageInterface
     */
    private ClientStorageInterface $clientStorage;

    /**
     * @var JWTTokenManagerInterface
     */
    private JWTTokenManagerInterface $tokenManager;

    /**
     * @var JWSProviderInterface
     */
    private JWSProviderInterface $JWSProvider;

    /**
     * @var UserProviderInterface|JWTUserProvider
     */
    private UserProviderInterface $jwtUserProvider;

    /**
     * @param ClientStorageInterface $clientStorage
     * @param JWTUserProvider $JWTUserProvider
     * @param JWSProviderInterface $JWSProvider
     */
    public function __construct(ClientStorageInterface $clientStorage,
                                UserProviderInterface $JWTUserProvider,
                                JWSProviderInterface $JWSProvider
    )
    {
        $this->clientStorage = $clientStorage;
        $this->JWSProvider = $JWSProvider;
        $this->jwtUserProvider = $JWTUserProvider;
    }

    public function authenticate(ConnectionInterface $conn): TokenInterface
    {
        if (1 === \count($this->firewalls) && 'ws_firewall' === $this->firewalls[0]) {
            if (null !== $this->logger) {
                $this->logger->warning(
                    sprintf(
                        'User firewall is not configured, we have set %s by default',
                        $this->firewalls[0]
                    )
                );
            }
        }

        $loggerContext = [
            'connection_id' => $conn->resourceId,
            'session_id' => $conn->WAMP->sessionId,
        ];

        $token = $this->getToken($conn);

        $identifier = $this->clientStorage->getStorageId($conn);

        $loggerContext['storage_id'] = $identifier;
        $this->clientStorage->addClient($identifier, $token);

        if (null !== $this->logger) {
            $this->logger->info(
                sprintf(
                    '%s connected',
                    $token->getUsername()
                ),
                $loggerContext
            );
        }

        return $token;
    }

    // @TODO: CLEAN UP HERE
    private function getToken(ConnectionInterface $connection): TokenInterface
    {
        $token = null;

        /** @var RequestInterface $req */
        $req = $connection->httpRequest;

        $cookies = explode('; ', $req->getHeader('Cookie')[0]);

        $cookiesCollection = new Collection();
        foreach ($cookies as $cookie) {
            $cookieArray = explode('=', $cookie);
            [$name, $value] = $cookieArray;
            $cookiesCollection->offsetSet($name, $value);
        }

        $rawToken = $cookiesCollection->get('BEARER');

        try {
            $jws = $this->JWSProvider->load($rawToken);
            $user = $this->jwtUserProvider->loadUserByUsername($jws->getPayload()['username']);
            $token = new JWTUserToken(
                user: $user,
                rawToken: $rawToken
            );
        } catch (JWTDecodeFailureException $decodeFailureException) {
            $token = new AnonymousToken(' main', 'anon-'.$connection->WAMP->sessionId);
        }

        return $token;
    }
}

in services.yaml:

    gos_web_socket.client.authentication.websocket_provider:
        class: App\Shared\Providers\WebSocketJWTAuthProvider
        public: false
        arguments:
            - '@gos_web_socket.client.storage'
        calls:
            - [ setLogger, ['@logger'] ]
        tags:
            - { name: monolog.logger, channel: websocket }

now you will be able to access your user in topic like this:

class ActivityStatus implements TopicInterface
{
    public function __construct(
        private MessageBusInterface $messageBus,
        private ClientManipulatorInterface $clientManipulator,
        private LoggerInterface $logger,
    )
    {

    }

    /**
     * This will receive any Subscription requests for this topic.
     *
     * @param ConnectionInterface $connection
     * @param Topic $topic
     * @param WampRequest $request
     *
     * @return void
     */
    public function onSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request)
    {
        /** @var JWTUserToken $user */
        $token = $this->clientManipulator->getClient($connection);

        if (empty($token->getUser())) {
            throw new FirewallRejectionException('User not authenticated to subscribe topic');
        }

        $this->messageBus->dispatch(
            new SetProfileOnlineCommand($token->getUser()->getId())
        );
    }

    /**
     *
     * @return string
     */
    public function getName(): string
    {
        return 'account.user.activity_status';
    }
}

I hope it helps someone who had similiar problem.

stipic commented 3 years ago

@astrocodespace thank you, it's help me. it's time for me to make some upgrades on my app

krajcikondra commented 3 years ago

Hey guys, I was looking for solving of this case, and finally created my own implementation of JWT Auth with this bundle. I store my JWT Token in cookie and I can access it from Request object inside Connection.

I've created WebSocketJWTAuthProvider class which looks like:

<?php declare(strict_types=1);

namespace App\Shared\Providers;

use Gos\Bundle\WebSocketBundle\Client\Auth\WebsocketAuthenticationProviderInterface;
use Gos\Bundle\WebSocketBundle\Client\ClientStorageInterface;
use Illuminate\Support\Collection;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken;
use Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUserProvider;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProvider\JWSProviderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Ratchet\ConnectionInterface;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

final class WebSocketJWTAuthProvider implements WebsocketAuthenticationProviderInterface, LoggerAwareInterface
{
    use LoggerAwareTrait;

    /**
     * @var ClientStorageInterface
     */
    private ClientStorageInterface $clientStorage;

    /**
     * @var JWTTokenManagerInterface
     */
    private JWTTokenManagerInterface $tokenManager;

    /**
     * @var JWSProviderInterface
     */
    private JWSProviderInterface $JWSProvider;

    /**
     * @var UserProviderInterface|JWTUserProvider
     */
    private UserProviderInterface $jwtUserProvider;

    /**
     * @param ClientStorageInterface $clientStorage
     * @param JWTUserProvider $JWTUserProvider
     * @param JWSProviderInterface $JWSProvider
     */
    public function __construct(ClientStorageInterface $clientStorage,
                                UserProviderInterface $JWTUserProvider,
                                JWSProviderInterface $JWSProvider
    )
    {
        $this->clientStorage = $clientStorage;
        $this->JWSProvider = $JWSProvider;
        $this->jwtUserProvider = $JWTUserProvider;
    }

    public function authenticate(ConnectionInterface $conn): TokenInterface
    {
        if (1 === \count($this->firewalls) && 'ws_firewall' === $this->firewalls[0]) {
            if (null !== $this->logger) {
                $this->logger->warning(
                    sprintf(
                        'User firewall is not configured, we have set %s by default',
                        $this->firewalls[0]
                    )
                );
            }
        }

        $loggerContext = [
            'connection_id' => $conn->resourceId,
            'session_id' => $conn->WAMP->sessionId,
        ];

        $token = $this->getToken($conn);

        $identifier = $this->clientStorage->getStorageId($conn);

        $loggerContext['storage_id'] = $identifier;
        $this->clientStorage->addClient($identifier, $token);

        if (null !== $this->logger) {
            $this->logger->info(
                sprintf(
                    '%s connected',
                    $token->getUsername()
                ),
                $loggerContext
            );
        }

        return $token;
    }

    // @TODO: CLEAN UP HERE
    private function getToken(ConnectionInterface $connection): TokenInterface
    {
        $token = null;

        /** @var RequestInterface $req */
        $req = $connection->httpRequest;

        $cookies = explode('; ', $req->getHeader('Cookie')[0]);

        $cookiesCollection = new Collection();
        foreach ($cookies as $cookie) {
            $cookieArray = explode('=', $cookie);
            [$name, $value] = $cookieArray;
            $cookiesCollection->offsetSet($name, $value);
        }

        $rawToken = $cookiesCollection->get('BEARER');

        try {
            $jws = $this->JWSProvider->load($rawToken);
            $user = $this->jwtUserProvider->loadUserByUsername($jws->getPayload()['username']);
            $token = new JWTUserToken(
                user: $user,
                rawToken: $rawToken
            );
        } catch (JWTDecodeFailureException $decodeFailureException) {
            $token = new AnonymousToken(' main', 'anon-'.$connection->WAMP->sessionId);
        }

        return $token;
    }
}

in services.yaml:

    gos_web_socket.client.authentication.websocket_provider:
        class: App\Shared\Providers\WebSocketJWTAuthProvider
        public: false
        arguments:
            - '@gos_web_socket.client.storage'
        calls:
            - [ setLogger, ['@logger'] ]
        tags:
            - { name: monolog.logger, channel: websocket }

now you will be able to access your user in topic like this:

class ActivityStatus implements TopicInterface
{
    public function __construct(
        private MessageBusInterface $messageBus,
        private ClientManipulatorInterface $clientManipulator,
        private LoggerInterface $logger,
    )
    {

    }

    /**
     * This will receive any Subscription requests for this topic.
     *
     * @param ConnectionInterface $connection
     * @param Topic $topic
     * @param WampRequest $request
     *
     * @return void
     */
    public function onSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request)
    {
        /** @var JWTUserToken $user */
        $token = $this->clientManipulator->getClient($connection);

        if (empty($token->getUser())) {
            throw new FirewallRejectionException('User not authenticated to subscribe topic');
        }

        $this->messageBus->dispatch(
            new SetProfileOnlineCommand($token->getUser()->getId())
        );
    }

    /**
     *
     * @return string
     */
    public function getName(): string
    {
        return 'account.user.activity_status';
    }
}

I hope it helps someone who had similiar problem.

Thank you very much for example.