Skip to content

Commit

Permalink
Sort providers via config
Browse files Browse the repository at this point in the history
  • Loading branch information
mbabker committed Feb 13, 2025
1 parent b02ffe4 commit 0d24b25
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 52 deletions.
15 changes: 2 additions & 13 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,14 @@ babdev_websocket:
firewalls: ['main'] # This can be an array to specify multiple firewalls or a string when specifying a single firewall
```

### Provider Priority

When providers are registered to the authenticator service, they are then used in a "first in, first out" order, meaning the order they are triggered will be the same order they are configured in. Assuming your application has multiple authenticators and you want a custom authenticator to be attempted before the session authenticator, you would use the below configuration to do so:

```yaml
babdev_websocket:
authentication:
providers:
custom: ~
session: ~
```

### Registering New Authenticators

In addition to creating a class implementing `BabDev\WebSocketBundle\Authentication\Provider\AuthenticationProvider`, you must also register the authenticator with a `BabDev\WebSocketBundle\DependencyInjection\Factory\Authentication\AuthenticationProviderFactory` to the bundle's container extension. Similar to factories used by Symfony's `SecurityBundle`, this factory is used to allow you to configure the authenticator for your application and build the authentication provider service.

A factory is required to have two methods:
A factory is required to have these methods:

- `getKey()` - A unique name to identify the provider in the application configuration, this name is used as the key in the `providers` list
- `getPriority()` - Defines the priority at which the authentication provider is called
- `addConfiguration()` - Defines the configuration nodes (if any are required) for the authenticator
- `createAuthenticationProvider()` - Registers the authentication provider service to the dependency injection container and returns the provider's service ID

Expand Down
46 changes: 41 additions & 5 deletions src/DependencyInjection/BabDevWebSocketExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,43 @@
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;

final class BabDevWebSocketExtension extends ConfigurableExtension
final class BabDevWebSocketExtension extends ConfigurableExtension implements PrependExtensionInterface
{
/**
* @var list<AuthenticationProviderFactory>
* @var list<array{int, AuthenticationProviderFactory}>
*/
private array $authenticationProviderFactories = [];

/**
* @var AuthenticationProviderFactory[]
*/
private array $sortedAuthenticationProviderFactories = [];

#[\Override]
public function prepend(ContainerBuilder $container): void
{
foreach ($this->getSortedAuthenticationProviderFactories() as $factory) {
if ($factory instanceof PrependExtensionInterface) {
$factory->prepend($container);
}
}
}

public function addAuthenticationProviderFactory(AuthenticationProviderFactory $factory): void
{
$this->authenticationProviderFactories[] = $factory;
$this->authenticationProviderFactories[] = [$factory->getPriority(), $factory];
$this->sortedAuthenticationProviderFactories = [];
}

#[\Override]
public function getConfiguration(array $config, ContainerBuilder $container): Configuration
{
return new Configuration($this->authenticationProviderFactories);
return new Configuration($this->getSortedAuthenticationProviderFactories());

Check failure on line 52 in src/DependencyInjection/BabDevWebSocketExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan

Parameter #1 $authenticationProviderFactories of class BabDev\WebSocketBundle\DependencyInjection\Configuration constructor expects list<BabDev\WebSocketBundle\DependencyInjection\Factory\Authentication\AuthenticationProviderFactory>, array<BabDev\WebSocketBundle\DependencyInjection\Factory\Authentication\AuthenticationProviderFactory> given.
}

#[\Override]
Expand Down Expand Up @@ -65,7 +82,7 @@ private function registerAuthenticationConfiguration(array $mergedConfig, Contai
$authenticators = [];

if (isset($mergedConfig['authentication']['providers'])) {
foreach ($this->authenticationProviderFactories as $factory) {
foreach ($this->getSortedAuthenticationProviderFactories() as $factory) {
$key = str_replace('-', '_', $factory->getKey());

if (!isset($mergedConfig['authentication']['providers'][$key])) {

Check failure on line 88 in src/DependencyInjection/BabDevWebSocketExtension.php

View workflow job for this annotation

GitHub Actions / PHPStan

Cannot access offset non-empty-string on mixed.
Expand Down Expand Up @@ -186,4 +203,23 @@ private function configureWebSocketSession(array $sessionConfig, ContainerBuilde
$container->removeDefinition('babdev_websocket_server.server.session.factory');
$container->removeDefinition('babdev_websocket_server.server.session.storage.factory.read_only_native');
}

/**
* @return AuthenticationProviderFactory[]
*/
private function getSortedAuthenticationProviderFactories(): array
{
if (!$this->sortedAuthenticationProviderFactories) {
$authenticationProviderFactories = [];
foreach ($this->authenticationProviderFactories as $i => $factory) {
$authenticationProviderFactories[] = array_merge($factory, [$i]);
}

usort($authenticationProviderFactories, static fn ($a, $b) => $b[0] <=> $a[0] ?: $a[2] <=> $b[2]);

$this->sortedAuthenticationProviderFactories = array_column($authenticationProviderFactories, 1);
}

return $this->sortedAuthenticationProviderFactories;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,23 @@
interface AuthenticationProviderFactory
{
/**
* Creates the authentication provider service for the provided configuration.
* Defines the configuration key used to reference the provider in the configuration.
*
* @return string The authentication provider service ID to be used
* @return non-empty-string
*/
public function createAuthenticationProvider(ContainerBuilder $container, array $config): string;
public function getKey(): string;

/**
* Defines the configuration key used to reference the provider in the configuration.
* Defines the priority at which the authentication provider is called.
*/
public function getKey(): string;
public function getPriority(): int;

public function addConfiguration(NodeDefinition $builder): void;

/**
* Creates the authentication provider service for the provided configuration.
*
* @return non-empty-string The authentication provider service ID to be used
*/
public function createAuthenticationProvider(ContainerBuilder $container, array $config): string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,64 @@
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Parameter;

final class SessionAuthenticationProviderFactory implements AuthenticationProviderFactory
{
/**
* Defines the configuration key used to reference the provider in the configuration.
*
* @return non-empty-string
*/
public function getKey(): string
{
return 'session';
}

/**
* Defines the priority at which the authentication provider is called.
*/
public function getPriority(): int
{
return 0;
}

public function addConfiguration(NodeDefinition $builder): void
{
$builder->children()
->variableNode('firewalls')
->defaultNull()
->info('The firewalls from which the session token can be used; can be an array, a string, or null to allow all firewalls.')
->validate()
->ifTrue(static fn ($firewalls): bool => !\is_array($firewalls) && !\is_string($firewalls) && null !== $firewalls)
->thenInvalid('The firewalls node must be an array, a string, or null')
->end()
->end()
;
}

/**
* Creates the authentication provider service for the provided configuration.
*
* @return string The authentication provider service ID to be used
* @param array{firewalls: list<non-empty-string>|non-empty-string|null} $config
*
* @return non-empty-string The authentication provider service ID to be used
*
* @throws InvalidArgumentException if the firewalls node is an invalid type
* @throws RuntimeException if the firewalls node is not configured and the "security.firewalls" container parameter is missing
* @throws RuntimeException if the firewalls node is not configured and the "security.firewalls" container parameter is missing
*/
public function createAuthenticationProvider(ContainerBuilder $container, array $config): string
{
if (\is_array($config['firewalls'])) {
$firewalls = $config['firewalls'];
} elseif (\is_string($config['firewalls'])) {
$firewalls = [$config['firewalls']];
} elseif (null === $config['firewalls']) {
} else {
if (!$container->hasParameter('security.firewalls')) {
throw new RuntimeException('The "firewalls" config for the session authentication provider is not set and the "security.firewalls" container parameter has not been set. Ensure the SecurityBundle is configured or set a list of firewalls to use.');
}

$firewalls = new Parameter('security.firewalls');
} else {
throw new InvalidArgumentException(\sprintf('The "firewalls" config must be an array, a string, or null; "%s" given.', get_debug_type($config['firewalls'])));
}

$providerId = 'babdev_websocket_server.authentication.provider.session.default';
Expand All @@ -42,26 +72,4 @@ public function createAuthenticationProvider(ContainerBuilder $container, array

return $providerId;
}

/**
* Defines the configuration key used to reference the provider in the configuration.
*/
public function getKey(): string
{
return 'session';
}

public function addConfiguration(NodeDefinition $builder): void
{
$builder->children()
->variableNode('firewalls')
->defaultNull()
->info('The firewalls from which the session token can be used; can be an array, a string, or null to allow all firewalls.')
->validate()
->ifTrue(static fn ($firewalls): bool => !\is_array($firewalls) && !\is_string($firewalls) && null !== $firewalls)
->thenInvalid('The firewalls node must be an array, a string, or null')
->end()
->end()
;
}
}

0 comments on commit 0d24b25

Please sign in to comment.