Skip to content

Commit

Permalink
JWT token authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
fre5h authored May 8, 2020
1 parent a19c3c7 commit 02cfb96
Show file tree
Hide file tree
Showing 33 changed files with 1,924 additions and 41 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ env:
- PHPUNIT_FLAGS="-v"
jobs:
- SYMFONY_VERSION=5.0.*
- SYMFONY_VERSION=4.4.*

stages:
- composer validation
Expand Down
2 changes: 1 addition & 1 deletion Command/ChannelsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if (!empty($data['channels'])) {
$io->title('Channels');
$io->listing($data['channels']);
$io->newLine();
$io->text(\sprintf('<info>TOTAL</info>: %d', \count($data['channels'])));
} else {
$io->success('NO DATA');
}
Expand Down
21 changes: 21 additions & 0 deletions Command/PresenceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
foreach ($data['presence'] as $id => $info) {
$io->text(\sprintf('<info>%s</info>', $id));
$io->text(\sprintf(' ├ client: <comment>%s</comment>', $info['client']));
if (isset($info['conn_info'])) {
$io->text(' ├ conn_info:');
$io->write($this->formatConnInfo($info['conn_info']));
}
$io->text(\sprintf(' └ user: <comment>%s</comment>', $info['user']));
}

Expand All @@ -105,4 +109,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int

return 0;
}

/**
* @param array $connInfo
*
* @return string
*/
private function formatConnInfo(array $connInfo): string
{
$json = \json_encode($connInfo, \JSON_PRETTY_PRINT | \JSON_THROW_ON_ERROR);

$jsonWithPadding = '';
foreach (\explode("\n", $json) as $line) {
$jsonWithPadding .= \sprintf(" │ <comment>%s</comment>\n", $line);
}

return $jsonWithPadding;
}
}
20 changes: 18 additions & 2 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,25 @@ public function getConfigTreeBuilder(): TreeBuilder

$root
->children()
->scalarNode('channel_max_length')
->arrayNode('jwt')
->addDefaultsIfNotSet()
->children()
->enumNode('algorithm')
->values(['HS256', 'RSA'])
->defaultValue('HS256')
->info('JWT algorithm. At moment the only supported JWT algorithms are HMAC and RSA.')
->end()
->integerNode('ttl')
->min(0)
->defaultNull()
->info('TTL for JWT tokens in seconds.')
->end()
->end()
->end()
->integerNode('channel_max_length')
->min(1)
->defaultValue(255)
->info('Sets maximum length of channel name.')
->info('Maximum length of channel name.')
->end()
->end()
;
Expand Down
10 changes: 7 additions & 3 deletions DependencyInjection/FreshCentrifugoExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace Fresh\CentrifugoBundle\DependencyInjection;

use Fresh\CentrifugoBundle\Service\ChannelAuthenticator\ChannelAuthenticatorInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
Expand All @@ -29,11 +30,14 @@ class FreshCentrifugoExtension extends Extension
*/
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yaml');

$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter('centrifugo.channel_max_length', (int) $config['channel_max_length']);

$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yaml');
$container->setParameter('centrifugo.jwt.algorithm', (string) $config['jwt']['algorithm']);
$container->setParameter('centrifugo.jwt.ttl', $config['jwt']['ttl']);
$container->registerForAutoconfiguration(ChannelAuthenticatorInterface::class)->addTag('centrifugo.channel_authenticator');
}
}
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@
[![StyleCI](https://styleci.io/repos/164834807/shield?style=flat-square)](https://styleci.io/repos/164834807)
[![Gitter](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/fre5h/CentrifugoBundle)

## Features ⚙️
## Features 🎁

- [x] Compatible with latest [Centrifugo 2.4](https://github.com/centrifugal/centrifugo/releases/tag/v2.4.0) 🚀
- [x] Wrapper over [Centrifugo HTTP API](https://centrifugal.github.io/centrifugo/server/http_api/) 🧥
- [ ] @todo Json Web Token generation for anonymous, authenticated users and _private channels_ 🗝️
- [x] Wrapper over [Centrifugo HTTP API](https://centrifugal.github.io/centrifugo/server/http_api/) 🔌
- [X] Authentication with JWT token for [anonymous](./Resources/docs/authentication.md#anonymous), [authenticated user](./Resources/docs/authentication.md#authenticated-user) and [private channel](./Resources/docs/authentication.md#private-channel) 🗝️
- [x] [Batch request](./Resources/docs/centrifugo_service_methods.md#batch-request) in [JSON streaming format](https://en.wikipedia.org/wiki/JSON_streaming) 💪
- [x] [Console commands](./Resources/docs/console_commands.md "Console commands") ⚒️️
- [ ] @todo Integration into Symfony Web-Profiler 🎛️
- [ ] @todo Add RSA

## Requirements

* PHP 7.3 *and later*
* Symfony 4.4, 5.0 *and later*

## Installation 🌱

Expand Down Expand Up @@ -53,8 +59,12 @@ CENTRIFUGO_SECRET=secret
###< fresh/centrifugo-bundle ###
```

ℹ️ [Customize bundle configuration](./Resources/docs/configuration.md "Customize bundle configuration")

## Using 🧑‍🎓

### Centrifugo service

```php
<?php
declare(strict_types=1);
Expand All @@ -65,12 +75,8 @@ use Fresh\CentrifugoBundle\Service\Centrifugo;

class YourService
{
/** @var Centrifugo */
private $centrifugo;

/**
* @param Centrifugo $centrifugo
*/
public function __construct(Centrifugo $centrifugo)
{
$this->centrifugo = $centrifugo;
Expand All @@ -85,7 +91,13 @@ class YourService

ℹ️ [More examples of using Centrifugo service](./Resources/docs/centrifugo_service_methods.md "More examples of using Centrifugo service")

## Console commands ⚒️
### Authentication with JWT tokens 🗝️

* [For anonymous](./Resources/docs/authentication.md#anonymous)
* [For authenticated User](./Resources/docs/authentication.md#authenticated-user)
* [For private channel](./Resources/docs/authentication.md#private-channel)

### Console commands ⚒️

* `centrifugo:publish`
* `centrifugo:broadcast`
Expand Down
9 changes: 6 additions & 3 deletions Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ services:
public: false
bind:
$centrifugoChannelMaxLength: '%centrifugo.channel_max_length%'
$centrifugoJwtAlgorithm: '%centrifugo.jwt.algorithm%'
$centrifugoJwtTtl: '%centrifugo.jwt.ttl%'
$centrifugoSecret: '%env(CENTRIFUGO_SECRET)%'
iterable $channelAuthenticators: !tagged_iterator 'centrifugo.channel_authenticator'

Fresh\CentrifugoBundle\:
resource: '../../{Command,Logger,Service}/'

Fresh\CentrifugoBundle\Service\Centrifugo:
class: Fresh\CentrifugoBundle\Service\Centrifugo
arguments:
- '%env(APP_CENTRIFUGO_API_ENDPOINT)%'
- '%env(APP_CENTRIFUGO_API_KEY)%'
- '%env(APP_CENTRIFUGO_SECRET)%'
- '%env(CENTRIFUGO_API_ENDPOINT)%'
- '%env(CENTRIFUGO_API_KEY)%'
- '@http_client'
- '@Fresh\CentrifugoBundle\Service\ResponseProcessor'
- '@Fresh\CentrifugoBundle\Logger\CommandHistoryLogger'
Expand Down
197 changes: 197 additions & 0 deletions Resources/docs/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
🔝 [Back to index](./../../README.md "Back to index")

# Authentication with JWT token 🗝️️

## Anonymous

### Use `CredentialsGenerator` to receive an anonymous JWT token

```php
<?php
declare(strict_types=1);

namespace App\Controller;

use Fresh\CentrifugoBundle\Service\Credentials\CredentialsGenerator;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;

class CentrifugoAnonymousController
{
private $credentialsGenerator;

/**
* @Route("/centrifugo/credentials/anonymous", methods={"GET"}, name="get_centrifugo_credentials_for_anonymous")
*/
public function __construct(CredentialsGenerator $credentialsGenerator)
{
$this->credentialsGenerator = $credentialsGenerator;
}

public function getJwtTokenForAnonymousAction(): JsonResponse
{
$token = $this->credentialsGenerator->generateJwtTokenForAnonymous();

return new JsonResponse(['token' => $token], JsonResponse::HTTP_OK);
}
}
```

## Authenticated User

If in your Symfony application you have a `User` entity, then it should implement the [`UserInterface`](https://github.com/symfony/security-core/blob/master/User/UserInterface.php) interface.

To allow user be authenticated in Centrifugo, you **have to implement interface** [`CentrifugoUserInterface`](./../../User/CentrifugoUserInterface.php).
It has two methods: `getCentrifugoSubject()`, `getCentrifugoUserInfo()`. Which return information needed for JWT token claims.

### Implement `CentrifugoUserInterface` for your User entity

```php
<?php
declare(strict_types=1);

namespace App\Entity;

use Fresh\CentrifugoBundle\User\CentrifugoUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class User implements CentrifugoUserInterface, UserInterface
{
// ... implement methods from UserInterface

public function getCentrifugoSubject(): string
{
return $this->getUsername(); // or ->getId()
}

public function getCentrifugoUserInfo(): array
{
// User info is not required, you can return an empty array
// return [];

return [
'username' => $this->getUsername(), // Or some additional info, if you wish
];
}
}

```

### Use `CredentialsGenerator` to receive a JWT token for authenticated user

```php
<?php
declare(strict_types=1);

namespace App\Controller;

use Fresh\CentrifugoBundle\Service\Credentials\CredentialsGenerator;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Routing\Annotation\Route;

class CentrifugoCredentialsController
{
private $credentialsGenerator;
private $tokenStorage;

/**
* @Route("/centrifugo/credentials/user", methods={"GET"}, name="get_centrifugo_credentials_for_current_user")
*/
public function __construct(CredentialsGenerator $credentialsGenerator, TokenStorageInterface $tokenStorage)
{
$this->credentialsGenerator = $credentialsGenerator;
$this->tokenStorage = $tokenStorage;
}

public function getJwtTokenForCurrentUserAction(): JsonResponse
{
/** @var Fresh\CentrifugoBundle\User\CentrifugoUserInterface $user */
$user = $this->tokenStorage->getToken()->getUser();

// $user should be an instance of Fresh\CentrifugoBundle\User\CentrifugoUserInterface
$token = $this->credentialsGenerator->generateJwtTokenForUser($user);

return new JsonResponse(['token' => $token], JsonResponse::HTTP_OK);
}
}
```

## Private Channel

### Create own channel authenticator

This bundle provides possibility to register custom channel authenticators for private channels.
What you need is to create a service which implements [`ChannelAuthenticatorInterface`](./../../Service/ChannelAuthenticator/ChannelAuthenticatorInterface.php).

```php
<?php
declare(strict_types=1);

namespace App\Service\Centrifugo\ChannelAuthenticator;

use Fresh\CentrifugoBundle\Service\ChannelAuthenticator\ChannelAuthenticatorInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

class AdminChannelAuthenticator implements ChannelAuthenticatorInterface
{
private $authorizationChecker;

public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}

// This method is used to detect channels which are supported by this channel authenticator
public function supports(string $channel): bool
{
return 0 === \mb_strpos($channel, '$admins');
}

// This method is used to decide if current user is granted to access this private channel
public function hasAccessToChannel(string $channel): bool
{
return $this->authorizationChecker->isGranted('ROLE_ADMIN');
}
}
```

### Use `PrivateChannelAuthenticator` in your controller

```php
<?php

namespace App\Controller;

use Fresh\CentrifugoBundle\Service\ChannelAuthenticator\PrivateChannelAuthenticator;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class CentrifugoSubscribeController
{
private $privateChannelAuthenticator;

public function __construct(PrivateChannelAuthenticator $privateChannelAuthenticator)
{
$this->privateChannelAuthenticator = $privateChannelAuthenticator;
}

/**
* @Route("/centrifugo/subscribe", methods={"POST"}, name="centrifugo_subscribe")
*/
public function centrifugoSubscribeAction(Request $request): JsonResponse
{
$data = $this->privateChannelAuthenticator->authChannelsForClientFromRequest($request);

return new JsonResponse($data, JsonResponse::HTTP_OK);
}
}
```

## More features

* [Back to index](./../../README.md "Back to index")
* [Examples of using Centrifugo service](./centrifugo_service_methods.md "Examples of using Centrifugo service")
* [Examples of using console commands](./console_commands.md "Examples of using console commands")
* [Customize bundle configuration](./configuration.md "Customize bundle configuration")
2 changes: 2 additions & 0 deletions Resources/docs/centrifugo_service_methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,5 @@ $data = $this->centrifugo->batchRequest([$publish, $broadcast, $channels]);

* [Back to index](./../../README.md "Back to index")
* [Examples of using console commands](./console_commands.md "Examples of using console commands")
* [Authentication with JWT tokens](./authentication.md "Authentication with JWT tokens")
* [Customize bundle configuration](./configuration.md "Customize bundle configuration")
Loading

0 comments on commit 02cfb96

Please sign in to comment.