diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
index 1aa9117..cd6bcc4 100644
--- a/.php-cs-fixer.php
+++ b/.php-cs-fixer.php
@@ -101,5 +101,8 @@
'simplified_null_return' => true,
'single_line_empty_body' => true,
'static_lambda' => true,
+
+ // PhpUnit rules
+ 'php_unit_strict' => false,
])
;
diff --git a/composer.json b/composer.json
index 99e937c..f98e270 100644
--- a/composer.json
+++ b/composer.json
@@ -33,7 +33,12 @@
"require-dev": {
"doctrine/orm": "^2.3 || ^3",
"symfony/event-dispatcher": "^6.4",
- "symfony/phpunit-bridge": "^6.4"
+ "symfony/phpunit-bridge": "^6.4",
+ "symfony/test-pack": "^1.2",
+ "phpstan/phpstan": "^2.1",
+ "vimeo/psalm": "^6.13",
+ "friendsofphp/php-cs-fixer": "^3.84",
+ "psalm/plugin-symfony": "^5.2"
},
"autoload": {
"psr-4": {
diff --git a/phpstan.dist.neon b/phpstan.dist.neon
new file mode 100644
index 0000000..5c63723
--- /dev/null
+++ b/phpstan.dist.neon
@@ -0,0 +1,8 @@
+parameters:
+ level: 8
+ paths:
+ - src/
+ - tests/
+ excludePaths:
+ - src/DependencyInjection/Configuration.php
+ - src/DependencyInjection/Security/UserProvider/SamlUserProviderFactory.php
diff --git a/phpstan.neon b/phpstan.neon
deleted file mode 100644
index f73ea60..0000000
--- a/phpstan.neon
+++ /dev/null
@@ -1,10 +0,0 @@
-parameters:
- level: 9
- paths:
- - src
- - tests
- bootstrapFiles:
- - vendor/bin/.phpunit/phpunit/vendor/autoload.php
-
- checkMissingIterableValueType: false
- checkGenericClassInNonGenericObjectType: false
diff --git a/psalm.xml b/psalm.xml
index f6e3dd9..b358616 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -13,11 +13,13 @@
-
+
-
+
+
+
diff --git a/src/Controller/Login.php b/src/Controller/Login.php
index a9a0e0c..6639bd0 100644
--- a/src/Controller/Login.php
+++ b/src/Controller/Login.php
@@ -7,6 +7,7 @@
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\SamlAuthenticator;
use OneLogin\Saml2\Auth;
+use OneLogin\Saml2\Error as Saml2Exception;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -16,10 +17,10 @@
use Symfony\Component\Security\Http\SecurityRequestAttributes;
#[AsController]
-class Login
+readonly class Login
{
public function __construct(
- private readonly FirewallMap $firewallMap,
+ private FirewallMap $firewallMap,
) {}
public function __invoke(Request $request, Auth $auth): RedirectResponse
@@ -44,30 +45,38 @@ public function __invoke(Request $request, Auth $auth): RedirectResponse
throw new \RuntimeException($error->getMessage());
}
- return new RedirectResponse($this->processLoginAndGetRedirectUrl($auth, $targetPath, $session));
+ try {
+ return new RedirectResponse($this->processLoginAndGetRedirectUrl($auth, $targetPath, $session));
+ } catch (Saml2Exception $e) {
+ throw new \RuntimeException($e->getMessage());
+ }
}
- /** @psalm-suppress MixedInferredReturnType, MixedReturnStatement */
private function getTargetPath(Request $request, SessionInterface $session): ?string
{
$firewallName = $this->firewallMap->getFirewallConfig($request)?->getName();
- if (!$firewallName) {
+ if (null === $firewallName || '' === $firewallName) {
throw new ServiceUnavailableHttpException(message: 'Unknown firewall.');
}
- /** @phpstan-ignore-next-line */
- return $session->get('_security.'.$firewallName.'.target_path');
+ $attribute = (string) $session->get('_security.'.$firewallName.'.target_path');
+
+ return ($attribute === '') ? null : $attribute;
}
+ /**
+ * @throws Saml2Exception
+ */
private function processLoginAndGetRedirectUrl(Auth $auth, ?string $targetPath, ?SessionInterface $session): string
{
+ /** @var string|null $redirectUrl */
$redirectUrl = $auth->login(returnTo: $targetPath, stay: true);
- if ($redirectUrl === null) {
- throw new \RuntimeException('Login cannot be performed: Auth did not returned redirect url.');
+ if (null === $redirectUrl) {
+ throw new \RuntimeException('Unable to initiate SAML login process.');
}
$security = $auth->getSettings()->getSecurityData();
- if (($security['rejectUnsolicitedResponsesWithInResponseTo'] ?? false) && $session instanceof SessionInterface) {
+ if ((($security['rejectUnsolicitedResponsesWithInResponseTo'] ?? false) === true) && $session instanceof SessionInterface) {
$session->set(SamlAuthenticator::LAST_REQUEST_ID, $auth->getLastRequestID());
}
diff --git a/src/Controller/Logout.php b/src/Controller/Logout.php
index 0b722ab..d93a903 100644
--- a/src/Controller/Logout.php
+++ b/src/Controller/Logout.php
@@ -8,7 +8,7 @@
use Symfony\Component\HttpKernel\Attribute\AsController;
#[AsController]
-class Logout
+readonly class Logout
{
public function __invoke(): void
{
diff --git a/src/Controller/Metadata.php b/src/Controller/Metadata.php
index 41c3252..aab9060 100644
--- a/src/Controller/Metadata.php
+++ b/src/Controller/Metadata.php
@@ -6,12 +6,16 @@
namespace Nbgrp\OneloginSamlBundle\Controller;
use OneLogin\Saml2\Auth;
+use OneLogin\Saml2\Error as Saml2Exception;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
#[AsController]
-class Metadata
+readonly class Metadata
{
+ /**
+ * @throws Saml2Exception
+ */
public function __invoke(Auth $auth): Response
{
return new Response(
diff --git a/src/DependencyInjection/Compiler/AuthRegistryCompilerPass.php b/src/DependencyInjection/Compiler/AuthRegistryCompilerPass.php
index b6ee84f..a7b7cf0 100644
--- a/src/DependencyInjection/Compiler/AuthRegistryCompilerPass.php
+++ b/src/DependencyInjection/Compiler/AuthRegistryCompilerPass.php
@@ -18,6 +18,7 @@
*/
class AuthRegistryCompilerPass implements CompilerPassInterface
{
+ #[\Override]
public function process(ContainerBuilder $container): void
{
$authRegistry = $container->getDefinition(AuthRegistryInterface::class);
@@ -27,11 +28,31 @@ public function process(ContainerBuilder $container): void
throw new \UnexpectedValueException('OneLogin settings should be an array.');
}
- /** @var array $settings */
- foreach ($oneloginSettings as $key => $settings) {
- $authDefinition = new Definition(Auth::class, [$settings]);
- $authDefinition->setFactory(new Reference(AuthFactory::class));
- $authRegistry->addMethodCall('addService', [$key, $authDefinition]);
+ /** @var array $idpSettings */
+ foreach ($oneloginSettings as $idpKey => $idpSettings) {
+ /** @var array $settings */
+ $settings = $idpSettings;
+ if (isset($idpSettings['sp'])) {
+ if (!\is_array($idpSettings['sp'])) {
+ throw new \UnexpectedValueException('OneLogin SP settings should be an array.');
+ }
+ /** @var array|null $spSettings */
+ foreach ($idpSettings['sp'] as $spKey => $spSettings) {
+ if (!\is_array($spSettings)) {
+ throw new \UnexpectedValueException('OneLogin SP settings for key "'.(string) $spKey.'" should be an array.');
+ }
+
+ $settings['sp'] = $spSettings;
+ $authDefinition = new Definition(Auth::class, [$settings]);
+ $authDefinition->setFactory(new Reference(AuthFactory::class));
+ $authRegistry->addMethodCall('addService', [$idpKey, $spKey, $authDefinition]);
+ }
+ } else {
+ $settings['sp'] = 'default';
+ $authDefinition = new Definition(Auth::class, [$settings]);
+ $authDefinition->setFactory(new Reference(AuthFactory::class));
+ $authRegistry->addMethodCall('addService', [$idpKey, 'default', $authDefinition]);
+ }
}
}
}
diff --git a/src/DependencyInjection/Compiler/EntityManagerCompilerPass.php b/src/DependencyInjection/Compiler/EntityManagerCompilerPass.php
index 5ba02bf..ed7e588 100644
--- a/src/DependencyInjection/Compiler/EntityManagerCompilerPass.php
+++ b/src/DependencyInjection/Compiler/EntityManagerCompilerPass.php
@@ -14,6 +14,7 @@
*/
class EntityManagerCompilerPass implements CompilerPassInterface
{
+ #[\Override]
public function process(ContainerBuilder $container): void
{
if (!$container->hasParameter('nbgrp_onelogin_saml.entity_manager')) {
diff --git a/src/DependencyInjection/Compiler/ProxyVarsCompilerPass.php b/src/DependencyInjection/Compiler/ProxyVarsCompilerPass.php
index e55e5c1..b278c20 100644
--- a/src/DependencyInjection/Compiler/ProxyVarsCompilerPass.php
+++ b/src/DependencyInjection/Compiler/ProxyVarsCompilerPass.php
@@ -19,6 +19,7 @@
*/
class ProxyVarsCompilerPass implements CompilerPassInterface
{
+ #[\Override]
public function process(ContainerBuilder $container): void
{
$useProxyVars = $container->getParameter('nbgrp_onelogin_saml.use_proxy_vars');
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 933f2f3..488c3bb 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -1,10 +1,12 @@
getRootNode();
// @formatter:off
- /** @phpstan-ignore-next-line */
$rootNode
->info('nb:group OneLogin PHP Symfony Bundle configuration')
->children()
@@ -31,6 +35,27 @@ public function getConfigTreeBuilder(): TreeBuilder
->useAttributeAsKey('name')
->normalizeKeys(false)
->arrayPrototype()
+ ->beforeNormalization()
+ ->always(function ($v) {
+ // Si no hay 'sp', meter default vacĂo
+ if (!isset($v['sp']) || $v['sp'] === []) {
+ $v['sp'] = ['default' => []];
+ }
+
+ // Si 'sp' es un array plano (i.e. no es indexado por string con valores que son arrays)
+ if (isset($v['sp']) && is_array($v['sp'])) {
+ $isAssoc = static fn ($arr) => array_keys($arr) !== range(0, count($arr) - 1);
+ $isLikelyFlatSp = $isAssoc($v['sp']) && array_filter($v['sp'], 'is_array') === [];
+
+ if ($isLikelyFlatSp || isset($v['sp']['entityId'])) {
+ $v['sp'] = ['default' => $v['sp']];
+ }
+ }
+
+
+ return $v;
+ })
+ ->end()
->children()
->scalarNode('baseurl')
->defaultValue('/saml/')
@@ -87,67 +112,69 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->end()
->arrayNode('sp')
- ->addDefaultsIfNotSet()
- ->children()
- ->scalarNode('entityId')
- ->defaultValue('/saml/metadata')
- ->end()
- ->arrayNode('assertionConsumerService')
- ->addDefaultsIfNotSet()
- ->children()
- ->scalarNode('url')
- ->defaultValue('/saml/acs')
- ->end()
- ->scalarNode('binding')
- ->validate()
- ->ifTrue(static fn ($value): bool => !str_starts_with($value, 'urn:oasis:names:tc:SAML:2.0:bindings:'))
- ->thenInvalid('invalid value.')
+ ->useAttributeAsKey('name')
+ ->arrayPrototype()
+ ->children()
+ ->scalarNode('entityId')
+ ->defaultValue('/saml/metadata')
+ ->end()
+ ->arrayNode('assertionConsumerService')
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->scalarNode('url')
+ ->defaultValue('/saml/acs')
+ ->end()
+ ->scalarNode('binding')
+ ->validate()
+ ->ifTrue(static fn ($value): bool => !str_starts_with($value, 'urn:oasis:names:tc:SAML:2.0:bindings:'))
+ ->thenInvalid('invalid value.')
+ ->end()
->end()
->end()
->end()
- ->end()
- ->arrayNode('attributeConsumingService')
- ->children()
- ->scalarNode('serviceName')->end()
- ->scalarNode('serviceDescription')->end()
- ->arrayNode('requestedAttributes')
- ->arrayPrototype()
- ->children()
- ->scalarNode('name')->end()
- ->booleanNode('isRequired')
- ->defaultFalse()
+ ->arrayNode('attributeConsumingService')
+ ->children()
+ ->scalarNode('serviceName')->end()
+ ->scalarNode('serviceDescription')->end()
+ ->arrayNode('requestedAttributes')
+ ->arrayPrototype()
+ ->children()
+ ->scalarNode('name')->end()
+ ->booleanNode('isRequired')
+ ->defaultFalse()
+ ->end()
+ ->scalarNode('nameFormat')->end()
+ ->scalarNode('friendlyName')->end()
+ ->arrayNode('attributeValue')->end()
->end()
- ->scalarNode('nameFormat')->end()
- ->scalarNode('friendlyName')->end()
- ->arrayNode('attributeValue')->end()
->end()
->end()
->end()
->end()
- ->end()
- ->arrayNode('singleLogoutService')
- ->addDefaultsIfNotSet()
- ->children()
- ->scalarNode('url')
- ->defaultValue('/saml/logout')
- ->end()
- ->scalarNode('binding')
- ->validate()
- ->ifTrue(static fn ($value): bool => !str_starts_with($value, 'urn:oasis:names:tc:SAML:2.0:bindings:'))
- ->thenInvalid('invalid value.')
+ ->arrayNode('singleLogoutService')
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->scalarNode('url')
+ ->defaultValue('/saml/logout')
+ ->end()
+ ->scalarNode('binding')
+ ->validate()
+ ->ifTrue(static fn ($value): bool => !str_starts_with($value, 'urn:oasis:names:tc:SAML:2.0:bindings:'))
+ ->thenInvalid('invalid value.')
+ ->end()
->end()
->end()
->end()
- ->end()
- ->scalarNode('NameIDFormat')
- ->validate()
- ->ifTrue(static fn ($value): bool => !(str_starts_with($value, 'urn:oasis:names:tc:SAML:1.1:nameid-format:') || str_starts_with($value, 'urn:oasis:names:tc:SAML:2.0:nameid-format:')))
- ->thenInvalid('invalid value.')
+ ->scalarNode('NameIDFormat')
+ ->validate()
+ ->ifTrue(static fn ($value): bool => !(str_starts_with($value, 'urn:oasis:names:tc:SAML:1.1:nameid-format:') || str_starts_with($value, 'urn:oasis:names:tc:SAML:2.0:nameid-format:')))
+ ->thenInvalid('invalid value.')
+ ->end()
->end()
+ ->scalarNode('x509cert')->end()
+ ->scalarNode('privateKey')->end()
+ ->scalarNode('x509certNew')->end()
->end()
- ->scalarNode('x509cert')->end()
- ->scalarNode('privateKey')->end()
- ->scalarNode('x509certNew')->end()
->end()
->end()
->arrayNode('compress')
@@ -174,7 +201,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->thenInvalid('must be an array or a boolean.')
->end()
->validate()
- ->ifTrue(static fn ($value) => \is_array($value) && array_filter($value, static fn ($item): bool => !str_starts_with($item, 'urn:oasis:names:tc:SAML:2.0:ac:classes:')))
+ ->ifTrue(static fn ($value) => \is_array($value) && \count(array_filter($value, static fn ($item): bool => !str_starts_with($item, 'urn:oasis:names:tc:SAML:2.0:ac:classes:'))) > 0)
->thenInvalid('invalid value.')
->end()
->end()
@@ -285,7 +312,9 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->end()
->validate()
- ->ifTrue(static fn ($value): bool => empty($value['organization']))
+ ->ifTrue(
+ static fn ($value): bool => !isset($value['organization']) || [] === $value['organization']
+ )
->then(static fn ($value): array => array_diff_key($value, ['organization' => null]))
->end()
->end()
@@ -297,13 +326,15 @@ public function getConfigTreeBuilder(): TreeBuilder
->cannotBeEmpty()
->defaultValue('idp')
->end()
+ ->scalarNode('sp_parameter_name')
+ ->cannotBeEmpty()
+ ->defaultValue('sp')
+ ->end()
->scalarNode('entity_manager_name')
->cannotBeEmpty()
->end()
->end()
;
- // @formatter:on
-
return $treeBuilder;
}
}
diff --git a/src/DependencyInjection/NbgrpOneloginSamlExtension.php b/src/DependencyInjection/NbgrpOneloginSamlExtension.php
index 7582d42..f117f32 100644
--- a/src/DependencyInjection/NbgrpOneloginSamlExtension.php
+++ b/src/DependencyInjection/NbgrpOneloginSamlExtension.php
@@ -12,11 +12,13 @@
/**
* @internal
+ *
* @final
*/
class NbgrpOneloginSamlExtension extends Extension
{
/** @psalm-suppress MixedArgument */
+ #[\Override]
public function load(array $configs, ContainerBuilder $container): void
{
$loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config'));
@@ -27,6 +29,7 @@ public function load(array $configs, ContainerBuilder $container): void
$container->setParameter('nbgrp_onelogin_saml.onelogin_settings', $config['onelogin_settings']);
$container->setParameter('nbgrp_onelogin_saml.use_proxy_vars', $config['use_proxy_vars']);
$container->setParameter('nbgrp_onelogin_saml.idp_parameter_name', $config['idp_parameter_name']);
+ $container->setParameter('nbgrp_onelogin_saml.sp_parameter_name', $config['sp_parameter_name']);
if (\array_key_exists('entity_manager_name', $config)) {
$container->setParameter('nbgrp_onelogin_saml.entity_manager', $config['entity_manager_name']);
diff --git a/src/DependencyInjection/Security/Factory/SamlFactory.php b/src/DependencyInjection/Security/Factory/SamlFactory.php
index 756aec8..eaf6cba 100644
--- a/src/DependencyInjection/Security/Factory/SamlFactory.php
+++ b/src/DependencyInjection/Security/Factory/SamlFactory.php
@@ -30,17 +30,20 @@ public function __construct()
$this->addOption('success_handler', SamlAuthenticationSuccessHandler::class);
}
+ #[\Override]
public function getPriority(): int
{
return self::PRIORITY;
}
+ #[\Override]
public function getKey(): string
{
return 'saml';
}
/** @psalm-suppress MixedArgument */
+ #[\Override]
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
{
$authenticatorId = 'security.authenticator.saml.'.$firewallName;
@@ -50,9 +53,7 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
->replaceArgument(5, new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config)))
->replaceArgument(6, array_intersect_key($config, $this->options))
;
-
- if (!empty($config['user_factory'])) {
- /** @phpstan-ignore-next-line */
+ if (isset($config['user_factory']) && '' !== (string) $config['user_factory'] && [] !== $config['user_factory']) {
$authenticator->replaceArgument(7, new Reference((string) $config['user_factory']));
}
@@ -63,6 +64,11 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
return $authenticatorId;
}
+ /**
+ * Crea los listeners de usuario para el firewall SAML.
+ *
+ * @param array $config
+ */
protected function createUserListeners(ContainerBuilder $container, string $firewallName, array $config): void
{
$container->setDefinition('nbgrp_onelogin_saml.user_created_listener.'.$firewallName, new ChildDefinition(UserCreatedListener::class))
diff --git a/src/DependencyInjection/Security/UserProvider/SamlUserProviderFactory.php b/src/DependencyInjection/Security/UserProvider/SamlUserProviderFactory.php
index 7e7f10b..fbb3f70 100644
--- a/src/DependencyInjection/Security/UserProvider/SamlUserProviderFactory.php
+++ b/src/DependencyInjection/Security/UserProvider/SamlUserProviderFactory.php
@@ -1,4 +1,5 @@
children()
->scalarNode('user_class')
diff --git a/src/EventListener/Security/SamlLogoutListener.php b/src/EventListener/Security/SamlLogoutListener.php
index 5ef8be9..7e88a92 100644
--- a/src/EventListener/Security/SamlLogoutListener.php
+++ b/src/EventListener/Security/SamlLogoutListener.php
@@ -10,27 +10,26 @@
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\SamlAuthenticator;
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken;
use OneLogin\Saml2\Auth;
+use OneLogin\Saml2\Error;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
+use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Event\LogoutEvent;
/**
* Process Single Logout by current OneLogin Auth service on user logout.
*/
-final class SamlLogoutListener
+final readonly class SamlLogoutListener
{
public function __construct(
- private readonly AuthRegistryInterface $authRegistry,
- private readonly IdpResolverInterface $idpResolver,
+ private AuthRegistryInterface $authRegistry,
+ private IdpResolverInterface $idpResolver,
) {}
#[AsEventListener(LogoutEvent::class)]
public function processSingleLogout(LogoutEvent $event): void
{
$authService = $this->getAuthService($event->getRequest());
- if (!$authService) {
- return;
- }
$token = $event->getToken();
if (!$token instanceof SamlToken) {
@@ -39,28 +38,33 @@ public function processSingleLogout(LogoutEvent $event): void
try {
$authService->processSLO();
- } catch (\OneLogin\Saml2\Error) {
- if (!empty($authService->getSLOurl())) {
+ } catch (Error) {
+ $sloUrl = $authService->getSLOurl();
+ if (null !== $sloUrl && '' !== $sloUrl) {
/** @var string|null $sessionIndex */
$sessionIndex = $token->hasAttribute(SamlAuthenticator::SESSION_INDEX_ATTRIBUTE)
? $token->getAttribute(SamlAuthenticator::SESSION_INDEX_ATTRIBUTE)
: null;
- $authService->logout(null, [], $token->getUserIdentifier(), $sessionIndex);
+
+ try {
+ $authService->logout(null, [], $token->getUserIdentifier(), $sessionIndex);
+ } catch (Error $e) {
+ // If logout fails, we can still redirect to SLO URL.
+ // This is useful for IdPs that do not support SLO.
+ $event->setResponse(new RedirectResponse($sloUrl));
+ }
}
}
}
- private function getAuthService(Request $request): ?Auth
+ private function getAuthService(Request $request): Auth
{
- $idp = $this->idpResolver->resolve($request);
- if (!$idp) {
- return $this->authRegistry->getDefaultService();
- }
-
- if ($this->authRegistry->hasService($idp)) {
- return $this->authRegistry->getService($idp);
- }
+ $resolve = $this->idpResolver->resolve($request);
+ $idp = $resolve['idp'];
+ $sp = $resolve['sp'];
- return null;
+ return (('' !== $idp) && ('' !== $sp))
+ ? $this->authRegistry->getService($idp, $sp)
+ : $this->authRegistry->getDefaultService();
}
}
diff --git a/src/EventListener/User/DeferredUserListener.php b/src/EventListener/User/DeferredUserListener.php
index 816403b..0cbda5a 100644
--- a/src/EventListener/User/DeferredUserListener.php
+++ b/src/EventListener/User/DeferredUserListener.php
@@ -21,7 +21,7 @@ public function dispatchDeferredEvent(CheckPassportEvent $event, string $eventNa
}
$deferredEvent = $badge->getEvent();
- if ($deferredEvent) {
+ if (null !== $deferredEvent) {
$eventDispatcher->dispatch($deferredEvent);
}
}
diff --git a/src/Idp/IdpResolver.php b/src/Idp/IdpResolver.php
index c694941..af62cb4 100644
--- a/src/Idp/IdpResolver.php
+++ b/src/Idp/IdpResolver.php
@@ -7,23 +7,30 @@
use Symfony\Component\HttpFoundation\Request;
-final class IdpResolver implements IdpResolverInterface
+final readonly class IdpResolver implements IdpResolverInterface
{
public function __construct(
- private readonly string $idpParameterName,
+ private string $idpParameterName,
+ private string $spParameterName,
) {}
- public function resolve(Request $request): ?string
+ /**
+ * {@inheritdoc}
+ */
+ #[\Override]
+ public function resolve(Request $request): array
{
- if ($request->query->has($this->idpParameterName)) {
- return (string) $request->query->get($this->idpParameterName);
- }
+ // Get IdP and SP names from query parameters or request attributes
+ $queryIdp = (string) $request->query->get($this->idpParameterName);
+ $querySp = (string) $request->query->get($this->spParameterName);
+ $attributesIdp = (string) $request->attributes->get($this->idpParameterName, 'default');
+ $attributesSp = (string) $request->attributes->get($this->spParameterName, 'default');
- if ($request->attributes->has($this->idpParameterName)) {
- /** @phpstan-ignore-next-line */
- return (string) $request->attributes->get($this->idpParameterName);
- }
+ // If query parameters are not set, use attributes
+ $idp = '' !== $queryIdp ? $queryIdp : $attributesIdp;
+ $sp = '' !== $querySp ? $querySp : $attributesSp;
- return null;
+ // If both IdP and SP are not set, return default values
+ return ['idp' => $idp, 'sp' => $sp];
}
}
diff --git a/src/Idp/IdpResolverInterface.php b/src/Idp/IdpResolverInterface.php
index 0328107..8d5c5b0 100644
--- a/src/Idp/IdpResolverInterface.php
+++ b/src/Idp/IdpResolverInterface.php
@@ -13,7 +13,11 @@
interface IdpResolverInterface
{
/**
- * Returns IdP name for specified request.
+ * Returns IdP name and Sp name for specified request.
+ *
+ * @param Request $request the request to resolve IdP for
+ *
+ * @return array{idp:string, sp:string} returns an array with IdP name and Sp name if they are found, or null if not
*/
- public function resolve(Request $request): ?string;
+ public function resolve(Request $request): array;
}
diff --git a/src/NbgrpOneloginSamlBundle.php b/src/NbgrpOneloginSamlBundle.php
index 0f4b1e6..be78c7c 100644
--- a/src/NbgrpOneloginSamlBundle.php
+++ b/src/NbgrpOneloginSamlBundle.php
@@ -18,6 +18,7 @@
*/
class NbgrpOneloginSamlBundle extends Bundle
{
+ #[\Override]
public function build(ContainerBuilder $container): void
{
parent::build($container);
diff --git a/src/Onelogin/AuthArgumentResolver.php b/src/Onelogin/AuthArgumentResolver.php
index 52aafde..b272bd9 100644
--- a/src/Onelogin/AuthArgumentResolver.php
+++ b/src/Onelogin/AuthArgumentResolver.php
@@ -11,36 +11,34 @@
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
-use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
* Yields the OneLogin Auth instance for current request
* (default or according to an idp parameter).
*/
-final class AuthArgumentResolver implements ValueResolverInterface
+final readonly class AuthArgumentResolver implements ValueResolverInterface
{
public function __construct(
- private readonly AuthRegistryInterface $authRegistry,
- private readonly IdpResolverInterface $idpResolver,
+ private AuthRegistryInterface $authRegistry,
+ private IdpResolverInterface $idpResolver,
) {}
+ /** @phpstan-ignore-next-line */
+ #[\Override]
public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
- if ($argument->getType() !== Auth::class) {
+ if (Auth::class !== $argument->getType()) {
return [];
}
- $idp = $this->idpResolver->resolve($request);
- if ($idp && !$this->authRegistry->hasService($idp)) {
- throw new BadRequestHttpException('There is no OneLogin PHP toolkit settings for IdP "'.$idp.'". See nbgrp_onelogin_saml config ("onelogin_settings" section).');
- }
-
- try {
- yield $idp
- ? $this->authRegistry->getService($idp)
- : $this->authRegistry->getDefaultService();
- } catch (\RuntimeException $exception) {
- throw new ServiceUnavailableHttpException($exception->getMessage());
+ $resolve = $this->idpResolver->resolve($request);
+ $idp = $resolve['idp'];
+ $sp = $resolve['sp'];
+ if (!$this->authRegistry->hasService($idp, $sp)) {
+ throw new BadRequestHttpException('There is no OneLogin PHP toolkit settings for IdP "'.$idp.'" and SP "'.$sp.'". See nbgrp_onelogin_saml config ("onelogin_settings" section).');
}
+ yield ('default' !== $idp && 'default' !== $sp)
+ ? $this->authRegistry->getService($idp, $sp)
+ : $this->authRegistry->getDefaultService();
}
}
diff --git a/src/Onelogin/AuthFactory.php b/src/Onelogin/AuthFactory.php
index 247eabb..438c761 100644
--- a/src/Onelogin/AuthFactory.php
+++ b/src/Onelogin/AuthFactory.php
@@ -6,6 +6,7 @@
namespace Nbgrp\OneloginSamlBundle\Onelogin;
use OneLogin\Saml2\Auth;
+use OneLogin\Saml2\Error;
use Symfony\Component\HttpFoundation\RequestStack;
final class AuthFactory
@@ -16,6 +17,13 @@ public function __construct(
private readonly RequestStack $requestStack,
) {}
+ /**
+ * Create a new OneLogin Auth instance with settings.
+ *
+ * @param array $settings
+ *
+ * @throws Error
+ */
public function __invoke(array $settings): Auth
{
$request = $this->requestStack->getMainRequest();
@@ -28,24 +36,65 @@ public function __invoke(array $settings): Auth
}
/**
- * @psalm-suppress MixedArrayAssignment, MixedArrayAccess
+ * Replace the scheme and host placeholder in the settings array.
+ *
+ * @param array $settings
+ *
+ * @return array
*/
private static function replaceSchemeAndHostPlaceholder(array $settings, string $replace): array
{
if (isset($settings['baseurl'])) {
- $settings['baseurl'] = str_replace(self::SCHEME_AND_HOST_PLACEHOLDER, $replace, (string) $settings['baseurl']);
+ $settings['baseurl'] = str_replace(
+ self::SCHEME_AND_HOST_PLACEHOLDER,
+ $replace,
+ (string) $settings['baseurl']
+ );
}
- if (isset($settings['sp']['entityId'])) {
- $settings['sp']['entityId'] = str_replace(self::SCHEME_AND_HOST_PLACEHOLDER, $replace, (string) $settings['sp']['entityId']);
- }
+ if (isset($settings['sp']) && \is_array($settings['sp'])) {
+ /** @var array $sp */
+ $sp = $settings['sp'];
- if (isset($settings['sp']['assertionConsumerService']['url'])) {
- $settings['sp']['assertionConsumerService']['url'] = str_replace(self::SCHEME_AND_HOST_PLACEHOLDER, $replace, (string) $settings['sp']['assertionConsumerService']['url']); // @phan-suppress-current-line PhanTypeArraySuspiciousNull, PhanTypeInvalidDimOffset
- }
+ if (isset($sp['entityId'])) {
+ $sp['entityId'] = str_replace(
+ self::SCHEME_AND_HOST_PLACEHOLDER,
+ $replace,
+ (string) $sp['entityId']
+ );
+ }
+
+ if (isset($sp['assertionConsumerService']) && \is_array($sp['assertionConsumerService'])) {
+ /** @var array $acs */
+ $acs = $sp['assertionConsumerService'];
+
+ if (isset($acs['url'])) {
+ $acs['url'] = str_replace(
+ self::SCHEME_AND_HOST_PLACEHOLDER,
+ $replace,
+ (string) $acs['url']
+ );
+ }
+
+ $sp['assertionConsumerService'] = $acs;
+ }
+
+ if (isset($sp['singleLogoutService']) && \is_array($sp['singleLogoutService'])) {
+ /** @var array $sls */
+ $sls = $sp['singleLogoutService'];
+
+ if (isset($sls['url'])) {
+ $sls['url'] = str_replace(
+ self::SCHEME_AND_HOST_PLACEHOLDER,
+ $replace,
+ (string) $sls['url']
+ );
+ }
+
+ $sp['singleLogoutService'] = $sls;
+ }
- if (isset($settings['sp']['singleLogoutService']['url'])) {
- $settings['sp']['singleLogoutService']['url'] = str_replace(self::SCHEME_AND_HOST_PLACEHOLDER, $replace, (string) $settings['sp']['singleLogoutService']['url']); // @phan-suppress-current-line PhanTypeArraySuspiciousNull, PhanTypeInvalidDimOffset
+ $settings['sp'] = $sp;
}
return $settings;
diff --git a/src/Onelogin/AuthRegistry.php b/src/Onelogin/AuthRegistry.php
index ed171a3..60d6531 100644
--- a/src/Onelogin/AuthRegistry.php
+++ b/src/Onelogin/AuthRegistry.php
@@ -10,37 +10,59 @@
final class AuthRegistry implements AuthRegistryInterface
{
/**
- * @var array
+ * @var array>
*/
private array $services = [];
- public function addService(string $key, Auth $auth): self
+ #[\Override]
+ public function addService(string $idpKey, string $spKey, Auth $auth): self
{
- if (\array_key_exists($key, $this->services)) {
- throw new \OverflowException('Auth service with key "'.$key.'" already exists.');
+ if (\array_key_exists($idpKey, $this->services)) {
+ if (\array_key_exists($spKey, $this->services[$idpKey])) {
+ throw new \OverflowException('Auth service with key "'.$spKey.'" already exists.');
+ }
}
- $this->services[$key] = $auth;
+ $this->services[$idpKey][$spKey] = $auth;
return $this;
}
- public function hasService(string $key): bool
+ #[\Override]
+ public function hasService(string $idpKey, string $spKey): bool
{
- return \array_key_exists($key, $this->services);
+ if (\array_key_exists($idpKey, $this->services)) {
+ if (\array_key_exists($spKey, $this->services[$idpKey])) {
+ return true;
+ }
+ }
+
+ return false;
}
- public function getService(string $key): Auth
+ /**
+ * {@inheritdoc}
+ */
+ #[\Override]
+ public function getService(string $idpKey, string $spKey): Auth
{
- return $this->services[$key] ?? throw new \OutOfBoundsException('Auth service for key "'.$key.'" does not exists.');
+ return $this->services[$idpKey][$spKey] ?? throw new \OutOfBoundsException('Auth service for keys "'.$idpKey.' '.$spKey.'" does not exists.');
}
+ #[\Override]
public function getDefaultService(): Auth
{
- if (empty($this->services)) {
+ if ([] === $this->services) {
+ throw new \UnderflowException('There is no configured Auth services.');
+ }
+
+ $firstIdp = reset($this->services);
+ $firstAuth = reset($firstIdp);
+
+ if (!$firstAuth instanceof Auth) {
throw new \UnderflowException('There is no configured Auth services.');
}
- return reset($this->services);
+ return $firstAuth;
}
}
diff --git a/src/Onelogin/AuthRegistryInterface.php b/src/Onelogin/AuthRegistryInterface.php
index 7bbfa87..1d5988d 100644
--- a/src/Onelogin/AuthRegistryInterface.php
+++ b/src/Onelogin/AuthRegistryInterface.php
@@ -12,11 +12,16 @@
*/
interface AuthRegistryInterface
{
- public function addService(string $key, Auth $auth): self;
+ public function addService(string $idpKey, string $spKey, Auth $auth): self;
- public function hasService(string $key): bool;
+ public function hasService(string $idpKey, string $spKey): bool;
- public function getService(string $key): Auth;
+ /**
+ * Get the Auth service for the given IdP and SP keys.
+ *
+ * @throws \OutOfBoundsException if the service does not exist
+ */
+ public function getService(string $idpKey, string $spKey): Auth;
public function getDefaultService(): Auth;
}
diff --git a/src/Resources/config/routes.php b/src/Resources/config/routes.php
index 149993e..46954f8 100644
--- a/src/Resources/config/routes.php
+++ b/src/Resources/config/routes.php
@@ -6,28 +6,30 @@
use Nbgrp\OneloginSamlBundle\Controller;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
-/**
- * @suppress PhanUndeclaredFunctionInCallable
- */
+// @suppress PhanUndeclaredFunctionInCallable
return static function (RoutingConfigurator $routes): void {
$routes->add('saml_metadata', '/saml/metadata')
->controller(Controller\Metadata::class)
->defaults(['idp' => null])
+ ->defaults(['sp' => null])
;
$routes->add('saml_acs', '/saml/acs')
->controller(Controller\AssertionConsumerService::class)
->defaults(['idp' => null])
+ ->defaults(['sp' => null])
->methods(['POST'])
;
$routes->add('saml_login', '/saml/login')
->controller(Controller\Login::class)
->defaults(['idp' => null])
+ ->defaults(['sp' => null])
;
$routes->add('saml_logout', '/saml/logout')
->controller(Controller\Logout::class)
->defaults(['idp' => null])
+ ->defaults(['sp' => null])
;
};
diff --git a/src/Resources/config/services.php b/src/Resources/config/services.php
index d87b5e3..f977cfd 100644
--- a/src/Resources/config/services.php
+++ b/src/Resources/config/services.php
@@ -1,4 +1,5 @@
set(Idp\IdpResolverInterface::class, Idp\IdpResolver::class)
->args([
param('nbgrp_onelogin_saml.idp_parameter_name'),
+ param('nbgrp_onelogin_saml.sp_parameter_name'),
])
->set(Onelogin\AuthArgumentResolver::class)
@@ -85,7 +87,8 @@
/* 7 */ null, // user factory
/* 8 */ service(LoggerInterface::class)->nullOnInvalid(),
/* 9 */ param('nbgrp_onelogin_saml.idp_parameter_name'),
- /* 10 */ param('nbgrp_onelogin_saml.use_proxy_vars'),
+ /* 10 */ param('nbgrp_onelogin_saml.sp_parameter_name'),
+ /* 11 */ param('nbgrp_onelogin_saml.use_proxy_vars'),
])
;
};
diff --git a/src/Security/Http/Authentication/SamlAuthenticationSuccessHandler.php b/src/Security/Http/Authentication/SamlAuthenticationSuccessHandler.php
index 9209907..8844923 100644
--- a/src/Security/Http/Authentication/SamlAuthenticationSuccessHandler.php
+++ b/src/Security/Http/Authentication/SamlAuthenticationSuccessHandler.php
@@ -18,19 +18,20 @@ class SamlAuthenticationSuccessHandler extends DefaultAuthenticationSuccessHandl
public const RELAY_STATE = 'RelayState';
/** @psalm-suppress MixedArrayAccess */
+ #[\Override]
protected function determineTargetUrl(Request $request): string
{
if ($this->options['always_use_default_target_path']) {
return (string) $this->options['default_target_path'];
}
- /** @psalm-suppress InvalidArgument */
- $relayState = $request->query->get(self::RELAY_STATE, $request->request->get(self::RELAY_STATE));
- if ($relayState !== null && $this->httpUtils instanceof HttpUtils) {
- /** @psalm-suppress RedundantCastGivenDocblockType */
- $relayState = (string) $relayState;
+ $relayState = $request->query->get(self::RELAY_STATE, '');
+ if ('' === $relayState) {
+ $relayState = $request->request->get(self::RELAY_STATE, '');
+ }
+ if ('' !== $relayState && $this->httpUtils instanceof HttpUtils) {
if ($relayState !== $this->httpUtils->generateUri($request, (string) $this->options['login_path'])) {
- return $relayState;
+ return (string) $relayState;
}
}
diff --git a/src/Security/Http/Authenticator/Passport/Badge/DeferredEventBadge.php b/src/Security/Http/Authenticator/Passport/Badge/DeferredEventBadge.php
index 076d674..8058904 100644
--- a/src/Security/Http/Authenticator/Passport/Badge/DeferredEventBadge.php
+++ b/src/Security/Http/Authenticator/Passport/Badge/DeferredEventBadge.php
@@ -30,6 +30,7 @@ public function getEvent(): ?Event
}
}
+ #[\Override]
public function isResolved(): bool
{
return $this->resolved;
diff --git a/src/Security/Http/Authenticator/Passport/Badge/SamlAttributesBadge.php b/src/Security/Http/Authenticator/Passport/Badge/SamlAttributesBadge.php
index 5148024..ea9069d 100644
--- a/src/Security/Http/Authenticator/Passport/Badge/SamlAttributesBadge.php
+++ b/src/Security/Http/Authenticator/Passport/Badge/SamlAttributesBadge.php
@@ -10,17 +10,24 @@
/**
* Allows to add SAML attributes to a passport.
*/
-class SamlAttributesBadge implements BadgeInterface
+readonly class SamlAttributesBadge implements BadgeInterface
{
+ /**
+ * @param array $attributes
+ */
public function __construct(
- private readonly array $attributes,
+ private array $attributes,
) {}
+ /**
+ * @return array
+ */
public function getAttributes(): array
{
return $this->attributes;
}
+ #[\Override]
public function isResolved(): bool
{
return true;
diff --git a/src/Security/Http/Authenticator/SamlAuthenticator.php b/src/Security/Http/Authenticator/SamlAuthenticator.php
index 83772c3..c814dfe 100644
--- a/src/Security/Http/Authenticator/SamlAuthenticator.php
+++ b/src/Security/Http/Authenticator/SamlAuthenticator.php
@@ -28,6 +28,7 @@
use Symfony\Component\Security\Core\Exception\LogicException;
use Symfony\Component\Security\Core\Exception\SessionUnavailableException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
+use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
@@ -44,6 +45,10 @@ class SamlAuthenticator implements AuthenticatorInterface, AuthenticationEntryPo
public const SESSION_INDEX_ATTRIBUTE = '_saml_session_index';
public const LAST_REQUEST_ID = '_saml_last_request_id';
+ /**
+ * @param UserProviderInterface $userProvider
+ * @param array $options
+ */
public function __construct(
private readonly HttpUtils $httpUtils,
private readonly UserProviderInterface $userProvider,
@@ -55,26 +60,30 @@ public function __construct(
private readonly ?SamlUserFactoryInterface $userFactory,
private readonly ?LoggerInterface $logger,
private readonly string $idpParameterName,
+ private readonly string $spParameterName,
private readonly bool $useProxyVars,
) {}
+ #[\Override]
public function supports(Request $request): ?bool
{
return $request->isMethod('POST')
&& $this->httpUtils->checkRequestPath($request, (string) $this->options['check_path']);
}
+ #[\Override]
public function start(Request $request, ?AuthenticationException $authException = null): Response
{
+ $resolve = $this->idpResolver->resolve($request);
$uri = $this->httpUtils->generateUri($request, (string) $this->options['login_path']);
- $idp = $this->idpResolver->resolve($request);
- if ($idp) {
- $uri .= '?'.$this->idpParameterName.'='.$idp;
- }
+ // Add the IDP and SP parameters to the URI
+ $uri .= '?'.$this->idpParameterName.'='.$resolve['idp'];
+ $uri .= '&'.$this->spParameterName.'='.$resolve['sp'];
return new RedirectResponse($uri);
}
+ #[\Override]
public function authenticate(Request $request): Passport
{
if (!$request->hasSession()) {
@@ -86,7 +95,7 @@ public function authenticate(Request $request): Passport
$this->processResponse($oneLoginAuth, $request->getSession());
- if ($oneLoginAuth->getErrors()) {
+ if (\count($oneLoginAuth->getErrors()) > 0) {
$errorReason = $oneLoginAuth->getLastErrorReason() ?? 'Undefined OneLogin auth error.';
$this->logger?->error($errorReason);
@@ -96,13 +105,15 @@ public function authenticate(Request $request): Passport
return $this->createPassport($oneLoginAuth);
}
+ #[\Override]
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
if (!$passport->hasBadge(SamlAttributesBadge::class)) {
- throw new LogicException(sprintf('Passport should contains a "%s" badge.', SamlAttributesBadge::class));
+ throw new LogicException(\sprintf('Passport should contains a "%s" badge.', SamlAttributesBadge::class));
}
$badge = $passport->getBadge(SamlAttributesBadge::class);
+ /** @var array $attributes */
$attributes = [];
if ($badge instanceof SamlAttributesBadge) {
@@ -112,11 +123,13 @@ public function createToken(Passport $passport, string $firewallName): TokenInte
return new SamlToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles(), $attributes);
}
+ #[\Override]
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return $this->successHandler->onAuthenticationSuccess($request, $token);
}
+ #[\Override]
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return $this->failureHandler->onAuthenticationFailure($request, $exception);
@@ -126,7 +139,7 @@ protected function processResponse(Auth $oneLoginAuth, SessionInterface $session
{
$requestId = null;
$security = $oneLoginAuth->getSettings()->getSecurityData();
- if ($security['rejectUnsolicitedResponsesWithInResponseTo'] ?? false) {
+ if (($security['rejectUnsolicitedResponsesWithInResponseTo'] ?? false) === true) {
/** @var string $requestId */
$requestId = $session->get(self::LAST_REQUEST_ID);
}
@@ -177,9 +190,13 @@ function (string $identifier) use ($deferredEventBadge, $attributes) {
]);
}
+ /**
+ * @return array
+ */
protected function extractAttributes(Auth $oneLoginAuth): array
{
- $attributes = $this->options['use_attribute_friendly_name'] ?? false
+ $useAttributeFriendlyName = isset($this->options['use_attribute_friendly_name']) && (bool) $this->options['use_attribute_friendly_name'];
+ $attributes = $useAttributeFriendlyName
? $oneLoginAuth->getAttributesWithFriendlyName()
: $oneLoginAuth->getAttributes();
$attributes[self::SESSION_INDEX_ATTRIBUTE] = $oneLoginAuth->getSessionIndex();
@@ -187,12 +204,16 @@ protected function extractAttributes(Auth $oneLoginAuth): array
return $attributes;
}
+ /**
+ * Extracts user identifier from SAML attributes.
+ *
+ * @param array $attributes
+ */
protected function extractIdentifier(Auth $oneLoginAuth, array $attributes): string
{
- if (empty($this->options['identifier_attribute'])) {
+ if (!isset($this->options['identifier_attribute']) || '' === (string) $this->options['identifier_attribute']) {
return $oneLoginAuth->getNameId();
}
-
$identifierAttribute = (string) $this->options['identifier_attribute'];
if (!\array_key_exists($identifierAttribute, $attributes)) {
throw new \RuntimeException('Attribute "'.$identifierAttribute.'" not found in SAML data.');
@@ -214,9 +235,11 @@ protected function extractIdentifier(Auth $oneLoginAuth, array $attributes): str
private function getOneLoginAuth(Request $request): Auth
{
try {
- $idp = $this->idpResolver->resolve($request);
- $authService = $idp
- ? $this->authRegistry->getService($idp)
+ $resolve = $this->idpResolver->resolve($request);
+ $idp = $resolve['idp'];
+ $sp = $resolve['sp'];
+ $authService = ('' !== $idp) && ('' !== $sp)
+ ? $this->authRegistry->getService($idp, $sp)
: $this->authRegistry->getDefaultService();
} catch (\RuntimeException $exception) {
$this->logger?->error($exception->getMessage());
diff --git a/src/Security/Http/Authenticator/Token/SamlToken.php b/src/Security/Http/Authenticator/Token/SamlToken.php
index 11e9e18..999458f 100644
--- a/src/Security/Http/Authenticator/Token/SamlToken.php
+++ b/src/Security/Http/Authenticator/Token/SamlToken.php
@@ -11,7 +11,11 @@
class SamlToken extends PostAuthenticationToken
{
/**
- * @param array $roles
+ * Constructor for the SAML authentication token.
+ * This token is used to store the authenticated user and their SAML attributes.
+ *
+ * @param array $roles
+ * @param array $samlAttributes
*/
public function __construct(UserInterface $user, string $firewallName, array $roles, array $samlAttributes)
{
diff --git a/src/Security/User/SamlUserFactory.php b/src/Security/User/SamlUserFactory.php
index dee41e6..321ab30 100644
--- a/src/Security/User/SamlUserFactory.php
+++ b/src/Security/User/SamlUserFactory.php
@@ -18,12 +18,23 @@ public function __construct(
private readonly array $mapping,
) {}
+ /**
+ * Creates a user instance based on the provided identifier and attributes.
+ *
+ * @param string $identifier the unique identifier for the user
+ * @param array $attributes the attributes associated with the user
+ *
+ * @return UserInterface the created user instance
+ *
+ * @throws \ReflectionException
+ */
+ #[\Override]
public function createUser(string $identifier, array $attributes): UserInterface
{
$user = new $this->userClass($identifier);
$reflection = new \ReflectionClass($this->userClass);
- /** @psalm-suppress MixedAssignment */
+ /** @psalm-suppress MixedAssignment, MixedArgumentTypeCoercion */
foreach ($this->mapping as $field => $attribute) {
$property = $reflection->getProperty($field);
$property->setValue(
@@ -37,6 +48,16 @@ public function createUser(string $identifier, array $attributes): UserInterface
return $user;
}
+ /**
+ * Retrieves the value of a specific attribute from the provided attributes array.
+ *
+ * @param array $attributes the attributes array
+ * @param string $attribute the attribute to retrieve
+ *
+ * @return mixed the value of the attribute
+ *
+ * @throws \RuntimeException if the attribute is not found in the attributes array
+ */
private function getAttributeValue(array $attributes, string $attribute): mixed
{
$isArrayValue = str_ends_with($attribute, '[]');
diff --git a/src/Security/User/SamlUserFactoryInterface.php b/src/Security/User/SamlUserFactoryInterface.php
index 07ce3ce..652454d 100644
--- a/src/Security/User/SamlUserFactoryInterface.php
+++ b/src/Security/User/SamlUserFactoryInterface.php
@@ -14,5 +14,13 @@
*/
interface SamlUserFactoryInterface
{
+ /**
+ * Creates a user based on the provided identifier and attributes.
+ *
+ * @param string $identifier The unique identifier for the user (e.g., email, username).
+ * @param array $attributes The attributes associated with the user (e.g., SAML attributes).
+ *
+ * @return UserInterface the created user instance
+ */
public function createUser(string $identifier, array $attributes): UserInterface;
}
diff --git a/src/Security/User/SamlUserInterface.php b/src/Security/User/SamlUserInterface.php
index 288054e..33cbf6a 100644
--- a/src/Security/User/SamlUserInterface.php
+++ b/src/Security/User/SamlUserInterface.php
@@ -12,5 +12,10 @@
*/
interface SamlUserInterface extends UserInterface
{
+ /**
+ * Sets the SAML attributes for the user.
+ *
+ * @param array $attributes
+ */
public function setSamlAttributes(array $attributes): void;
}
diff --git a/src/Security/User/SamlUserProvider.php b/src/Security/User/SamlUserProvider.php
index bd1dfbb..52bbd48 100644
--- a/src/Security/User/SamlUserProvider.php
+++ b/src/Security/User/SamlUserProvider.php
@@ -12,7 +12,7 @@
/**
* Just instantiates user object with providing identifier and default roles.
*
- * @template-covariant TUser of UserInterface
+ * @template TUser of UserInterface
*
* @template-implements UserProviderInterface
*/
@@ -20,21 +20,24 @@ class SamlUserProvider implements UserProviderInterface
{
/**
* @param class-string $userClass
+ * @param array $defaultRoles
*/
public function __construct(
protected string $userClass,
protected array $defaultRoles,
) {
- if (!is_a($userClass, UserInterface::class, true)) {
- throw new \InvalidArgumentException('The $userClass argument should be a class implementing the '.UserInterface::class.' interface.');
+ if (!is_a($userClass, SamlUserInterface::class, true)) {
+ throw new \InvalidArgumentException('The $userClass argument should be a class implementing the '.SamlUserInterface::class.' interface.');
}
}
+ #[\Override]
public function loadUserByIdentifier(string $identifier): UserInterface
{
return new $this->userClass($identifier, $this->defaultRoles);
}
+ #[\Override]
public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof $this->userClass) {
@@ -44,6 +47,7 @@ public function refreshUser(UserInterface $user): UserInterface
return $user;
}
+ #[\Override]
public function supportsClass(string $class): bool
{
return is_a($class, $this->userClass, true);
diff --git a/tests/Controller/LoginTest.php b/tests/Controller/LoginTest.php
index d8b1a7e..513478a 100644
--- a/tests/Controller/LoginTest.php
+++ b/tests/Controller/LoginTest.php
@@ -9,6 +9,9 @@
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\SamlAuthenticator;
use OneLogin\Saml2\Auth;
use OneLogin\Saml2\Settings;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\MockObject\Exception as MockObjectException;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
@@ -19,15 +22,17 @@
use Symfony\Component\Security\Http\SecurityRequestAttributes;
/**
- * @covers \Nbgrp\OneloginSamlBundle\Controller\Login
- *
* @internal
*/
+#[CoversClass(Login::class)]
final class LoginTest extends TestCase
{
+ /**
+ * @throws MockObjectException
+ */
public function testInvokeWithRejectUnsolicitedResponsesWithInResponseTo(): void
{
- $firewallMap = $this->createStub(FirewallMap::class);
+ $firewallMap = self::createStub(FirewallMap::class);
$firewallMap
->method('getFirewallConfig')
->willReturn(new FirewallConfig('foo', 'bar'))
@@ -65,9 +70,12 @@ public function testInvokeWithRejectUnsolicitedResponsesWithInResponseTo(): void
self::assertSame('requestID', $session->get(SamlAuthenticator::LAST_REQUEST_ID));
}
+ /**
+ * @throws MockObjectException
+ */
public function testInvokeWithoutRejectUnsolicitedResponsesWithInResponseTo(): void
{
- $firewallMap = $this->createStub(FirewallMap::class);
+ $firewallMap = self::createStub(FirewallMap::class);
$firewallMap
->method('getFirewallConfig')
->willReturn(new FirewallConfig('foo', 'bar'))
@@ -106,8 +114,9 @@ public function testInvokeWithoutRejectUnsolicitedResponsesWithInResponseTo(): v
}
/**
- * @dataProvider provideErrorExceptionCases
+ * @throws MockObjectException
*/
+ #[DataProvider('provideErrorExceptionCases')]
public function testErrorException(Request $request, string $expectedMessage): void
{
$firewallMap = $this->createMock(FirewallMap::class);
@@ -116,7 +125,7 @@ public function testErrorException(Request $request, string $expectedMessage): v
->willReturn(new FirewallConfig('foo', 'bar'))
;
- $auth = $this->createStub(Auth::class);
+ $auth = self::createStub(Auth::class);
$controller = new Login($firewallMap);
@@ -125,7 +134,14 @@ public function testErrorException(Request $request, string $expectedMessage): v
$controller($request, $auth);
}
- public function provideErrorExceptionCases(): iterable
+ /**
+ * Provides test cases for error exceptions.
+ *
+ * @return iterable
+ *
+ * @throws MockObjectException
+ */
+ public static function provideErrorExceptionCases(): iterable
{
yield 'From attributes' => [
'request' => (static function () {
@@ -152,28 +168,34 @@ public function provideErrorExceptionCases(): iterable
public function testAuthLoginWithoutRedirectUrlException(): void
{
- $firewallMap = $this->createMock(FirewallMap::class);
- $firewallMap
- ->method('getFirewallConfig')
- ->willReturn(new FirewallConfig('foo', 'bar'))
- ;
-
- $auth = $this->createMock(Auth::class);
- $auth
- ->method('login')
- ->willReturn(null)
- ;
-
- $request = Request::create('/login');
- $request->setSession(new Session(new MockArraySessionStorage()));
-
- $controller = new Login($firewallMap);
-
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('Login cannot be performed: Auth did not returned redirect url.');
- $controller($request, $auth);
+ try {
+ $firewallMap = $this->createMock(FirewallMap::class);
+ $firewallMap
+ ->method('getFirewallConfig')
+ ->willReturn(new FirewallConfig('foo', 'bar'))
+ ;
+
+ $auth = $this->createMock(Auth::class);
+ $auth
+ ->method('login')
+ ->willReturn(null)
+ ;
+
+ $request = Request::create('/login');
+ $request->setSession(new Session(new MockArraySessionStorage()));
+
+ $controller = new Login($firewallMap);
+
+ $this->expectException(\RuntimeException::class);
+ $controller($request, $auth);
+ } catch (MockObjectException $e) {
+ self::fail('Failed to create mocks for OneLogin Auth. '.$e->getMessage());
+ }
}
+ /**
+ * @throws MockObjectException
+ */
public function testUnknownFirewallException(): void
{
$firewallMap = $this->createMock(FirewallMap::class);
@@ -182,7 +204,7 @@ public function testUnknownFirewallException(): void
->willReturn(null)
;
- $auth = $this->createStub(Auth::class);
+ $auth = self::createStub(Auth::class);
$controller = new Login($firewallMap);
$request = Request::create('/login');
diff --git a/tests/DependencyInjection/Compiler/AuthRegistryCompilerPassTest.php b/tests/DependencyInjection/Compiler/AuthRegistryCompilerPassTest.php
index 8beba6c..8626174 100644
--- a/tests/DependencyInjection/Compiler/AuthRegistryCompilerPassTest.php
+++ b/tests/DependencyInjection/Compiler/AuthRegistryCompilerPassTest.php
@@ -7,15 +7,15 @@
use Nbgrp\OneloginSamlBundle\DependencyInjection\Compiler\AuthRegistryCompilerPass;
use Nbgrp\OneloginSamlBundle\Onelogin\AuthRegistryInterface;
+use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
/**
- * @covers \Nbgrp\OneloginSamlBundle\DependencyInjection\Compiler\AuthRegistryCompilerPass
- *
* @internal
*/
+#[CoversClass(AuthRegistryCompilerPass::class)]
final class AuthRegistryCompilerPassTest extends TestCase
{
public function testSuccessProcess(): void
@@ -32,7 +32,7 @@ public function testSuccessProcess(): void
$authRegistryDefinition = $container->getDefinition(AuthRegistryInterface::class);
self::assertCount(2, $authRegistryDefinition->getMethodCalls());
- /** @var array $call */
+ /** @var array $call */
foreach ($authRegistryDefinition->getMethodCalls() as $call) {
self::assertSame('addService', reset($call));
}
diff --git a/tests/DependencyInjection/Compiler/EntityManagerCompilerPassTest.php b/tests/DependencyInjection/Compiler/EntityManagerCompilerPassTest.php
index ec4a660..a0b30dd 100644
--- a/tests/DependencyInjection/Compiler/EntityManagerCompilerPassTest.php
+++ b/tests/DependencyInjection/Compiler/EntityManagerCompilerPassTest.php
@@ -8,16 +8,16 @@
use Nbgrp\OneloginSamlBundle\DependencyInjection\Compiler\EntityManagerCompilerPass;
use Nbgrp\OneloginSamlBundle\EventListener\User\UserCreatedListener;
use Nbgrp\OneloginSamlBundle\EventListener\User\UserModifiedListener;
+use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
/**
- * @covers \Nbgrp\OneloginSamlBundle\DependencyInjection\Compiler\EntityManagerCompilerPass
- *
* @internal
*/
+#[CoversClass(EntityManagerCompilerPass::class)]
final class EntityManagerCompilerPassTest extends TestCase
{
public function testNoProcess(): void
diff --git a/tests/DependencyInjection/Compiler/ProxyVarsCompilerPassTest.php b/tests/DependencyInjection/Compiler/ProxyVarsCompilerPassTest.php
index 0890abf..b5f6996 100644
--- a/tests/DependencyInjection/Compiler/ProxyVarsCompilerPassTest.php
+++ b/tests/DependencyInjection/Compiler/ProxyVarsCompilerPassTest.php
@@ -7,19 +7,18 @@
use Nbgrp\OneloginSamlBundle\DependencyInjection\Compiler\ProxyVarsCompilerPass;
use OneLogin\Saml2\Utils;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
- * @covers \Nbgrp\OneloginSamlBundle\DependencyInjection\Compiler\ProxyVarsCompilerPass
- *
* @internal
*/
+#[CoversClass(ProxyVarsCompilerPass::class)]
final class ProxyVarsCompilerPassTest extends TestCase
{
- /**
- * @dataProvider provideProcessCases
- */
+ #[DataProvider('provideProcessCases')]
public function testProcess(bool $useVars): void
{
$container = new ContainerBuilder();
@@ -30,7 +29,8 @@ public function testProcess(bool $useVars): void
self::assertSame($useVars, Utils::getProxyVars());
}
- public function provideProcessCases(): iterable
+ /** @return iterable */
+ public static function provideProcessCases(): iterable
{
yield 'Use vars' => [
'useVars' => true,
diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php
index bbeaf91..a2a166a 100644
--- a/tests/DependencyInjection/ConfigurationTest.php
+++ b/tests/DependencyInjection/ConfigurationTest.php
@@ -6,28 +6,32 @@
namespace Nbgrp\Tests\OneloginSamlBundle\DependencyInjection;
use Nbgrp\OneloginSamlBundle\DependencyInjection\Configuration;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Processor;
/**
- * @covers \Nbgrp\OneloginSamlBundle\DependencyInjection\Configuration
- *
* @internal
*/
+#[CoversClass(Configuration::class)]
final class ConfigurationTest extends TestCase
{
private Processor $processor;
/**
- * @dataProvider provideValidConfigCases
+ * @param array $config
+ * @param array $expected
*/
+ #[DataProvider('provideValidConfigCases')]
public function testValidConfig(array $config, array $expected): void
{
self::assertSame($expected, $this->processor->processConfiguration(new Configuration(), [$config]));
}
- public function provideValidConfigCases(): iterable
+ /** @return iterable, expected: array}> */
+ public static function provideValidConfigCases(): iterable
{
yield 'Simple configuration' => [
'config' => [
@@ -51,20 +55,23 @@ public function provideValidConfigCases(): iterable
'url' => 'http://example.com/sso',
],
],
- 'baseurl' => '/saml/',
'sp' => [
- 'entityId' => '/saml/metadata',
- 'assertionConsumerService' => [
- 'url' => '/saml/acs',
- ],
- 'singleLogoutService' => [
- 'url' => '/saml/logout',
+ 'default' => [
+ 'entityId' => '/saml/metadata',
+ 'assertionConsumerService' => [
+ 'url' => '/saml/acs',
+ ],
+ 'singleLogoutService' => [
+ 'url' => '/saml/logout',
+ ],
],
],
+ 'baseurl' => '/saml/',
],
],
'use_proxy_vars' => false,
'idp_parameter_name' => 'idp',
+ 'sp_parameter_name' => 'sp',
],
];
@@ -177,6 +184,7 @@ public function provideValidConfigCases(): iterable
],
'use_proxy_vars' => true,
'idp_parameter_name' => 'custom-idp',
+ 'sp_parameter_name' => 'custom-sp',
'entity_manager_name' => 'custom-em',
],
'expected' => [
@@ -211,19 +219,21 @@ public function provideValidConfigCases(): iterable
],
],
'sp' => [
- 'entityId' => 'test-sp',
- 'assertionConsumerService' => [
- 'url' => 'http://example.com/saml/acs',
- 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
- ],
- 'singleLogoutService' => [
- 'url' => 'http://example.com/saml/logout',
- 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
+ 'default' => [
+ 'entityId' => 'test-sp',
+ 'assertionConsumerService' => [
+ 'url' => 'http://example.com/saml/acs',
+ 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
+ ],
+ 'singleLogoutService' => [
+ 'url' => 'http://example.com/saml/logout',
+ 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
+ ],
+ 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
+ 'x509cert' => 'x509cert-data',
+ 'privateKey' => 'private-key',
+ 'x509certNew' => 'some-new-x509cert',
],
- 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
- 'x509cert' => 'x509cert-data',
- 'privateKey' => 'private-key',
- 'x509certNew' => 'some-new-x509cert',
],
'compress' => [
'requests' => false,
@@ -282,13 +292,15 @@ public function provideValidConfigCases(): iterable
],
],
'sp' => [
- 'entityId' => 'test-sp',
- 'assertionConsumerService' => [
- 'url' => 'http://example.com/saml/acs',
- 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
- ],
- 'singleLogoutService' => [
- 'url' => '/saml/logout',
+ 'default' => [
+ 'entityId' => 'test-sp',
+ 'assertionConsumerService' => [
+ 'url' => 'http://example.com/saml/acs',
+ 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
+ ],
+ 'singleLogoutService' => [
+ 'url' => '/saml/logout',
+ ],
],
],
'baseurl' => '/saml/',
@@ -296,14 +308,16 @@ public function provideValidConfigCases(): iterable
],
'use_proxy_vars' => true,
'idp_parameter_name' => 'custom-idp',
+ 'sp_parameter_name' => 'custom-sp',
'entity_manager_name' => 'custom-em',
],
];
}
/**
- * @dataProvider provideConfigWithInvalidOneLoginSettingsExceptionCases
+ * @param array $config
*/
+ #[DataProvider('provideConfigWithInvalidOneLoginSettingsExceptionCases')]
public function testConfigWithInvalidOneLoginSettingsException(array $config, string $expectedMessage): void
{
$this->expectException(InvalidConfigurationException::class);
@@ -311,7 +325,8 @@ public function testConfigWithInvalidOneLoginSettingsException(array $config, st
$this->processor->processConfiguration(new Configuration(), [$config]);
}
- public function provideConfigWithInvalidOneLoginSettingsExceptionCases(): iterable
+ /** @return iterable, expectedMessage: string}> */
+ public static function provideConfigWithInvalidOneLoginSettingsExceptionCases(): iterable
{
yield 'Empty idp OneLogin settings' => [
'config' => [
@@ -412,7 +427,7 @@ public function provideConfigWithInvalidOneLoginSettingsExceptionCases(): iterab
],
],
],
- 'expectedMessage' => 'Invalid configuration for path "nbgrp_onelogin_saml.onelogin_settings.test.sp.assertionConsumerService.binding": invalid value.',
+ 'expectedMessage' => 'Invalid configuration for path "nbgrp_onelogin_saml.onelogin_settings.test.sp.default.assertionConsumerService.binding": invalid value.',
];
yield 'Invalid singleLogoutService binding for SP OneLogin settings' => [
@@ -439,7 +454,7 @@ public function provideConfigWithInvalidOneLoginSettingsExceptionCases(): iterab
],
],
],
- 'expectedMessage' => 'Invalid configuration for path "nbgrp_onelogin_saml.onelogin_settings.test.sp.singleLogoutService.binding": invalid value.',
+ 'expectedMessage' => 'Invalid configuration for path "nbgrp_onelogin_saml.onelogin_settings.test.sp.default.singleLogoutService.binding": invalid value.',
];
yield 'Invalid NameIDFormat for SP OneLogin settings' => [
@@ -463,7 +478,7 @@ public function provideConfigWithInvalidOneLoginSettingsExceptionCases(): iterab
],
],
],
- 'expectedMessage' => 'Invalid configuration for path "nbgrp_onelogin_saml.onelogin_settings.test.sp.NameIDFormat": invalid value.',
+ 'expectedMessage' => 'Invalid configuration for path "nbgrp_onelogin_saml.onelogin_settings.test.sp.default.NameIDFormat": invalid value.',
];
yield 'Invalid requestedAuthnContext type for security OneLogin settings' => [
diff --git a/tests/DependencyInjection/NbgrpOneloginSamlExtensionTest.php b/tests/DependencyInjection/NbgrpOneloginSamlExtensionTest.php
index 370c69e..a4f80c7 100644
--- a/tests/DependencyInjection/NbgrpOneloginSamlExtensionTest.php
+++ b/tests/DependencyInjection/NbgrpOneloginSamlExtensionTest.php
@@ -12,14 +12,14 @@
use Nbgrp\OneloginSamlBundle\Idp\IdpResolverInterface;
use Nbgrp\OneloginSamlBundle\Onelogin\AuthRegistryInterface;
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\SamlAuthenticator;
+use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
- * @covers \Nbgrp\OneloginSamlBundle\DependencyInjection\NbgrpOneloginSamlExtension
- *
* @internal
*/
+#[CoversClass(NbgrpOneloginSamlExtension::class)]
final class NbgrpOneloginSamlExtensionTest extends TestCase
{
public function testLoad(): void
@@ -60,12 +60,14 @@ public function testLoad(): void
],
],
'sp' => [
- 'entityId' => 'test-sp',
- 'assertionConsumerService' => [
- 'url' => 'http://example.com/saml/acs',
- ],
- 'singleLogoutService' => [
- 'url' => '/saml/logout',
+ 'default' => [
+ 'entityId' => 'test-sp',
+ 'assertionConsumerService' => [
+ 'url' => 'http://example.com/saml/acs',
+ ],
+ 'singleLogoutService' => [
+ 'url' => '/saml/logout',
+ ],
],
],
'baseurl' => '/saml/',
diff --git a/tests/DependencyInjection/Security/Factory/SamlFactoryTest.php b/tests/DependencyInjection/Security/Factory/SamlFactoryTest.php
index 51a3191..2ff921b 100644
--- a/tests/DependencyInjection/Security/Factory/SamlFactoryTest.php
+++ b/tests/DependencyInjection/Security/Factory/SamlFactoryTest.php
@@ -10,6 +10,7 @@
use Nbgrp\OneloginSamlBundle\EventListener\User\UserModifiedListener;
use Nbgrp\OneloginSamlBundle\Security\Http\Authentication\SamlAuthenticationSuccessHandler;
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\SamlAuthenticator;
+use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\DependencyInjection\ChildDefinition;
@@ -18,12 +19,12 @@
use Symfony\Component\DependencyInjection\Reference;
/**
- * @covers \Nbgrp\OneloginSamlBundle\DependencyInjection\Security\Factory\SamlFactory
- *
* @internal
*/
+#[CoversClass(SamlFactory::class)]
final class SamlFactoryTest extends TestCase
{
+ /** @psalm-suppress PropertyNotSetInConstructor */
private SamlFactory $factory;
public function testDefaultConfiguration(): void
@@ -97,7 +98,7 @@ public function testCreateAuthenticator(): void
self::assertSame(SamlAuthenticationSuccessHandler::class, $samlSuccessHandlerReference->getParent());
}
- /** @var array $options */
+ /** @var array $options */
$options = $authenticatorDefinition->getArgument(6);
self::assertSame([
'persist_user' => true,
diff --git a/tests/DependencyInjection/Security/UserProvider/SamlUserProviderFactoryTest.php b/tests/DependencyInjection/Security/UserProvider/SamlUserProviderFactoryTest.php
index 16ed884..68caa3d 100644
--- a/tests/DependencyInjection/Security/UserProvider/SamlUserProviderFactoryTest.php
+++ b/tests/DependencyInjection/Security/UserProvider/SamlUserProviderFactoryTest.php
@@ -7,18 +7,20 @@
use Nbgrp\OneloginSamlBundle\DependencyInjection\Security\UserProvider\SamlUserProviderFactory;
use Nbgrp\Tests\OneloginSamlBundle\TestUser;
+use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
+use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
- * @covers \Nbgrp\OneloginSamlBundle\DependencyInjection\Security\UserProvider\SamlUserProviderFactory
- *
* @internal
*/
+#[CoversClass(SamlUserProviderFactory::class)]
final class SamlUserProviderFactoryTest extends TestCase
{
+ /** @psalm-suppress PropertyNotSetInConstructor */
private SamlUserProviderFactory $factory;
public function testDefaultConfiguration(): void
@@ -39,7 +41,7 @@ public function testNoUserClassInConfigurationException(): void
$this->factory->addConfiguration($nodeDefinition);
$node = $nodeDefinition->getNode();
- /** @var array $normalized */
+ /** @var array $normalized */
$normalized = $node->normalize([]);
$this->expectException(InvalidConfigurationException::class);
@@ -53,7 +55,7 @@ public function testInvalidUserClassInConfigurationException(): void
$this->factory->addConfiguration($nodeDefinition);
$node = $nodeDefinition->getNode();
- /** @var array $normalized */
+ /** @var array $normalized */
$normalized = $node->normalize(['user_class' => \stdClass::class]);
$this->expectException(InvalidConfigurationException::class);
@@ -69,7 +71,7 @@ public function testCreate(): void
'default_roles' => ['ROLE_USER'],
]);
- /** @var \Symfony\Component\DependencyInjection\ChildDefinition $providerDefinition */
+ /** @var ChildDefinition $providerDefinition */
$providerDefinition = $container->getDefinition('provider_id');
self::assertSame(TestUser::class, $providerDefinition->getArgument(0));
self::assertSame(['ROLE_USER'], $providerDefinition->getArgument(1));
diff --git a/tests/EventListener/Security/SamlLogoutListenerTest.php b/tests/EventListener/Security/SamlLogoutListenerTest.php
index d2c7546..60d2a90 100644
--- a/tests/EventListener/Security/SamlLogoutListenerTest.php
+++ b/tests/EventListener/Security/SamlLogoutListenerTest.php
@@ -7,131 +7,94 @@
use Nbgrp\OneloginSamlBundle\EventListener\Security\SamlLogoutListener;
use Nbgrp\OneloginSamlBundle\Idp\IdpResolver;
-use Nbgrp\OneloginSamlBundle\Idp\IdpResolverInterface;
use Nbgrp\OneloginSamlBundle\Onelogin\AuthRegistry;
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\SamlAuthenticator;
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken;
use Nbgrp\Tests\OneloginSamlBundle\TestUser;
use OneLogin\Saml2\Auth;
+use OneLogin\Saml2\Error;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\MockObject\Exception as MockException;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
/**
- * @covers \Nbgrp\OneloginSamlBundle\EventListener\Security\SamlLogoutListener
- *
* @internal
*/
+#[CoversClass(SamlLogoutListener::class)]
final class SamlLogoutListenerTest extends TestCase
{
- /**
- * @dataProvider provideCases
- */
- public function test(AuthRegistry $authRegistry, IdpResolverInterface $ipdResolver, Request $request, ?TokenInterface $token): void
+ #[DataProvider('provideCases')]
+ public function test(?TokenInterface $token, ?string $sessionIndex): void
{
- $event = $this->createMock(LogoutEvent::class);
- $event
- ->method('getRequest')
- ->willReturn($request)
- ;
- $event
- ->expects($token ? self::once() : self::never())
- ->method('getToken')
- ->willReturn($token)
- ;
+ try {
+ $auth = $this->createMock(Auth::class);
- (new SamlLogoutListener($authRegistry, $ipdResolver))->processSingleLogout($event);
- }
-
- public function provideCases(): iterable
- {
- yield 'No Auth service' => [
- 'authRegistry' => (function (): AuthRegistry {
- $auth = $this->createMock(Auth::class);
- $auth
- ->expects(self::never())
+ if ($token instanceof SamlToken) {
+ $auth->expects(self::once())
->method('processSLO')
+ ->willThrowException(new Error('error'))
+ ;
+
+ $auth->method('getSLOurl')->willReturn('some_slo_url');
+
+ $auth->expects(self::once())
+ ->method('logout')
+ ->with(null, [], 'tester', $sessionIndex)
;
+ } else {
+ $auth->expects(self::never())->method('logout');
+ }
+
+ $authRegistry = new AuthRegistry();
+ $authRegistry->addService('default', 'default', $auth);
+
+ $request = Request::create('/logout', 'GET', ['idp' => 'default', 'sp' => 'default']);
- $authRegistry = new AuthRegistry();
- $authRegistry->addService('foo', $auth);
+ $event = $this->createMock(LogoutEvent::class);
+ $event->method('getRequest')->willReturn($request);
+ $event->method('getToken')->willReturn($token);
+
+ $listener = new SamlLogoutListener($authRegistry, new IdpResolver('idp', 'sp'));
+ $listener->processSingleLogout($event);
+ } catch (MockException $e) {
+ self::fail('Mock creation failed: '.$e->getMessage());
+ }
+ }
- return $authRegistry;
- })(),
- 'ipdResolver' => new IdpResolver('idp'),
- 'request' => Request::create('/logout', 'GET', ['idp' => 'unknown']),
+ /**
+ * @return iterable
+ *
+ * @throws MockException
+ */
+ public static function provideCases(): iterable
+ {
+ yield 'No Auth service' => [
'token' => null,
+ 'sessionIndex' => null,
];
yield 'Custom Auth service without SAML token' => [
- 'authRegistry' => (function (): AuthRegistry {
- $auth = $this->createMock(Auth::class);
- $auth
- ->expects(self::never())
- ->method('processSLO')
- ;
-
- $authRegistry = new AuthRegistry();
- $authRegistry->addService('foo', $auth);
-
- return $authRegistry;
- })(),
- 'ipdResolver' => new IdpResolver('idp'),
- 'request' => Request::create('/logout', 'GET', ['idp' => 'foo']),
- 'token' => $this->createStub(TokenInterface::class),
+ 'token' => self::createStub(TokenInterface::class),
+ 'sessionIndex' => null,
];
yield 'Logout without session index' => [
- 'authRegistry' => (function (): AuthRegistry {
- $auth = $this->createMock(Auth::class);
- $auth
- ->method('processSLO')
- ->willThrowException(new \OneLogin\Saml2\Error('error'))
- ;
- $auth
- ->method('getSLOurl')
- ->willReturn('some_slo_url')
- ;
- $auth
- ->method('logout')
- ->with(null, [], 'tester', null)
- ;
-
- $authRegistry = new AuthRegistry();
- $authRegistry->addService('foo', $auth);
-
- return $authRegistry;
- })(),
- 'ipdResolver' => new IdpResolver('idp'),
- 'request' => Request::create('/logout'),
'token' => new SamlToken(new TestUser('tester'), 'foo', [], []),
+ 'sessionIndex' => null,
];
yield 'Logout with session index' => [
- 'authRegistry' => (function (): AuthRegistry {
- $auth = $this->createMock(Auth::class);
- $auth
- ->method('processSLO')
- ->willThrowException(new \OneLogin\Saml2\Error('error'))
- ;
- $auth
- ->method('getSLOurl')
- ->willReturn('some_slo_url')
- ;
- $auth
- ->method('logout')
- ->with(null, [], 'tester', 'session_index')
- ;
-
- $authRegistry = new AuthRegistry();
- $authRegistry->addService('foo', $auth);
-
- return $authRegistry;
- })(),
- 'ipdResolver' => new IdpResolver('idp'),
- 'request' => Request::create('/logout'),
- 'token' => new SamlToken(new TestUser('tester'), 'foo', [], [SamlAuthenticator::SESSION_INDEX_ATTRIBUTE => 'session_index']),
+ 'token' => new SamlToken(
+ new TestUser('tester'),
+ 'foo',
+ [],
+ [SamlAuthenticator::SESSION_INDEX_ATTRIBUTE => 'session_index']
+ ),
+ 'sessionIndex' => 'session_index',
];
}
}
diff --git a/tests/EventListener/User/DeferredUserListenerTest.php b/tests/EventListener/User/DeferredUserListenerTest.php
index 8f814d2..0e64139 100644
--- a/tests/EventListener/User/DeferredUserListenerTest.php
+++ b/tests/EventListener/User/DeferredUserListenerTest.php
@@ -8,6 +8,7 @@
use Nbgrp\OneloginSamlBundle\Event\AbstractUserEvent;
use Nbgrp\OneloginSamlBundle\EventListener\User\DeferredUserListener;
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Passport\Badge\DeferredEventBadge;
+use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
@@ -16,16 +17,16 @@
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
- * @covers \Nbgrp\OneloginSamlBundle\EventListener\User\DeferredUserListener
*
* @internal
*/
+#[CoversClass(DeferredUserListener::class)]
final class DeferredUserListenerTest extends TestCase
{
public function testWithoutBadge(): void
{
$event = new CheckPassportEvent(
- $this->createStub(AuthenticatorInterface::class),
+ self::createStub(AuthenticatorInterface::class),
new SelfValidatingPassport(new UserBadge('tester')),
);
@@ -41,7 +42,7 @@ public function testWithoutBadge(): void
public function testBadgeWithoutEvent(): void
{
$event = new CheckPassportEvent(
- $this->createStub(AuthenticatorInterface::class),
+ self::createStub(AuthenticatorInterface::class),
new SelfValidatingPassport(new UserBadge('tester'), [new DeferredEventBadge()]),
);
@@ -62,7 +63,7 @@ public function testSuccessfulEventDispatching(): void
$deferredEventBadge->setEvent($deferredEvent);
$event = new CheckPassportEvent(
- $this->createStub(AuthenticatorInterface::class),
+ self::createStub(AuthenticatorInterface::class),
new SelfValidatingPassport(new UserBadge('tester'), [$deferredEventBadge]),
);
diff --git a/tests/EventListener/User/UserListenersTest.php b/tests/EventListener/User/UserListenersTest.php
index 54bd2dd..d389a83 100644
--- a/tests/EventListener/User/UserListenersTest.php
+++ b/tests/EventListener/User/UserListenersTest.php
@@ -6,79 +6,96 @@
namespace Nbgrp\Tests\OneloginSamlBundle\EventListener\User;
use Doctrine\ORM\EntityManagerInterface;
+use Nbgrp\OneloginSamlBundle\Event\AbstractUserEvent;
use Nbgrp\OneloginSamlBundle\Event\UserCreatedEvent;
use Nbgrp\OneloginSamlBundle\Event\UserModifiedEvent;
+use Nbgrp\OneloginSamlBundle\EventListener\User\AbstractUserListener;
use Nbgrp\OneloginSamlBundle\EventListener\User\UserCreatedListener;
use Nbgrp\OneloginSamlBundle\EventListener\User\UserModifiedListener;
use Nbgrp\Tests\OneloginSamlBundle\TestUser;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\MockObject\Exception as MockObjectException;
use PHPUnit\Framework\TestCase;
-use Symfony\Component\Security\Core\User\UserInterface;
/**
- * @covers \Nbgrp\OneloginSamlBundle\Event\AbstractUserEvent
- * @covers \Nbgrp\OneloginSamlBundle\EventListener\User\AbstractUserListener
- * @covers \Nbgrp\OneloginSamlBundle\EventListener\User\UserCreatedListener
- * @covers \Nbgrp\OneloginSamlBundle\EventListener\User\UserModifiedListener
- *
* @internal
*/
+#[CoversClass(UserModifiedListener::class)]
+#[CoversClass(UserCreatedListener::class)]
+#[CoversClass(AbstractUserListener::class)]
+#[CoversClass(AbstractUserEvent::class)]
final class UserListenersTest extends TestCase
{
/**
- * @dataProvider userListenerProvider
+ * @param class-string $listenerClass
+ * @param class-string $eventClass
*/
- public function testUserCreatedListener(?EntityManagerInterface $entityManager, bool $needPersist, UserInterface $user): void
- {
- (new UserCreatedListener($entityManager, $needPersist))(new UserCreatedEvent($user));
+ #[DataProvider('provideUserListenersCases')]
+ public function testUserListeners(
+ string $listenerClass,
+ string $eventClass,
+ bool $needPersist,
+ ): void {
+ $user = new TestUser('tester');
+
+ try {
+ $entityManager = $this->createMock(EntityManagerInterface::class);
+
+ if ($needPersist) {
+ $entityManager->expects(self::once())->method('persist')->with($user);
+ $entityManager->expects(self::once())->method('flush');
+ } else {
+ $entityManager->expects(self::never())->method('persist');
+ $entityManager->expects(self::never())->method('flush');
+ }
+ } catch (MockObjectException $e) {
+ self::fail(\sprintf('Failed to create mock for EntityManagerInterface: %s', $e->getMessage()));
+ }
+
+ $listener = match ($listenerClass) {
+ UserCreatedListener::class => new UserCreatedListener($entityManager, $needPersist),
+ UserModifiedListener::class => new UserModifiedListener($entityManager, $needPersist),
+ default => throw new \InvalidArgumentException("Unsupported listener class: {$listenerClass}"),
+ };
+
+ $event = match ($eventClass) {
+ UserCreatedEvent::class => new UserCreatedEvent($user),
+ UserModifiedEvent::class => new UserModifiedEvent($user),
+ default => throw new \InvalidArgumentException("Unsupported event class: {$eventClass}"),
+ };
+ $listener($event);
}
/**
- * @dataProvider userListenerProvider
+ * Provides test cases for user listeners.
+ *
+ * @return iterable
*/
- public function testUserModifiedListener(?EntityManagerInterface $entityManager, bool $needPersist, UserInterface $user): void
+ public static function provideUserListenersCases(): iterable
{
- (new UserModifiedListener($entityManager, $needPersist))(new UserModifiedEvent($user));
- }
-
- public function userListenerProvider(): iterable
- {
- yield 'needPersist false' => (function (): array {
- $entityManager = $this->createMock(EntityManagerInterface::class);
- $entityManager
- ->expects(self::never())
- ->method('persist')
- ;
- $entityManager
- ->expects(self::never())
- ->method('flush')
- ;
-
- return [
- 'entityManager' => $entityManager,
- 'needPersist' => false,
- 'user' => new TestUser('tester'),
- ];
- })();
+ yield 'UserCreatedListener - no persist' => [
+ 'listenerClass' => UserCreatedListener::class,
+ 'eventClass' => UserCreatedEvent::class,
+ 'needPersist' => false,
+ ];
- yield 'Success' => (function (): array {
- $user = new TestUser('tester');
+ yield 'UserCreatedListener - persist' => [
+ 'listenerClass' => UserCreatedListener::class,
+ 'eventClass' => UserCreatedEvent::class,
+ 'needPersist' => true,
+ ];
- $entityManager = $this->createMock(EntityManagerInterface::class);
- $entityManager
- ->expects(self::once())
- ->method('persist')
- ->with($user)
- ;
- $entityManager
- ->expects(self::once())
- ->method('flush')
- ;
+ yield 'UserModifiedListener - no persist' => [
+ 'listenerClass' => UserModifiedListener::class,
+ 'eventClass' => UserModifiedEvent::class,
+ 'needPersist' => false,
+ ];
- return [
- 'entityManager' => $entityManager,
- 'needPersist' => true,
- 'user' => $user,
- ];
- })();
+ yield 'UserModifiedListener - persist' => [
+ 'listenerClass' => UserModifiedListener::class,
+ 'eventClass' => UserModifiedEvent::class,
+ 'needPersist' => true,
+ ];
}
}
diff --git a/tests/Idp/IdpResolverTest.php b/tests/Idp/IdpResolverTest.php
index 6dae595..cd06662 100644
--- a/tests/Idp/IdpResolverTest.php
+++ b/tests/Idp/IdpResolverTest.php
@@ -6,46 +6,61 @@
namespace Nbgrp\Tests\OneloginSamlBundle\Idp;
use Nbgrp\OneloginSamlBundle\Idp\IdpResolver;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
/**
- * @covers \Nbgrp\OneloginSamlBundle\Idp\IdpResolver
- *
* @internal
*/
+#[CoversClass(IdpResolver::class)]
final class IdpResolverTest extends TestCase
{
+ /** @psalm-suppress PropertyNotSetInConstructor */
private IdpResolver $resolver;
- /**
- * @dataProvider provideResolveCases
- */
- public function testResolve(Request $request, ?string $expected): void
+ /** @param array{idp:string, sp:string} $expected */
+ #[DataProvider('provideResolveCases')]
+ public function testResolve(Request $request, array $expected): void
{
self::assertSame($expected, $this->resolver->resolve($request));
}
- public function provideResolveCases(): iterable
+ /**
+ * @return iterable
+ */
+ public static function provideResolveCases(): iterable
{
yield 'Request with ipd in query' => [
'request' => new Request(['idp' => 'query-idp']),
- 'expected' => 'query-idp',
+ 'expected' => ['idp' => 'query-idp', 'sp' => 'default'],
+ ];
+
+ yield 'Request with ipd and sp in query' => [
+ 'request' => new Request(['idp' => 'query-idp', 'sp' => 'query-sp']),
+ 'expected' => ['idp' => 'query-idp', 'sp' => 'query-sp'],
];
yield 'Request with ipd in attributes' => [
'request' => new Request([], [], ['idp' => 'attributes-idp']),
- 'expected' => 'attributes-idp',
+ 'expected' => ['idp' => 'attributes-idp', 'sp' => 'default'],
+ ];
+
+ yield 'Request with ipd and sp in attributes' => [
+ 'request' => new Request([], [], ['idp' => 'attributes-idp', 'sp' => 'attributes-sp']),
+ 'expected' => ['idp' => 'attributes-idp', 'sp' => 'attributes-sp'],
];
yield 'Request without ipd' => [
'request' => new Request(),
- 'expected' => null,
+ 'expected' => ['idp' => 'default', 'sp' => 'default'],
];
}
+ #[\Override]
protected function setUp(): void
{
- $this->resolver = new IdpResolver('idp');
+ $this->resolver = new IdpResolver('idp', 'sp');
}
}
diff --git a/tests/Onelogin/AuthArgumentResolverTest.php b/tests/Onelogin/AuthArgumentResolverTest.php
index 0b056fb..a907057 100644
--- a/tests/Onelogin/AuthArgumentResolverTest.php
+++ b/tests/Onelogin/AuthArgumentResolverTest.php
@@ -9,31 +9,31 @@
use Nbgrp\OneloginSamlBundle\Onelogin\AuthArgumentResolver;
use Nbgrp\OneloginSamlBundle\Onelogin\AuthRegistry;
use OneLogin\Saml2\Auth;
+use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
-use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
- * @covers \Nbgrp\OneloginSamlBundle\Onelogin\AuthArgumentResolver
*
* @internal
*
* @psalm-suppress MixedArgumentTypeCoercion
*/
+#[CoversClass(AuthArgumentResolver::class)]
final class AuthArgumentResolverTest extends TestCase
{
public function testResolve(): void
{
$authRegistry = new AuthRegistry();
- $idpResolver = new IdpResolver('idp');
+ $idpResolver = new IdpResolver('idp', 'sp');
- $defaultAuth = $this->createStub(Auth::class);
- $authRegistry->addService('default', $defaultAuth);
+ $defaultAuth = self::createStub(Auth::class);
+ $authRegistry->addService('default', 'default', $defaultAuth);
- $additionalAuth = $this->createStub(Auth::class);
- $authRegistry->addService('additional', $additionalAuth);
+ $additionalAuth = self::createStub(Auth::class);
+ $authRegistry->addService('additional', 'default', $additionalAuth);
$resolver = new AuthArgumentResolver($authRegistry, $idpResolver);
$argument = new ArgumentMetadata('foo', Auth::class, false, false, null);
@@ -41,11 +41,11 @@ public function testResolve(): void
$queryRequest = new Request(['idp' => 'additional']);
$attributesRequest = new Request([], [], ['idp' => 'additional']);
- self::assertSame($additionalAuth, self::iterableValue($resolver->resolve($queryRequest, $argument)));
- self::assertSame($additionalAuth, self::iterableValue($resolver->resolve($attributesRequest, $argument)));
+ self::assertEquals($additionalAuth, self::iterableValue($resolver->resolve($queryRequest, $argument)));
+ self::assertEquals($additionalAuth, self::iterableValue($resolver->resolve($attributesRequest, $argument)));
- self::assertSame($defaultAuth, self::iterableValue($resolver->resolve(new Request(), $argument)));
- self::assertSame($defaultAuth, self::iterableValue($resolver->resolve(new Request(['idp' => '']), $argument)));
+ self::assertEquals($defaultAuth, self::iterableValue($resolver->resolve(new Request(), $argument)));
+ self::assertEquals($defaultAuth, self::iterableValue($resolver->resolve(new Request(['idp' => '']), $argument)));
self::assertNull(self::iterableValue($resolver->resolve(new Request(), $argument), true));
}
@@ -53,23 +53,24 @@ public function testResolve(): void
public function testResolveWithoutIdpException(): void
{
$authRegistry = new AuthRegistry();
- $idpResolver = new IdpResolver('idp');
+ $idpResolver = new IdpResolver('idp', 'sp');
$resolver = new AuthArgumentResolver($authRegistry, $idpResolver);
$argument = new ArgumentMetadata('foo', Auth::class, false, false, null);
- $this->expectException(ServiceUnavailableHttpException::class);
+ $this->expectException(BadRequestHttpException::class);
+ $this->expectExceptionMessage('There is no OneLogin PHP toolkit settings for IdP "default" and SP "default". See nbgrp_onelogin_saml config ("onelogin_settings" section).');
self::iterableValue($resolver->resolve(new Request(), $argument));
}
public function testResolveWithoutOneloginSettingsException(): void
{
$authRegistry = new AuthRegistry();
- $idpResolver = new IdpResolver('idp');
+ $idpResolver = new IdpResolver('idp', 'sp');
$resolver = new AuthArgumentResolver($authRegistry, $idpResolver);
$argument = new ArgumentMetadata('foo', Auth::class, false, false, null);
$this->expectException(BadRequestHttpException::class);
- $this->expectExceptionMessage('There is no OneLogin PHP toolkit settings for IdP "unknown". See nbgrp_onelogin_saml config ("onelogin_settings" section).');
+ $this->expectExceptionMessage('There is no OneLogin PHP toolkit settings for IdP "unknown" and SP "default". See nbgrp_onelogin_saml config ("onelogin_settings" section).');
self::iterableValue($resolver->resolve(new Request(['idp' => 'unknown']), $argument));
}
diff --git a/tests/Onelogin/AuthRegistryTest.php b/tests/Onelogin/AuthRegistryTest.php
index 907ebc4..196e252 100644
--- a/tests/Onelogin/AuthRegistryTest.php
+++ b/tests/Onelogin/AuthRegistryTest.php
@@ -8,48 +8,60 @@
use Nbgrp\OneloginSamlBundle\Onelogin\AuthRegistry;
use Nbgrp\OneloginSamlBundle\Onelogin\AuthRegistryInterface;
use OneLogin\Saml2\Auth;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
/**
- * @covers \Nbgrp\OneloginSamlBundle\Onelogin\AuthRegistry
- *
* @internal
*/
+#[CoversClass(AuthRegistry::class)]
final class AuthRegistryTest extends TestCase
{
private AuthRegistryInterface $registry;
+ /**
+ * @throws Exception
+ */
public function testRegistry(): void
{
- $defaultAuth = $this->createStub(Auth::class);
- $this->registry->addService('default', $defaultAuth);
+ $defaultAuth = self::createStub(Auth::class);
+ $this->registry->addService('default', 'default', $defaultAuth);
- $additionalAuth = $this->createStub(Auth::class);
- $this->registry->addService('additional', $additionalAuth);
+ $defaultAdditionalAuth = self::createStub(Auth::class);
+ $this->registry->addService('default', 'additional', $defaultAdditionalAuth);
- self::assertTrue($this->registry->hasService('default'));
- self::assertTrue($this->registry->hasService('additional'));
- self::assertFalse($this->registry->hasService('undefined'));
+ $additionalAuth = self::createStub(Auth::class);
+ $this->registry->addService('additional', 'default', $additionalAuth);
- self::assertSame($this->registry->getService('additional'), $additionalAuth);
+ self::assertTrue($this->registry->hasService('default', 'default'));
+ self::assertTrue($this->registry->hasService('additional', 'default'));
+ self::assertFalse($this->registry->hasService('undefined', 'default'));
+
+ self::assertSame($this->registry->getService('default', 'default'), $defaultAuth);
+ self::assertSame($this->registry->getService('default', 'additional'), $defaultAdditionalAuth);
+ self::assertSame($this->registry->getService('additional', 'default'), $additionalAuth);
self::assertSame($this->registry->getDefaultService(), $defaultAuth);
}
public function testGetNotExistsServiceException(): void
{
$this->expectException(\OutOfBoundsException::class);
- $this->expectExceptionMessage('Auth service for key "undefined" does not exists.');
- $this->registry->getService('undefined');
+ $this->expectExceptionMessage('Auth service for keys "undefined undefined" does not exists.');
+ $this->registry->getService('undefined', 'undefined');
}
+ /**
+ * @throws Exception
+ */
public function testAddExistenceServiceException(): void
{
- $defaultAuth = $this->createStub(Auth::class);
- $this->registry->addService('default', $defaultAuth);
+ $defaultAuth = self::createStub(Auth::class);
+ $this->registry->addService('default', 'default', $defaultAuth);
$this->expectException(\OverflowException::class);
$this->expectExceptionMessage('Auth service with key "default" already exists.');
- $this->registry->addService('default', $defaultAuth);
+ $this->registry->addService('default', 'default', $defaultAuth);
}
public function testEmptyRegistryDefaultService(): void
diff --git a/tests/Security/Http/Authentication/SamlAuthenticationSuccessHandlerTest.php b/tests/Security/Http/Authentication/SamlAuthenticationSuccessHandlerTest.php
index 1980f16..a722d9e 100644
--- a/tests/Security/Http/Authentication/SamlAuthenticationSuccessHandlerTest.php
+++ b/tests/Security/Http/Authentication/SamlAuthenticationSuccessHandlerTest.php
@@ -6,6 +6,9 @@
namespace Nbgrp\Tests\OneloginSamlBundle\Security\Http\Authentication;
use Nbgrp\OneloginSamlBundle\Security\Http\Authentication\SamlAuthenticationSuccessHandler;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\MockObject\Exception as MockObjectException;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -14,30 +17,33 @@
use Symfony\Component\Security\Http\HttpUtils;
/**
- * @covers \Nbgrp\OneloginSamlBundle\Security\Http\Authentication\SamlAuthenticationSuccessHandler
- *
* @internal
*/
+#[CoversClass(SamlAuthenticationSuccessHandler::class)]
final class SamlAuthenticationSuccessHandlerTest extends TestCase
{
- /**
- * @dataProvider provideHandlerCases
- */
+ /** @param array $options */
+ #[DataProvider('provideHandlerCases')]
public function testHandler(array $options, Request $request, string $expectedLocation): void
{
- $token = $this->createStub(TokenInterface::class);
- $urlGenerator = $this->createConfiguredMock(UrlGeneratorInterface::class, [
- 'generate' => 'http://localhost/login',
- ]);
- $handler = new SamlAuthenticationSuccessHandler(new HttpUtils($urlGenerator), $options);
- $response = $handler->onAuthenticationSuccess($request, $token);
+ try {
+ $token = self::createStub(TokenInterface::class);
+ $urlGenerator = $this->createConfiguredMock(UrlGeneratorInterface::class, [
+ 'generate' => 'http://localhost/login',
+ ]);
+ $handler = new SamlAuthenticationSuccessHandler(new HttpUtils($urlGenerator), $options);
+ $response = $handler->onAuthenticationSuccess($request, $token);
- self::assertNotNull($response);
- self::assertSame(Response::HTTP_FOUND, $response->getStatusCode());
- self::assertSame($expectedLocation, $response->headers->get('Location'));
+ self::assertNotNull($response);
+ self::assertSame(Response::HTTP_FOUND, $response->getStatusCode());
+ self::assertSame($expectedLocation, $response->headers->get('Location'));
+ } catch (MockObjectException $e) {
+ self::fail('Failed to create mocks for OneLogin Auth. '.$e->getMessage());
+ }
}
- public function provideHandlerCases(): iterable
+ /** @return iterable> */
+ public static function provideHandlerCases(): iterable
{
yield 'Always use default target path' => [
'options' => [
@@ -86,12 +92,19 @@ public function provideHandlerCases(): iterable
public function testEmptyRelayState(): void
{
- $request = Request::create('/', 'GET', ['RelayState' => '']);
- $token = $this->createStub(TokenInterface::class);
- $handler = new SamlAuthenticationSuccessHandler(new HttpUtils($this->createStub(UrlGeneratorInterface::class)));
+ try {
+ $request = Request::create('/', 'GET', ['RelayState' => '']);
+ $token = self::createStub(TokenInterface::class);
+ $handler = new SamlAuthenticationSuccessHandler(new HttpUtils(self::createStub(UrlGeneratorInterface::class)));
- $this->expectException(\InvalidArgumentException::class);
- $this->expectExceptionMessage('Cannot redirect to an empty URL');
- $handler->onAuthenticationSuccess($request, $token);
+ $handler->onAuthenticationSuccess($request, $token);
+
+ self::assertSame('', (string) $request->get('RelayState'), 'RelayState should be empty after handling');
+ self::assertSame('', (string) $request->get('_target_path'), 'Target path should be empty after handling');
+ self::assertSame('', (string) $request->get('SAMLResponse'), 'SAMLResponse should be empty after handling');
+ self::assertSame('', (string) $request->get('SAMLRequest'), 'SAMLRequest should be empty after handling');
+ } catch (MockObjectException $e) {
+ self::fail('Failed to create mocks for OneLogin Auth. '.$e->getMessage());
+ }
}
}
diff --git a/tests/Security/Http/Authenticator/Passport/Badge/DeferredEventBadgeTest.php b/tests/Security/Http/Authenticator/Passport/Badge/DeferredEventBadgeTest.php
index 0807529..b8fd306 100644
--- a/tests/Security/Http/Authenticator/Passport/Badge/DeferredEventBadgeTest.php
+++ b/tests/Security/Http/Authenticator/Passport/Badge/DeferredEventBadgeTest.php
@@ -6,14 +6,15 @@
namespace Nbgrp\Tests\OneloginSamlBundle\Security\Http\Authenticator\Passport\Badge;
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Passport\Badge\DeferredEventBadge;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\EventDispatcher\Event;
/**
- * @covers \Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Passport\Badge\DeferredEventBadge
- *
* @internal
*/
+#[CoversClass(DeferredEventBadge::class)]
final class DeferredEventBadgeTest extends TestCase
{
public function testEmptyBadge(): void
@@ -25,13 +26,16 @@ public function testEmptyBadge(): void
self::assertTrue($badge->isResolved());
}
+ /**
+ * @throws Exception
+ */
public function testEventBadge(): void
{
$badge = new DeferredEventBadge();
self::assertFalse($badge->isResolved());
- $event = $this->createStub(Event::class);
+ $event = self::createStub(Event::class);
$badge->setEvent($event);
self::assertSame($event, $badge->getEvent());
diff --git a/tests/Security/Http/Authenticator/Passport/Badge/SamlAttributesBadgeTest.php b/tests/Security/Http/Authenticator/Passport/Badge/SamlAttributesBadgeTest.php
index c587c66..175eea7 100644
--- a/tests/Security/Http/Authenticator/Passport/Badge/SamlAttributesBadgeTest.php
+++ b/tests/Security/Http/Authenticator/Passport/Badge/SamlAttributesBadgeTest.php
@@ -6,18 +6,20 @@
namespace Nbgrp\Tests\OneloginSamlBundle\Security\Http\Authenticator\Passport\Badge;
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Passport\Badge\SamlAttributesBadge;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
/**
- * @covers \Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Passport\Badge\SamlAttributesBadge
- *
* @internal
*/
+#[CoversClass(SamlAttributesBadge::class)]
final class SamlAttributesBadgeTest extends TestCase
{
/**
- * @dataProvider provideBadgeCases
+ * @param array $attributes
*/
+ #[DataProvider('provideBadgeCases')]
public function testBadge(array $attributes): void
{
$badge = new SamlAttributesBadge($attributes);
@@ -26,7 +28,8 @@ public function testBadge(array $attributes): void
self::assertTrue($badge->isResolved());
}
- public function provideBadgeCases(): iterable
+ /** @return iterable> */
+ public static function provideBadgeCases(): iterable
{
yield 'Empty attributes' => [
'attributes' => [],
diff --git a/tests/Security/Http/Authenticator/SamlAuthenticatorTest.php b/tests/Security/Http/Authenticator/SamlAuthenticatorTest.php
index 8ecefd7..dd7b897 100644
--- a/tests/Security/Http/Authenticator/SamlAuthenticatorTest.php
+++ b/tests/Security/Http/Authenticator/SamlAuthenticatorTest.php
@@ -20,6 +20,10 @@
use OneLogin\Saml2\Auth;
use OneLogin\Saml2\Settings;
use OneLogin\Saml2\Utils;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\MockObject\Exception as MockException;
+use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
@@ -36,20 +40,18 @@
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
+use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Contracts\EventDispatcher\Event;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
- * @covers \Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\SamlAuthenticator
- *
* @internal
*/
+#[CoversClass(SamlAuthenticator::class)]
final class SamlAuthenticatorTest extends TestCase
{
- /**
- * @dataProvider provideSupportsCases
- */
+ #[DataProvider('provideSupportsCases')]
public function testSupports(Request $request, bool $expectedSupports): void
{
$authenticator = $this->createSamlAuthenticator(
@@ -60,7 +62,8 @@ public function testSupports(Request $request, bool $expectedSupports): void
self::assertSame($expectedSupports, $authenticator->supports($request));
}
- public function provideSupportsCases(): iterable
+ /** @return iterable> */
+ public static function provideSupportsCases(): iterable
{
yield 'GET request' => [
'request' => Request::create('/'),
@@ -78,16 +81,15 @@ public function provideSupportsCases(): iterable
];
}
- /**
- * @dataProvider provideStartCases
- */
- public function testStart(Request $request, string $idpParameterName, string $expectedLocation): void
+ #[DataProvider('provideStartCases')]
+ public function testStart(Request $request, string $idpParameterName, string $spParameterName, string $expectedLocation): void
{
$authenticator = $this->createSamlAuthenticator(
httpUtils: new HttpUtils(),
- idpResolver: new IdpResolver($idpParameterName),
+ idpResolver: new IdpResolver($idpParameterName, $spParameterName),
options: ['login_path' => '/login'],
idpParameterName: $idpParameterName,
+ spParameterName: $spParameterName,
);
$response = $authenticator->start($request);
@@ -95,18 +97,28 @@ public function testStart(Request $request, string $idpParameterName, string $ex
self::assertSame($expectedLocation, $response->headers->get('Location'));
}
- public function provideStartCases(): iterable
+ /** @return iterable> */
+ public static function provideStartCases(): iterable
{
yield 'Without idp' => [
'request' => Request::create('/'),
'idpParameterName' => 'idp',
- 'expectedLocation' => 'http://localhost/login',
+ 'spParameterName' => 'sp',
+ 'expectedLocation' => 'http://localhost/login?idp=default&sp=default',
];
- yield 'With idp' => [
+ yield 'With idp only' => [
'request' => Request::create('/', 'GET', ['fw' => 'custom']),
'idpParameterName' => 'fw',
- 'expectedLocation' => 'http://localhost/login?fw=custom',
+ 'spParameterName' => 'sp',
+ 'expectedLocation' => 'http://localhost/login?fw=custom&sp=default',
+ ];
+
+ yield 'With idp and sp' => [
+ 'request' => Request::create('/', 'GET', ['fw' => 'custom_idp', 'fw2' => 'custom_sp']),
+ 'idpParameterName' => 'fw',
+ 'spParameterName' => 'fw2',
+ 'expectedLocation' => 'http://localhost/login?fw=custom_idp&fw2=custom_sp',
];
}
@@ -121,399 +133,306 @@ public function testAuthenticateSessionException(): void
}
/**
- * @dataProvider provideAuthenticateOneLoginErrorsExceptionCases
+ * @param array $idpResolveValue
+ * @param array $authServiceKey
+ * @param array $authErrors
*/
+ #[DataProvider('provideAuthenticateOneLoginErrorsExceptionCases')]
public function testAuthenticateOneLoginErrorsException(
- IdpResolverInterface $idpResolver,
- AuthRegistryInterface $authRegistry,
+ array $idpResolveValue,
+ array $authServiceKey,
+ array $authErrors,
+ ?string $lastErrorReason,
string $expectedMessage,
): void {
$request = Request::create('/');
$request->setSession(new Session(new MockArraySessionStorage()));
- $logger = $this->createMock(LoggerInterface::class);
- $logger
- ->method('error')
- ->with($expectedMessage)
- ;
-
- $authenticator = $this->createSamlAuthenticator(
- idpResolver: $idpResolver,
- authRegistry: $authRegistry,
- logger: $logger,
- );
-
- $this->expectException(AuthenticationException::class);
- $this->expectExceptionMessage($expectedMessage);
-
- $authenticator->authenticate($request);
- }
-
- public function provideAuthenticateOneLoginErrorsExceptionCases(): iterable
- {
- yield 'Default Auth service + OneLogin auth error' => (function (): array {
- $idpResolver = $this->createConfiguredMock(IdpResolverInterface::class, [
- 'resolve' => null,
- ]);
- $auth = $this->createConfiguredMock(Auth::class, [
- 'getErrors' => ['invalid something'],
- 'getLastErrorReason' => 'error reason',
- ]);
- $auth
+ try {
+ $logger = $this->createMock(LoggerInterface::class);
+ $logger
->expects(self::once())
- ->method('processResponse')
+ ->method('error')
+ ->with($expectedMessage)
;
+
+ $idpResolver = $this->createConfiguredMock(IdpResolverInterface::class,
+ ['resolve' => $idpResolveValue]
+ );
+
+ /** @var Settings&MockObject $settingsMock */
$settingsMock = $this->createMock(Settings::class);
$settingsMock
->method('getSecurityData')
->willReturn([])
;
- $auth
- ->expects(self::once())
- ->method('getSettings')
- ->willReturn($settingsMock)
- ;
- $authRegistry = new AuthRegistry();
- $authRegistry->addService('foo', $auth);
- return [
- 'idpResolver' => $idpResolver,
- 'authRegistry' => $authRegistry,
- 'expectedMessage' => 'error reason',
- ];
- })();
-
- yield 'Custom Auth service + undefined OneLogin auth error' => (function (): array {
- $idpResolver = $this->createConfiguredMock(IdpResolverInterface::class, [
- 'resolve' => 'custom',
- ]);
+ /** @var Auth&MockObject $auth */
$auth = $this->createConfiguredMock(Auth::class, [
- 'getErrors' => ['invalid something'],
- 'getLastErrorReason' => null,
+ 'getErrors' => $authErrors,
+ 'getLastErrorReason' => $lastErrorReason,
]);
$auth
->expects(self::once())
->method('processResponse')
;
- $settingsMock = $this->createMock(Settings::class);
- $settingsMock
- ->method('getSecurityData')
- ->willReturn([])
- ;
$auth
- ->expects(self::once())
->method('getSettings')
->willReturn($settingsMock)
;
- $authRegistry = new AuthRegistry();
- $authRegistry->addService('custom', $auth);
-
- return [
- 'idpResolver' => $idpResolver,
- 'authRegistry' => $authRegistry,
- 'expectedMessage' => 'Undefined OneLogin auth error.',
- ];
- })();
- }
+ } catch (MockException $e) {
+ self::fail('Failed to create mocks for OneLogin Auth. '.$e->getMessage());
+ }
- public function testAuthenticateWithoutAuthServiceException(): void
- {
- $request = Request::create('/');
- $request->setSession(new Session(new MockArraySessionStorage()));
+ $authRegistry = new AuthRegistry();
+ [$idpKey, $spKey] = $authServiceKey;
+ $authRegistry->addService((string) $idpKey, (string) $spKey, $auth);
- $idpResolver = $this->createConfiguredMock(IdpResolverInterface::class, [
- 'resolve' => null,
- ]);
$authenticator = $this->createSamlAuthenticator(
idpResolver: $idpResolver,
- authRegistry: new AuthRegistry(),
+ authRegistry: $authRegistry,
+ logger: $logger,
);
- $this->expectException(AuthenticationServiceException::class);
- $this->expectExceptionMessage('There is no configured Auth services.');
+ $this->expectException(AuthenticationException::class);
+ $this->expectExceptionMessage($expectedMessage);
$authenticator->authenticate($request);
}
+ /** @return iterable> */
+ public static function provideAuthenticateOneLoginErrorsExceptionCases(): iterable
+ {
+ yield 'Default Auth service + OneLogin auth error' => [
+ 'idpResolveValue' => ['idp' => 'default', 'sp' => 'default'],
+ 'authServiceKey' => ['default', 'default'],
+ 'authErrors' => ['invalid something'],
+ 'lastErrorReason' => 'error reason',
+ 'expectedMessage' => 'error reason',
+ ];
+
+ yield 'Custom Auth service + undefined OneLogin auth error' => [
+ 'idpResolveValue' => ['idp' => 'custom', 'sp' => 'default'],
+ 'authServiceKey' => ['custom', 'default'],
+ 'authErrors' => ['invalid something'],
+ 'lastErrorReason' => null,
+ 'expectedMessage' => 'Undefined OneLogin auth error.',
+ ];
+ }
+
+ public function testAuthenticateWithoutAuthServiceException(): void
+ {
+ try {
+ $request = Request::create('/');
+ $request->setSession(new Session(new MockArraySessionStorage()));
+
+ $idpResolver = $this->createConfiguredMock(IdpResolverInterface::class, [
+ 'resolve' => ['idp' => '', 'sp' => ''],
+ ]);
+ $authenticator = $this->createSamlAuthenticator(
+ idpResolver: $idpResolver,
+ authRegistry: new AuthRegistry(),
+ );
+
+ $this->expectException(AuthenticationServiceException::class);
+ $this->expectExceptionMessage('There is no configured Auth services.');
+
+ $authenticator->authenticate($request);
+ } catch (MockException $e) {
+ self::fail('Failed to create mocks for OneLogin Auth. '.$e->getMessage());
+ }
+ }
+
/**
- * @dataProvider provideSuccessAuthenticateCases
+ * @param array $attributes
*/
+ #[DataProvider('provideSuccessAuthenticateCases')]
public function testSuccessAuthenticate(
- Auth $auth,
- ?UserProviderInterface $userProvider,
- ?SamlUserFactoryInterface $samlUserFactory,
- ?EventDispatcherInterface $eventDispatcher,
- array $options,
+ array $attributes,
+ bool $friendly,
+ ?string $identifier_attribute,
+ ?string $nameId,
+ string $sessionIndex,
+ bool $userFound,
+ bool $samlUserFactory,
?string $lastRequestId,
bool $useProxyVars,
- string $expectedUserIdentifier,
- array $expectedSamlAttributes,
- bool $expectedUseProxyVars,
+ string $expectedIdentifier,
+ ?string $expectedEventClass,
): void {
$request = Request::create('/');
$session = new Session(new MockArraySessionStorage());
- if ($lastRequestId) {
+ if ($lastRequestId !== null) {
$session->set(SamlAuthenticator::LAST_REQUEST_ID, $lastRequestId);
}
$request->setSession($session);
- $idpResolver = $this->createConfiguredMock(IdpResolverInterface::class, [
- 'resolve' => null,
- ]);
+ try {
+ $settings = self::createStub(Settings::class);
+ $settings->method('getSecurityData')
+ ->willReturn(['rejectUnsolicitedResponsesWithInResponseTo' => (bool) $lastRequestId])
+ ;
+
+ $auth = $this->createMock(Auth::class);
+ if ($friendly) {
+ $auth->method('getAttributesWithFriendlyName')->willReturn($attributes);
+ } else {
+ $auth->method('getAttributes')->willReturn($attributes);
+ }
+
+ $auth->method('getSessionIndex')->willReturn($sessionIndex);
+ $auth->method('getSettings')->willReturn($settings);
+ $auth->method('getErrors')->willReturn([]);
+ $auth->method('processResponse')->with($lastRequestId);
+ $auth->method('getNameId')->willReturn($nameId);
+
+ $userProvider = self::createStub(UserProviderInterface::class);
+ if ($userFound) {
+ $user = $this->createConfiguredMock(SamlUserInterface::class, [
+ 'getUserIdentifier' => $expectedIdentifier,
+ ]);
+ $user->method('setSamlAttributes');
+ $userProvider->method('loadUserByIdentifier')->willReturn($user);
+ } else {
+ $userProvider->method('loadUserByIdentifier')->willThrowException(new UserNotFoundException());
+ }
+
+ $samlUserFactoryMock = null;
+ if ($samlUserFactory) {
+ $user = $this->createConfiguredMock(SamlUserInterface::class, [
+ 'getUserIdentifier' => $expectedIdentifier,
+ ]);
+ $user->method('setSamlAttributes');
+
+ $samlUserFactoryMock = $this->createMock(SamlUserFactoryInterface::class);
+ $samlUserFactoryMock
+ ->method('createUser')
+ ->willReturn($user)
+ ;
+ }
+
+ $eventDispatcherMock = null;
+ if (isset($expectedEventClass) && $expectedEventClass !== '') {
+ $eventDispatcherMock = $this->createMock(EventDispatcherInterface::class);
+ $eventDispatcherMock
+ ->expects(self::once())
+ ->method('dispatch')
+ ->with(self::isInstanceOf($expectedEventClass))
+ ;
+ }
+ $idpResolver = $this->createConfiguredMock(IdpResolverInterface::class, ['resolve' => ['idp' => 'foo', 'sp' => 'boo']]);
+ } catch (MockException $e) {
+ self::fail('Failed to create mocks for OneLogin Auth. '.$e->getMessage());
+ }
$authRegistry = new AuthRegistry();
- $authRegistry->addService('foo', $auth);
+ $authRegistry->addService('foo', 'boo', $auth);
+
+ $options = ['use_attribute_friendly_name' => $friendly];
+ if ($identifier_attribute !== null) {
+ $options['identifier_attribute'] = $identifier_attribute;
+ }
+
+ self::assertFalse(Utils::getProxyVars());
$authenticator = $this->createSamlAuthenticator(
userProvider: $userProvider,
idpResolver: $idpResolver,
authRegistry: $authRegistry,
options: $options,
- samlUserFactory: $samlUserFactory,
- useProxyVars: $expectedUseProxyVars,
+ samlUserFactory: $samlUserFactoryMock,
+ useProxyVars: $useProxyVars,
);
- self::assertFalse(Utils::getProxyVars());
$passport = $authenticator->authenticate($request);
- self::assertSame($expectedUseProxyVars, Utils::getProxyVars());
- self::assertSame($expectedUserIdentifier, $passport->getUser()->getUserIdentifier());
+
+ self::assertSame($useProxyVars, Utils::getProxyVars());
+ self::assertSame($expectedIdentifier, $passport->getUser()->getUserIdentifier());
/** @var SamlAttributesBadge $badge */
$badge = $passport->getBadge(SamlAttributesBadge::class);
- self::assertSame($expectedSamlAttributes, $badge->getAttributes());
-
- if (!$eventDispatcher) {
- return;
+ $expectedAttrs = $attributes;
+ $expectedAttrs[SamlAuthenticator::SESSION_INDEX_ATTRIBUTE] = $sessionIndex;
+ self::assertSame($expectedAttrs, $badge->getAttributes());
+
+ if (isset($expectedEventClass) && $expectedEventClass !== '') {
+ $deferredBadge = $passport->getBadge(DeferredEventBadge::class);
+ self::assertInstanceOf(DeferredEventBadge::class, $deferredBadge);
+ $event = $deferredBadge->getEvent();
+ if ($event instanceof Event) {
+ /** @var EventDispatcherInterface&MockObject $eventDispatcherMock */
+ $eventDispatcherMock->dispatch($event);
+ } else {
+ self::fail('Expected event is not set in DeferredEventBadge.');
+ }
+
+ if ($expectedEventClass === UserCreatedEvent::class) {
+ self::assertInstanceOf(UserCreatedEvent::class, $deferredBadge->getEvent());
+ } elseif ($expectedEventClass === UserModifiedEvent::class) {
+ self::assertInstanceOf(UserModifiedEvent::class, $deferredBadge->getEvent());
+ } else {
+ self::fail('Unexpected event class: '.$expectedEventClass);
+ }
}
-
- /** @var DeferredEventBadge $deferredEventBadge */
- $deferredEventBadge = $passport->getBadge(DeferredEventBadge::class);
- self::assertInstanceOf(DeferredEventBadge::class, $deferredEventBadge);
-
- /** @var Event $deferredEvent */
- $deferredEvent = $deferredEventBadge->getEvent();
- self::assertInstanceOf(Event::class, $deferredEvent);
-
- $eventDispatcher->dispatch($deferredEvent);
}
- public function provideSuccessAuthenticateCases(): iterable
+ /** @return iterable> */
+ public static function provideSuccessAuthenticateCases(): iterable
{
- yield 'Not attribute friendly name + user identifier from OneLogin auth' => (function (): array {
- $settingsMock = $this->createMock(Settings::class);
- $settingsMock
- ->method('getSecurityData')
- ->willReturn([])
- ;
- $auth = $this->createConfiguredMock(Auth::class, [
- 'getAttributes' => [
- 'username' => 'tester',
- 'email' => 'tester@example.com',
- ],
- 'getSessionIndex' => 'session_index',
- 'getSettings' => $settingsMock,
- 'getNameId' => 'tester_id',
- ]);
- $auth
- ->expects(self::never())
- ->method('getAttributesWithFriendlyName')
- ;
- $auth
- ->method('processResponse')
- ->with(null)
- ;
-
- $userProvider = $this->createMock(UserProviderInterface::class);
- $userProvider
- ->method('loadUserByIdentifier')
- ->with('tester_id')
- ->willReturn(new TestUser('tester_id'))
- ;
-
- return [
- 'auth' => $auth,
- 'userProvider' => $userProvider,
- 'samlUserFactory' => null,
- 'eventDispatcher' => null,
- 'options' => [
- 'use_attribute_friendly_name' => false,
- ],
- 'lastRequestId' => null,
- 'useProxyVars' => false,
- 'expectedUserIdentifier' => 'tester_id',
- 'expectedSamlAttributes' => [
- 'username' => 'tester',
- 'email' => 'tester@example.com',
- SamlAuthenticator::SESSION_INDEX_ATTRIBUTE => 'session_index',
- ],
- 'expectedUseProxyVars' => false,
- ];
- })();
-
- yield 'Attribute friendly name + user identifier from SAML attributes (array) + SamlUser created' => (function (): array {
- $settingsMock = $this->createMock(Settings::class);
- $settingsMock
- ->method('getSecurityData')
- ->willReturn(['rejectUnsolicitedResponsesWithInResponseTo' => false])
- ;
- $auth = $this->createConfiguredMock(Auth::class, [
- 'getAttributesWithFriendlyName' => [
- 'username' => ['tester_attribute'],
- 'email' => 'tester@example.com',
- ],
- 'getSessionIndex' => 'session_index',
- 'getSettings' => $settingsMock,
- ]);
- $auth
- ->expects(self::never())
- ->method('getAttributes')
- ;
- $auth
- ->expects(self::never())
- ->method('getNameId')
- ;
- $auth
- ->method('processResponse')
- ->with(null)
- ;
-
- $userProvider = $this->createMock(UserProviderInterface::class);
- $userProvider
- ->method('loadUserByIdentifier')
- ->willThrowException(new UserNotFoundException())
- ;
-
- $user = $this->createConfiguredMock(SamlUserInterface::class, [
- 'getUserIdentifier' => 'tester_attribute',
- ]);
- $user
- ->expects(self::never())
- ->method('setSamlAttributes')
- ;
-
- $samlUserFactory = $this->createMock(SamlUserFactoryInterface::class);
- $samlUserFactory
- ->method('createUser')
- ->with('tester_attribute', [
- 'username' => ['tester_attribute'],
- 'email' => 'tester@example.com',
- SamlAuthenticator::SESSION_INDEX_ATTRIBUTE => 'session_index',
- ])
- ->willReturn($user)
- ;
-
- $eventDispatcher = $this->createMock(EventDispatcherInterface::class);
- $eventDispatcher
- ->expects(self::once())
- ->method('dispatch')
- ->with(self::isInstanceOf(UserCreatedEvent::class))
- ;
-
- return [
- 'auth' => $auth,
- 'userProvider' => $userProvider,
- 'samlUserFactory' => $samlUserFactory,
- 'eventDispatcher' => $eventDispatcher,
- 'options' => [
- 'use_attribute_friendly_name' => true,
- 'identifier_attribute' => 'username',
- ],
- 'lastRequestId' => null,
- 'useProxyVars' => false,
- 'expectedUserIdentifier' => 'tester_attribute',
- 'expectedSamlAttributes' => [
- 'username' => ['tester_attribute'],
- 'email' => 'tester@example.com',
- SamlAuthenticator::SESSION_INDEX_ATTRIBUTE => 'session_index',
- ],
- 'expectedUseProxyVars' => false,
- ];
- })();
-
- yield 'Attribute friendly name + user identifier from SAML attributes (string) + SamlUser modified + InResponseTo' => (function (): array {
- $settingsMock = $this->createMock(Settings::class);
- $settingsMock
- ->method('getSecurityData')
- ->willReturn(['rejectUnsolicitedResponsesWithInResponseTo' => true])
- ;
- $auth = $this->createConfiguredMock(Auth::class, [
- 'getAttributesWithFriendlyName' => [
- 'username' => 'tester_attribute',
- 'email' => 'tester@example.com',
- ],
- 'getSessionIndex' => 'session_index',
- 'getSettings' => $settingsMock,
- ]);
- $auth
- ->expects(self::never())
- ->method('getAttributes')
- ;
- $auth
- ->expects(self::never())
- ->method('getNameId')
- ;
- $auth
- ->method('processResponse')
- ->with('requestID')
- ;
-
- $user = $this->createConfiguredMock(SamlUserInterface::class, [
- 'getUserIdentifier' => 'tester_attribute',
- ]);
- $user
- ->method('setSamlAttributes')
- ->with([
- 'username' => 'tester_attribute',
- 'email' => 'tester@example.com',
- SamlAuthenticator::SESSION_INDEX_ATTRIBUTE => 'session_index',
- ])
- ;
-
- $userProvider = $this->createMock(UserProviderInterface::class);
- $userProvider
- ->method('loadUserByIdentifier')
- ->with('tester_attribute')
- ->willReturn($user)
- ;
+ yield 'Not attribute friendly name + user identifier from OneLogin auth' => [
+ 'attributes' => ['username' => 'tester', 'email' => 'tester@example.com'],
+ 'friendly' => false,
+ 'identifier_attribute' => null,
+ 'nameId' => 'tester_id',
+ 'sessionIndex' => 'session_index',
+ 'userFound' => true,
+ 'samlUserFactory' => false,
+ 'lastRequestId' => null,
+ 'useProxyVars' => false,
+ 'expectedIdentifier' => 'tester_id',
+ 'expectedEventClass' => null,
+ ];
- $eventDispatcher = $this->createMock(EventDispatcherInterface::class);
- $eventDispatcher
- ->expects(self::once())
- ->method('dispatch')
- ->with(self::isInstanceOf(UserModifiedEvent::class))
- ;
+ yield 'Friendly name + user created via factory' => [
+ 'attributes' => ['username' => ['tester_attribute'], 'email' => 'tester@example.com'],
+ 'friendly' => true,
+ 'identifier_attribute' => 'username',
+ 'nameId' => null,
+ 'sessionIndex' => 'session_index',
+ 'userFound' => false,
+ 'samlUserFactory' => true,
+ 'lastRequestId' => null,
+ 'useProxyVars' => false,
+ 'expectedIdentifier' => 'tester_attribute',
+ 'expectedEventClass' => UserCreatedEvent::class,
+ ];
- return [
- 'auth' => $auth,
- 'userProvider' => $userProvider,
- 'samlUserFactory' => null,
- 'eventDispatcher' => $eventDispatcher,
- 'options' => [
- 'use_attribute_friendly_name' => true,
- 'identifier_attribute' => 'username',
- ],
- 'lastRequestId' => 'requestID',
- 'useProxyVars' => true,
- 'expectedUserIdentifier' => 'tester_attribute',
- 'expectedSamlAttributes' => [
- 'username' => 'tester_attribute',
- 'email' => 'tester@example.com',
- SamlAuthenticator::SESSION_INDEX_ATTRIBUTE => 'session_index',
- ],
- 'expectedUseProxyVars' => true,
- ];
- })();
+ yield 'Friendly name + user loaded and modified' => [
+ 'attributes' => ['username' => 'tester_attribute', 'email' => 'tester@example.com'],
+ 'friendly' => true,
+ 'identifier_attribute' => 'username',
+ 'nameId' => null,
+ 'sessionIndex' => 'session_index',
+ 'userFound' => true,
+ 'samlUserFactory' => false,
+ 'lastRequestId' => 'requestID',
+ 'useProxyVars' => true,
+ 'expectedIdentifier' => 'tester_attribute',
+ 'expectedEventClass' => UserModifiedEvent::class,
+ ];
}
/**
- * @dataProvider provideAuthenticateExceptionCases
- *
+ * @param array $attributes
+ * @param array $options
* @param class-string<\Throwable> $expectedException
*/
- public function testAuthenticateException(
- Auth $auth,
- ?UserProviderInterface $userProvider,
- ?SamlUserFactoryInterface $samlUserFactory,
+ #[DataProvider('provideAuthenticateExceptionWithProviderCases')]
+ public function testAuthenticateExceptionWithProvider(
+ array $attributes,
+ ?string $nameId,
+ ?string $userProviderFlag,
+ ?string $samlUserFactoryFlag,
array $options,
string $expectedException,
?string $expectedMessage,
@@ -521,12 +440,48 @@ public function testAuthenticateException(
$request = Request::create('/');
$request->setSession(new Session(new MockArraySessionStorage()));
- $idpResolver = $this->createConfiguredMock(IdpResolverInterface::class, [
- 'resolve' => null,
- ]);
+ try {
+ $settingsMock = self::createConfiguredMock(Settings::class, [
+ 'getSecurityData' => [],
+ ]);
+
+ $auth = self::createConfiguredMock(Auth::class, [
+ 'getAttributes' => $attributes,
+ 'getSessionIndex' => 'session_index',
+ 'getSettings' => $settingsMock,
+ 'getErrors' => [],
+ ]);
+ if ($nameId !== null) {
+ $auth->method('getNameId')->willReturn($nameId);
+ }
+
+ $userProvider = null;
+ if ($userProviderFlag === 'not_found') {
+ $userProvider = self::createMock(UserProviderInterface::class);
+ $userProvider
+ ->method('loadUserByIdentifier')
+ ->willThrowException(new UserNotFoundException())
+ ;
+ }
+
+ $samlUserFactory = null;
+ if ($samlUserFactoryFlag === 'factory_fails') {
+ $samlUserFactory = self::createMock(SamlUserFactoryInterface::class);
+ $samlUserFactory
+ ->method('createUser')
+ ->willThrowException(new \Exception())
+ ;
+ }
+
+ $idpResolver = self::createConfiguredMock(IdpResolverInterface::class, [
+ 'resolve' => ['idp' => 'foo', 'sp' => 'boo'],
+ ]);
+ } catch (MockException $e) {
+ self::fail('Failed to create mocks for OneLogin Auth. '.$e->getMessage());
+ }
$authRegistry = new AuthRegistry();
- $authRegistry->addService('foo', $auth);
+ $authRegistry->addService('foo', 'boo', $auth);
$authenticator = $this->createSamlAuthenticator(
userProvider: $userProvider,
@@ -544,137 +499,86 @@ public function testAuthenticateException(
$authenticator->authenticate($request)->getUser();
}
- public function provideAuthenticateExceptionCases(): iterable
+ /** @return iterable> */
+ public static function provideAuthenticateExceptionWithProviderCases(): iterable
{
- yield 'SAML attributes without identifier attribute' => (function (): array {
- $settingsMock = $this->createMock(Settings::class);
- $settingsMock
- ->method('getSecurityData')
- ->willReturn([])
- ;
- $auth = $this->createConfiguredMock(Auth::class, [
- 'getAttributes' => [],
- 'getSessionIndex' => 'session_index',
- 'getSettings' => $settingsMock,
- ]);
- $auth
- ->expects(self::never())
- ->method('getNameId')
- ;
-
- return [
- 'auth' => $auth,
- 'userProvider' => null,
- 'samlUserFactory' => null,
- 'options' => [
- 'identifier_attribute' => 'username',
- ],
- 'expectedException' => \RuntimeException::class,
- 'expectedMessage' => 'Attribute "username" not found in SAML data.',
- ];
- })();
-
- yield 'SAML attributes with invalid identifier attribute' => (function (): array {
- $settingsMock = $this->createMock(Settings::class);
- $settingsMock
- ->method('getSecurityData')
- ->willReturn([])
- ;
- $auth = $this->createConfiguredMock(Auth::class, [
- 'getAttributes' => [
- 'username' => [],
- ],
- 'getSessionIndex' => 'session_index',
- 'getSettings' => $settingsMock,
- ]);
- $auth
- ->expects(self::never())
- ->method('getNameId')
- ;
-
- return [
- 'auth' => $auth,
- 'userProvider' => null,
- 'samlUserFactory' => null,
- 'options' => [
- 'identifier_attribute' => 'username',
- ],
- 'expectedException' => \RuntimeException::class,
- 'expectedMessage' => 'Attribute "username" does not contain valid user identifier.',
- ];
- })();
-
- yield 'User not found without SAML user factory' => (function (): array {
- $settingsMock = $this->createMock(Settings::class);
- $settingsMock
- ->method('getSecurityData')
- ->willReturn([])
- ;
- $auth = $this->createConfiguredMock(Auth::class, [
- 'getAttributes' => [],
- 'getSessionIndex' => 'session_index',
- 'getSettings' => $settingsMock,
- 'getNameId' => 'tester_id',
- ]);
- $auth
- ->expects(self::never())
- ->method('getAttributesWithFriendlyName')
- ;
-
- $userProvider = $this->createMock(UserProviderInterface::class);
- $userProvider
- ->method('loadUserByIdentifier')
- ->willThrowException(new UserNotFoundException())
- ;
+ yield 'missing identifier attribute' => [
+ [],
+ null,
+ null,
+ null,
+ ['identifier_attribute' => 'username'],
+ \RuntimeException::class,
+ 'Attribute "username" not found in SAML data.',
+ ];
- return [
- 'auth' => $auth,
- 'userProvider' => $userProvider,
- 'samlUserFactory' => null,
- 'options' => [],
- 'expectedException' => UserNotFoundException::class,
- 'expectedMessage' => null,
- ];
- })();
-
- yield 'User not found + SAML user factory exception' => (function (): array {
- $settingsMock = $this->createMock(Settings::class);
- $settingsMock
- ->method('getSecurityData')
- ->willReturn([])
- ;
- $auth = $this->createConfiguredMock(Auth::class, [
- 'getAttributes' => [],
- 'getSessionIndex' => 'session_index',
- 'getSettings' => $settingsMock,
- 'getNameId' => 'tester_id',
- ]);
- $auth
- ->expects(self::never())
- ->method('getAttributesWithFriendlyName')
- ;
+ yield 'invalid identifier attribute (empty array)' => [
+ ['username' => []],
+ null,
+ null,
+ null,
+ ['identifier_attribute' => 'username'],
+ \RuntimeException::class,
+ 'Attribute "username" does not contain valid user identifier.',
+ ];
- $userProvider = $this->createMock(UserProviderInterface::class);
- $userProvider
- ->method('loadUserByIdentifier')
- ->willThrowException(new UserNotFoundException())
- ;
+ yield 'user not found, no user factory' => [
+ [],
+ 'tester_id',
+ 'not_found',
+ null,
+ [],
+ UserNotFoundException::class,
+ null,
+ ];
- $samlUserFactory = $this->createMock(SamlUserFactoryInterface::class);
- $samlUserFactory
- ->method('createUser')
- ->willThrowException(new \Exception())
- ;
+ yield 'user factory fails' => [
+ [],
+ 'tester_id',
+ 'not_found',
+ 'factory_fails',
+ [],
+ AuthenticationException::class,
+ 'The authentication failed.',
+ ];
+ }
- return [
- 'auth' => $auth,
- 'userProvider' => $userProvider,
- 'samlUserFactory' => $samlUserFactory,
- 'options' => [],
- 'expectedException' => AuthenticationException::class,
- 'expectedMessage' => 'The authentication failed.',
- ];
- })();
+ /**
+ * @param UserProviderInterface|null $userProvider
+ * @param array $options
+ */
+ private function createSamlAuthenticator(
+ ?HttpUtils $httpUtils = null,
+ ?UserProviderInterface $userProvider = null,
+ ?IdpResolverInterface $idpResolver = null,
+ ?AuthRegistryInterface $authRegistry = null,
+ ?AuthenticationSuccessHandlerInterface $authenticationSuccessHandler = null,
+ ?AuthenticationFailureHandlerInterface $authenticationFailureHandler = null,
+ array $options = [],
+ ?SamlUserFactoryInterface $samlUserFactory = null,
+ ?LoggerInterface $logger = null,
+ string $idpParameterName = 'idp',
+ string $spParameterName = 'sp',
+ bool $useProxyVars = false,
+ ): SamlAuthenticator {
+ try {
+ return new SamlAuthenticator(
+ $httpUtils ?? self::createStub(HttpUtils::class),
+ $userProvider ?? self::createStub(UserProviderInterface::class),
+ $idpResolver ?? self::createStub(IdpResolverInterface::class),
+ $authRegistry ?? self::createStub(AuthRegistryInterface::class),
+ $authenticationSuccessHandler ?? self::createStub(AuthenticationSuccessHandlerInterface::class),
+ $authenticationFailureHandler ?? self::createStub(AuthenticationFailureHandlerInterface::class),
+ $options,
+ $samlUserFactory,
+ $logger,
+ $idpParameterName,
+ $spParameterName,
+ $useProxyVars,
+ );
+ } catch (\Throwable $e) {
+ self::fail('Failed to create SamlAuthenticator instance.'.$e->getMessage());
+ }
}
public function testCreateToken(): void
@@ -685,12 +589,12 @@ public function testCreateToken(): void
[new SamlAttributesBadge(['username' => 'tester'])],
);
- /** @var \Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken $token */
- $token = $authenticator->createToken($passport, 'fwname');
+ /** @var PostAuthenticationToken $token */
+ $token = $authenticator->createToken($passport, 'firewallName');
self::assertSame('tester', $token->getUserIdentifier());
self::assertSame(['ROLE_EXTRA_USER'], $token->getRoleNames());
- self::assertSame('fwname', $token->getFirewallName());
+ self::assertSame('firewallName', $token->getFirewallName());
self::assertSame(['username' => 'tester'], $token->getAttributes());
}
@@ -702,72 +606,49 @@ public function testCreateTokenWithoutSamlAttributesBadgeException(): void
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Passport should contains a "Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Passport\Badge\SamlAttributesBadge" badge.');
- $authenticator->createToken($passport, 'foo');
+ $authenticator->createToken($passport, 'firewallName');
}
public function testOnAuthenticationSuccess(): void
{
- $request = $this->createStub(Request::class);
- $token = $this->createStub(TokenInterface::class);
+ try {
+ $request = self::createStub(Request::class);
+ $token = self::createStub(TokenInterface::class);
- $authenticationSuccessHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class);
- $authenticationSuccessHandler
- ->expects(self::once())
- ->method('onAuthenticationSuccess')
- ->with($request, $token)
- ;
+ $authenticationSuccessHandler = self::createMock(AuthenticationSuccessHandlerInterface::class);
+ $authenticationSuccessHandler
+ ->method('onAuthenticationSuccess')
+ ->with($request, $token)
+ ;
+ } catch (MockException $e) {
+ self::fail('Failed to create mock for AuthenticationSuccessHandlerInterface. '.$e->getMessage());
+ }
$authenticator = $this->createSamlAuthenticator(
authenticationSuccessHandler: $authenticationSuccessHandler,
);
- $authenticator->onAuthenticationSuccess($request, $token, 'foo');
+ $authenticator->onAuthenticationSuccess($request, $token, 'firewallName');
}
public function testOnAuthenticationFailure(): void
{
- $request = $this->createStub(Request::class);
- $exception = new AuthenticationException();
-
- $authenticationFailureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class);
- $authenticationFailureHandler
- ->expects(self::once())
- ->method('onAuthenticationFailure')
- ->with($request, $exception)
- ;
+ try {
+ $request = self::createStub(Request::class);
+ $exception = new AuthenticationException();
+ $authenticationFailureHandler = self::createMock(AuthenticationFailureHandlerInterface::class);
+ $authenticationFailureHandler
+ ->method('onAuthenticationFailure')
+ ->with($request, $exception)
+ ;
+ } catch (MockException $e) {
+ self::fail('Failed to create mock for AuthenticationFailureHandlerInterface. '.$e->getMessage());
+ }
$authenticator = $this->createSamlAuthenticator(
authenticationFailureHandler: $authenticationFailureHandler,
);
$authenticator->onAuthenticationFailure($request, $exception);
}
-
- private function createSamlAuthenticator(
- ?HttpUtils $httpUtils = null,
- ?UserProviderInterface $userProvider = null,
- ?IdpResolverInterface $idpResolver = null,
- ?AuthRegistryInterface $authRegistry = null,
- ?AuthenticationSuccessHandlerInterface $authenticationSuccessHandler = null,
- ?AuthenticationFailureHandlerInterface $authenticationFailureHandler = null,
- array $options = [],
- ?SamlUserFactoryInterface $samlUserFactory = null,
- ?LoggerInterface $logger = null,
- string $idpParameterName = 'idp',
- bool $useProxyVars = false,
- ): SamlAuthenticator {
- return new SamlAuthenticator(
- $httpUtils ?? $this->createStub(HttpUtils::class),
- $userProvider ?? $this->createStub(UserProviderInterface::class),
- $idpResolver ?? $this->createStub(IdpResolverInterface::class),
- $authRegistry ?? $this->createStub(AuthRegistryInterface::class),
- $authenticationSuccessHandler ?? $this->createStub(AuthenticationSuccessHandlerInterface::class),
- $authenticationFailureHandler ?? $this->createStub(AuthenticationFailureHandlerInterface::class),
- $options,
- $samlUserFactory,
- $logger,
- $idpParameterName,
- $useProxyVars,
- );
- }
}
diff --git a/tests/Security/Http/Authenticator/Token/SamlTokenTest.php b/tests/Security/Http/Authenticator/Token/SamlTokenTest.php
index 104c34e..53a259e 100644
--- a/tests/Security/Http/Authenticator/Token/SamlTokenTest.php
+++ b/tests/Security/Http/Authenticator/Token/SamlTokenTest.php
@@ -7,29 +7,32 @@
use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken;
use Nbgrp\Tests\OneloginSamlBundle\TestUser;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
/**
- * @covers \Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken
- *
* @internal
*/
+#[CoversClass(SamlToken::class)]
final class SamlTokenTest extends TestCase
{
/**
- * @dataProvider provideTokenCases
+ * @param array $attributes
*/
+ #[DataProvider('provideTokenCases')]
public function testToken(array $attributes): void
{
$user = new TestUser('tester');
- $token = new SamlToken($user, 'fwname', ['ROLE_USER', 'ROLE_EXTRA'], $attributes);
+ $token = new SamlToken($user, 'firewallName', ['ROLE_USER', 'ROLE_EXTRA'], $attributes);
self::assertSame($token->getUserIdentifier(), 'tester');
self::assertSame($token->getRoleNames(), ['ROLE_USER', 'ROLE_EXTRA']);
self::assertSame($token->getAttributes(), $attributes);
}
- public function provideTokenCases(): iterable
+ /** @return iterable> */
+ public static function provideTokenCases(): iterable
{
yield 'Empty attributes' => [
'attributes' => [],
diff --git a/tests/Security/User/SamlUserFactoryTest.php b/tests/Security/User/SamlUserFactoryTest.php
index e22ab61..dc26cc3 100644
--- a/tests/Security/User/SamlUserFactoryTest.php
+++ b/tests/Security/User/SamlUserFactoryTest.php
@@ -7,15 +7,18 @@
use Nbgrp\OneloginSamlBundle\Security\User\SamlUserFactory;
use Nbgrp\Tests\OneloginSamlBundle\TestUser;
+use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
/**
- * @covers \Nbgrp\OneloginSamlBundle\Security\User\SamlUserFactory
- *
* @internal
*/
+#[CoversClass(SamlUserFactory::class)]
final class SamlUserFactoryTest extends TestCase
{
+ /**
+ * @throws \ReflectionException
+ */
public function testCreateUser(): void
{
$factory = new SamlUserFactory(TestUser::class, [
@@ -34,6 +37,9 @@ public function testCreateUser(): void
self::assertSame('tester@example.com', $user->getEmail());
}
+ /**
+ * @throws \ReflectionException
+ */
public function testCreateUserException(): void
{
$factory = new SamlUserFactory(TestUser::class, [
diff --git a/tests/Security/User/SamlUserProviderTest.php b/tests/Security/User/SamlUserProviderTest.php
index 4969ab4..0bb2248 100644
--- a/tests/Security/User/SamlUserProviderTest.php
+++ b/tests/Security/User/SamlUserProviderTest.php
@@ -5,18 +5,18 @@
namespace Nbgrp\Tests\OneloginSamlBundle\Security\User;
+use Nbgrp\OneloginSamlBundle\Security\User\SamlUserInterface;
use Nbgrp\OneloginSamlBundle\Security\User\SamlUserProvider;
use Nbgrp\Tests\OneloginSamlBundle\TestUser;
+use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\InMemoryUser;
-use Symfony\Component\Security\Core\User\UserInterface;
/**
- * @covers \Nbgrp\OneloginSamlBundle\Security\User\SamlUserProvider
- *
* @internal
*/
+#[CoversClass(SamlUserProvider::class)]
final class SamlUserProviderTest extends TestCase
{
public function testLoadUserByIdentifier(): void
@@ -53,7 +53,7 @@ public function testSupportsClass(): void
public function testSupportsSubclass(): void
{
- $provider = new SamlUserProvider(UserInterface::class, []);
+ $provider = new SamlUserProvider(SamlUserInterface::class, []);
self::assertTrue($provider->supportsClass(TestUser::class));
}
@@ -66,7 +66,7 @@ public function testNotSupports(): void
public function testInvalidUserClass(): void
{
$this->expectException(\InvalidArgumentException::class);
- $this->expectExceptionMessage('The $userClass argument should be a class implementing the Symfony\Component\Security\Core\User\UserInterface interface.');
+ $this->expectExceptionMessage('The $userClass argument should be a class implementing the Nbgrp\OneloginSamlBundle\Security\User\SamlUserInterface');
/**
* @psalm-suppress InvalidArgument
* @phpstan-ignore-next-line
diff --git a/tests/TestUser.php b/tests/TestUser.php
index e797ea1..ab8d97f 100644
--- a/tests/TestUser.php
+++ b/tests/TestUser.php
@@ -5,9 +5,9 @@
namespace Nbgrp\Tests\OneloginSamlBundle;
-use Symfony\Component\Security\Core\User\UserInterface;
+use Nbgrp\OneloginSamlBundle\Security\User\SamlUserInterface;
-final class TestUser implements UserInterface
+final class TestUser implements SamlUserInterface
{
private string $email;
@@ -42,4 +42,6 @@ public function getEmail(): string
{
return $this->email;
}
+
+ public function setSamlAttributes(array $attributes): void {}
}