diff --git a/Resources/config/security.php b/Resources/config/security.php
index 7411c6dc..7b08ebe5 100644
--- a/Resources/config/security.php
+++ b/Resources/config/security.php
@@ -31,7 +31,9 @@
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
+use Symfony\Component\Security\Core\Authorization\UserAuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
+use Symfony\Component\Security\Core\Authorization\Voter\ClosureVoter;
use Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter;
use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter;
use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter;
@@ -66,6 +68,7 @@
service('security.access.decision_manager'),
])
->alias(AuthorizationCheckerInterface::class, 'security.authorization_checker')
+ ->alias(UserAuthorizationCheckerInterface::class, 'security.authorization_checker')
->set('security.token_storage', UsageTrackingTokenStorage::class)
->args([
@@ -85,6 +88,7 @@
service_locator([
'security.token_storage' => service('security.token_storage'),
'security.authorization_checker' => service('security.authorization_checker'),
+ 'security.user_authorization_checker' => service('security.authorization_checker'),
'security.authenticator.managers_locator' => service('security.authenticator.managers_locator')->ignoreOnInvalid(),
'request_stack' => service('request_stack'),
'security.firewall.map' => service('security.firewall.map'),
@@ -162,6 +166,12 @@
])
->tag('security.voter', ['priority' => 245])
+ ->set('security.access.closure_voter', ClosureVoter::class)
+ ->args([
+ service('security.authorization_checker'),
+ ])
+ ->tag('security.voter', ['priority' => 245])
+
->set('security.impersonate_url_generator', ImpersonateUrlGenerator::class)
->args([
service('request_stack'),
diff --git a/Resources/config/security_authenticator.php b/Resources/config/security_authenticator.php
index 1ea4ef55..babcdb82 100644
--- a/Resources/config/security_authenticator.php
+++ b/Resources/config/security_authenticator.php
@@ -42,7 +42,7 @@
abstract_arg('provider key'),
service('logger')->nullOnInvalid(),
param('security.authentication.manager.erase_credentials'),
- param('security.authentication.hide_user_not_found'),
+ param('.security.authentication.expose_security_errors'),
abstract_arg('required badges'),
])
->tag('monolog.logger', ['channel' => 'security'])
diff --git a/Resources/config/security_authenticator_access_token.php b/Resources/config/security_authenticator_access_token.php
index c0fced49..9099bad4 100644
--- a/Resources/config/security_authenticator_access_token.php
+++ b/Resources/config/security_authenticator_access_token.php
@@ -15,6 +15,15 @@
use Jose\Component\Core\AlgorithmManagerFactory;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
+use Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256;
+use Jose\Component\Encryption\Algorithm\ContentEncryption\A128GCM;
+use Jose\Component\Encryption\Algorithm\ContentEncryption\A192CBCHS384;
+use Jose\Component\Encryption\Algorithm\ContentEncryption\A192GCM;
+use Jose\Component\Encryption\Algorithm\ContentEncryption\A256CBCHS512;
+use Jose\Component\Encryption\Algorithm\ContentEncryption\A256GCM;
+use Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHES;
+use Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHSS;
+use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\Algorithm\ES384;
use Jose\Component\Signature\Algorithm\ES512;
@@ -27,6 +36,7 @@
use Symfony\Component\Security\Http\AccessToken\ChainAccessTokenExtractor;
use Symfony\Component\Security\Http\AccessToken\FormEncodedBodyExtractor;
use Symfony\Component\Security\Http\AccessToken\HeaderAccessTokenExtractor;
+use Symfony\Component\Security\Http\AccessToken\OAuth2\Oauth2TokenHandler;
use Symfony\Component\Security\Http\AccessToken\Oidc\OidcTokenHandler;
use Symfony\Component\Security\Http\AccessToken\Oidc\OidcUserInfoTokenHandler;
use Symfony\Component\Security\Http\AccessToken\QueryAccessTokenExtractor;
@@ -82,6 +92,11 @@
service('clock'),
])
+ ->set('security.access_token_handler.oidc_discovery.http_client', HttpClientInterface::class)
+ ->abstract()
+ ->factory([service('http_client'), 'withOptions'])
+ ->args([abstract_arg('http client options')])
+
->set('security.access_token_handler.oidc.jwk', JWK::class)
->abstract()
->deprecate('symfony/security-http', '7.1', 'The "%service_id%" service is deprecated. Please use "security.access_token_handler.oidc.jwkset" instead')
@@ -135,5 +150,55 @@
->set('security.access_token_handler.oidc.signature.PS512', PS512::class)
->tag('security.access_token_handler.oidc.signature_algorithm')
+
+ // Encryption
+ // Note that - all xxxKW algorithms are not defined as an extra dependency is required
+ // - The RSA_1.5 is missing as deprecated
+ ->set('security.access_token_handler.oidc.encryption_algorithm_manager_factory', AlgorithmManagerFactory::class)
+ ->args([
+ tagged_iterator('security.access_token_handler.oidc.encryption_algorithm'),
+ ])
+
+ ->set('security.access_token_handler.oidc.encryption', AlgorithmManager::class)
+ ->abstract()
+ ->factory([service('security.access_token_handler.oidc.encryption_algorithm_manager_factory'), 'create'])
+ ->args([
+ abstract_arg('encryption algorithms'),
+ ])
+
+ ->set('security.access_token_handler.oidc.encryption.RSAOAEP', RSAOAEP::class)
+ ->tag('security.access_token_handler.oidc.encryption_algorithm')
+
+ ->set('security.access_token_handler.oidc.encryption.ECDHES', ECDHES::class)
+ ->tag('security.access_token_handler.oidc.encryption_algorithm')
+
+ ->set('security.access_token_handler.oidc.encryption.ECDHSS', ECDHSS::class)
+ ->tag('security.access_token_handler.oidc.encryption_algorithm')
+
+ ->set('security.access_token_handler.oidc.encryption.A128CBCHS256', A128CBCHS256::class)
+ ->tag('security.access_token_handler.oidc.encryption_algorithm')
+
+ ->set('security.access_token_handler.oidc.encryption.A192CBCHS384', A192CBCHS384::class)
+ ->tag('security.access_token_handler.oidc.encryption_algorithm')
+
+ ->set('security.access_token_handler.oidc.encryption.A256CBCHS512', A256CBCHS512::class)
+ ->tag('security.access_token_handler.oidc.encryption_algorithm')
+
+ ->set('security.access_token_handler.oidc.encryption.A128GCM', A128GCM::class)
+ ->tag('security.access_token_handler.oidc.encryption_algorithm')
+
+ ->set('security.access_token_handler.oidc.encryption.A192GCM', A192GCM::class)
+ ->tag('security.access_token_handler.oidc.encryption_algorithm')
+
+ ->set('security.access_token_handler.oidc.encryption.A256GCM', A256GCM::class)
+ ->tag('security.access_token_handler.oidc.encryption_algorithm')
+
+ // OAuth2 Introspection (RFC 7662)
+ ->set('security.access_token_handler.oauth2', Oauth2TokenHandler::class)
+ ->abstract()
+ ->args([
+ service('http_client'),
+ service('logger')->nullOnInvalid(),
+ ])
;
};
diff --git a/Resources/config/security_debug.php b/Resources/config/security_debug.php
index c98e3a69..76b4e31b 100644
--- a/Resources/config/security_debug.php
+++ b/Resources/config/security_debug.php
@@ -22,6 +22,7 @@
->args([
service('debug.security.access.decision_manager.inner'),
])
+ ->tag('kernel.reset', ['method' => 'reset', 'on_invalid' => 'ignore'])
->set('debug.security.voter.vote_listener', VoteListener::class)
->args([
diff --git a/Resources/views/Collector/security.html.twig b/Resources/views/Collector/security.html.twig
index 635d61e2..f2706858 100644
--- a/Resources/views/Collector/security.html.twig
+++ b/Resources/views/Collector/security.html.twig
@@ -571,14 +571,19 @@
{% endif %}
{% if voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_GRANTED') %}
- ACCESS GRANTED
+ GRANTED
{% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_ABSTAIN') %}
- ACCESS ABSTAIN
+ ABSTAIN
{% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_DENIED') %}
- ACCESS DENIED
+ DENIED
{% else %}
unknown ({{ voter_detail['vote'] }})
{% endif %}
+ {% if voter_detail['reasons'] is not empty %}
+ {% for voter_reason in voter_detail['reasons'] %}
+ {{ voter_reason }}
+ {% endfor %}
+ {% endif %}
|
{% endfor %}
diff --git a/Security.php b/Security.php
index 252fce01..2efbb67f 100644
--- a/Security.php
+++ b/Security.php
@@ -17,7 +17,9 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
+use Symfony\Component\Security\Core\Authorization\UserAuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\LogicException;
use Symfony\Component\Security\Core\Exception\LogoutException;
use Symfony\Component\Security\Core\User\UserInterface;
@@ -37,7 +39,7 @@
*
* @final
*/
-class Security implements AuthorizationCheckerInterface
+class Security implements AuthorizationCheckerInterface, UserAuthorizationCheckerInterface
{
public function __construct(
private readonly ContainerInterface $container,
@@ -57,10 +59,21 @@ public function getUser(): ?UserInterface
/**
* Checks if the attributes are granted against the current authentication token and optionally supplied subject.
*/
- public function isGranted(mixed $attributes, mixed $subject = null): bool
+ public function isGranted(mixed $attributes, mixed $subject = null, ?AccessDecision $accessDecision = null): bool
{
return $this->container->get('security.authorization_checker')
- ->isGranted($attributes, $subject);
+ ->isGranted($attributes, $subject, $accessDecision);
+ }
+
+ /**
+ * Checks if the attribute is granted against the user and optionally supplied subject.
+ *
+ * This should be used over isGranted() when checking permissions against a user that is not currently logged in or while in a CLI context.
+ */
+ public function isGrantedForUser(UserInterface $user, mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool
+ {
+ return $this->container->get('security.user_authorization_checker')
+ ->isGrantedForUser($user, $attribute, $subject, $accessDecision);
}
public function getToken(): ?TokenInterface
diff --git a/SecurityBundle.php b/SecurityBundle.php
index 3247ff12..1433b5c9 100644
--- a/SecurityBundle.php
+++ b/SecurityBundle.php
@@ -24,6 +24,7 @@
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\ReplaceDecoratedRememberMeHandlerPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\SortFirewallListenersPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\CasTokenHandlerFactory;
+use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OAuth2TokenHandlerFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcTokenHandlerFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcUserInfoTokenHandlerFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\ServiceTokenHandlerFactory;
@@ -80,6 +81,7 @@ public function build(ContainerBuilder $container): void
new OidcUserInfoTokenHandlerFactory(),
new OidcTokenHandlerFactory(),
new CasTokenHandlerFactory(),
+ new OAuth2TokenHandlerFactory(),
]));
$extension->addUserProviderFactory(new InMemoryFactory());
diff --git a/Tests/Command/DebugFirewallCommandTest.php b/Tests/Command/DebugFirewallCommandTest.php
new file mode 100644
index 00000000..673f0c43
--- /dev/null
+++ b/Tests/Command/DebugFirewallCommandTest.php
@@ -0,0 +1,197 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\SecurityBundle\Tests\Command;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Bundle\SecurityBundle\Command\DebugFirewallCommand;
+use Symfony\Bundle\SecurityBundle\Security\FirewallConfig;
+use Symfony\Bundle\SecurityBundle\Security\FirewallContext;
+use Symfony\Bundle\SecurityBundle\Tests\Fixtures\DummyAuthenticator;
+use Symfony\Component\Console\Tester\CommandTester;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
+
+class DebugFirewallCommandTest extends TestCase
+{
+ public function testFirewallListOutputMatchesFixture()
+ {
+ $firewallNames = ['main', 'api'];
+ $contexts = $this->createMock(ContainerInterface::class);
+ $eventDispatchers = $this->createMock(ContainerInterface::class);
+
+ $command = new DebugFirewallCommand($firewallNames, $contexts, $eventDispatchers, []);
+ $tester = new CommandTester($command);
+
+ $this->assertSame(0, $tester->execute([]));
+ $this->assertStringContainsString('Firewalls', $tester->getDisplay());
+ $this->assertStringContainsString('The following firewalls are defined:', $tester->getDisplay());
+ $this->assertStringContainsString('* main', $tester->getDisplay());
+ $this->assertStringContainsString('* api', $tester->getDisplay());
+ $this->assertStringContainsString('To view details of a specific firewall', $tester->getDisplay());
+ }
+
+ public function testFirewallNotFoundDisplaysError()
+ {
+ $firewallNames = ['main', 'api'];
+
+ $contexts = $this->createMock(ContainerInterface::class);
+ $contexts->method('has')->willReturn(false);
+
+ $eventDispatchers = $this->createMock(ContainerInterface::class);
+ $authenticators = [];
+
+ $command = new DebugFirewallCommand(
+ $firewallNames,
+ $contexts,
+ $eventDispatchers,
+ $authenticators
+ );
+
+ $tester = new CommandTester($command);
+
+ $this->assertSame(1, $tester->execute(['name' => 'admin']));
+ $this->assertStringContainsString('Firewall admin was not found.', $tester->getDisplay());
+ $this->assertStringContainsString('Available firewalls are: main, api', $tester->getDisplay());
+ }
+
+ public function testFirewallMainOutputMatchesFixture()
+ {
+ $firewallNames = ['main'];
+
+ $config = new FirewallConfig(
+ name: 'main',
+ userChecker: 'user_checker_service',
+ requestMatcher: null,
+ securityEnabled: true,
+ stateless: false,
+ provider: 'user_provider_service',
+ context: 'main',
+ entryPoint: 'entry_point_service',
+ accessDeniedHandler: 'access_denied_handler_service',
+ accessDeniedUrl: '/access-denied',
+ authenticators: [],
+ switchUser: null
+ );
+
+ $context = new FirewallContext([], config: $config);
+
+ $contexts = $this->createMock(ContainerInterface::class);
+ $contexts->method('has')->willReturn(true);
+ $contexts->method('get')->willReturn($context);
+
+ $eventDispatchers = $this->createMock(ContainerInterface::class);
+ $authenticator = new DummyAuthenticator();
+ $authenticators = ['main' => [$authenticator]];
+
+ $command = new DebugFirewallCommand($firewallNames, $contexts, $eventDispatchers, $authenticators);
+ $tester = new CommandTester($command);
+
+ $this->assertSame(0, $tester->execute(['name' => 'main', '--events' => true]));
+ $this->assertEquals($this->getFixtureOutput('firewall_main_output.txt'), trim(str_replace(\PHP_EOL, "\n", $tester->getDisplay())));
+ }
+
+ public function testFirewallWithEventsOutputMatchesFixture()
+ {
+ $firewallNames = ['main'];
+
+ $config = new FirewallConfig(
+ name: 'main',
+ userChecker: 'user_checker_service',
+ context: 'main',
+ stateless: false,
+ provider: 'user_provider_service',
+ entryPoint: 'entry_point_service',
+ accessDeniedHandler: 'access_denied_handler_service',
+ accessDeniedUrl: '/access-denied',
+ );
+
+ $context = new FirewallContext([], config: $config);
+
+ $contexts = $this->createMock(ContainerInterface::class);
+ $contexts->method('has')->willReturn(true);
+ $contexts->method('get')->willReturn($context);
+
+ $dispatcher = $this->createMock(EventDispatcherInterface::class);
+ $listener = fn () => null;
+ $listenerTwo = fn (int $number) => $number * 2;
+ $dispatcher->method('getListeners')->willReturn([
+ 'security.event' => [$listener, $listenerTwo],
+ ]);
+ $dispatcher->method('getListenerPriority')->willReturn(42);
+
+ $eventDispatchers = $this->createMock(ContainerInterface::class);
+ $eventDispatchers->method('has')->willReturn(true);
+ $eventDispatchers->method('get')->willReturn($dispatcher);
+
+ $authenticator = new DummyAuthenticator();
+ $authenticatorTwo = new DummyAuthenticator();
+ $authenticatorThree = new DummyAuthenticator();
+ $authenticators = ['main' => [$authenticator, $authenticatorTwo], 'api' => [$authenticatorThree]];
+
+ $command = new DebugFirewallCommand($firewallNames, $contexts, $eventDispatchers, $authenticators);
+ $tester = new CommandTester($command);
+
+ $this->assertSame(0, $tester->execute(['name' => 'main', '--events' => true]));
+ $this->assertEquals($this->getFixtureOutput('firewall_main_with_events_output.txt'), trim(str_replace(\PHP_EOL, "\n", $tester->getDisplay())));
+ }
+
+ public function testFirewallWithSwitchUserDisplaysSection()
+ {
+ $firewallNames = ['main'];
+
+ $switchUserConfig = [
+ 'parameter' => '_switch_user_test',
+ 'provider' => 'custom_provider_test',
+ 'role' => 'ROLE_ALLOWED_TO_SWITCH',
+ ];
+
+ $config = new FirewallConfig(
+ name: 'main',
+ userChecker: 'user_checker_service_test',
+ context: 'main',
+ stateless: false,
+ provider: 'user_provider_service_test',
+ entryPoint: 'entry_point_service_test',
+ accessDeniedHandler: 'access_denied_handler_service_test',
+ accessDeniedUrl: '/access-denied-test',
+ switchUser: $switchUserConfig,
+ );
+
+ $context = new FirewallContext([], config: $config);
+
+ $contexts = $this->createMock(ContainerInterface::class);
+ $contexts->method('has')->willReturn(true);
+ $contexts->method('get')->willReturn($context);
+
+ $eventDispatchers = $this->createMock(ContainerInterface::class);
+ $authenticator = new DummyAuthenticator();
+ $authenticatorTwo = $this->createMock(AuthenticatorInterface::class);
+ $authenticators = ['main' => [$authenticator], 'api' => [$authenticatorTwo]];
+
+ $command = new DebugFirewallCommand(
+ $firewallNames,
+ $contexts,
+ $eventDispatchers,
+ $authenticators
+ );
+ $tester = new CommandTester($command);
+
+ $this->assertSame(0, $tester->execute(['name' => 'main']));
+ $this->assertEquals($this->getFixtureOutput('firewall_main_with_switch_user.txt'), trim(str_replace(\PHP_EOL, "\n", $tester->getDisplay())));
+ }
+
+ private function getFixtureOutput(string $file): string
+ {
+ return trim(file_get_contents(__DIR__.'/../Fixtures/Descriptor/'.$file));
+ }
+}
diff --git a/Tests/DataCollector/SecurityDataCollectorTest.php b/Tests/DataCollector/SecurityDataCollectorTest.php
index 21161d28..df6bcd0c 100644
--- a/Tests/DataCollector/SecurityDataCollectorTest.php
+++ b/Tests/DataCollector/SecurityDataCollectorTest.php
@@ -28,6 +28,7 @@
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
use Symfony\Component\Security\Core\User\InMemoryUser;
@@ -40,7 +41,7 @@ class SecurityDataCollectorTest extends TestCase
{
public function testCollectWhenSecurityIsDisabled()
{
- $collector = new SecurityDataCollector(null, null, null, null, null, null, true);
+ $collector = new SecurityDataCollector(null, null, null, null, null, null);
$collector->collect(new Request(), new Response());
$this->assertSame('security', $collector->getName());
@@ -53,14 +54,14 @@ public function testCollectWhenSecurityIsDisabled()
$this->assertFalse($collector->supportsRoleHierarchy());
$this->assertCount(0, $collector->getRoles());
$this->assertCount(0, $collector->getInheritedRoles());
- $this->assertEmpty($collector->getUser());
+ $this->assertSame('', $collector->getUser());
$this->assertNull($collector->getFirewall());
}
public function testCollectWhenAuthenticationTokenIsNull()
{
$tokenStorage = new TokenStorage();
- $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy(), null, null, null, null, true);
+ $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy(), null, null, null, null);
$collector->collect(new Request(), new Response());
$this->assertTrue($collector->isEnabled());
@@ -72,7 +73,7 @@ public function testCollectWhenAuthenticationTokenIsNull()
$this->assertTrue($collector->supportsRoleHierarchy());
$this->assertCount(0, $collector->getRoles());
$this->assertCount(0, $collector->getInheritedRoles());
- $this->assertEmpty($collector->getUser());
+ $this->assertSame('', $collector->getUser());
$this->assertNull($collector->getFirewall());
}
@@ -82,7 +83,7 @@ public function testCollectAuthenticationTokenAndRoles(array $roles, array $norm
$tokenStorage = new TokenStorage();
$tokenStorage->setToken(new UsernamePasswordToken(new InMemoryUser('hhamon', 'P4$$w0rD', $roles), 'provider', $roles));
- $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy(), null, null, null, null, true);
+ $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy(), null, null, null, null);
$collector->collect(new Request(), new Response());
$collector->lateCollect();
@@ -105,7 +106,7 @@ public function testCollectSwitchUserToken()
$tokenStorage = new TokenStorage();
$tokenStorage->setToken(new SwitchUserToken(new InMemoryUser('hhamon', 'P4$$w0rD', ['ROLE_USER']), 'provider', ['ROLE_USER'], $adminToken));
- $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy(), null, null, null, null, true);
+ $collector = new SecurityDataCollector($tokenStorage, $this->getRoleHierarchy(), null, null, null, null);
$collector->collect(new Request(), new Response());
$collector->lateCollect();
@@ -135,7 +136,7 @@ public function testGetFirewall()
->with($request)
->willReturn($firewallConfig);
- $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()), true);
+ $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()));
$collector->collect($request, new Response());
$collector->lateCollect();
$collected = $collector->getFirewall();
@@ -159,7 +160,7 @@ public function testGetFirewallReturnsNull()
$response = new Response();
// Don't inject any firewall map
- $collector = new SecurityDataCollector(null, null, null, null, null, null, true);
+ $collector = new SecurityDataCollector(null, null, null, null, null, null);
$collector->collect($request, $response);
$this->assertNull($collector->getFirewall());
@@ -169,7 +170,7 @@ public function testGetFirewallReturnsNull()
->disableOriginalConstructor()
->getMock();
- $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()), true);
+ $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()));
$collector->collect($request, $response);
$this->assertNull($collector->getFirewall());
@@ -179,7 +180,7 @@ public function testGetFirewallReturnsNull()
->disableOriginalConstructor()
->getMock();
- $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()), true);
+ $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator()));
$collector->collect($request, $response);
$this->assertNull($collector->getFirewall());
}
@@ -213,7 +214,7 @@ public function testGetListeners()
$firewall = new TraceableFirewallListener($firewallMap, new EventDispatcher(), new LogoutUrlGenerator());
$firewall->onKernelRequest($event);
- $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, $firewall, true);
+ $collector = new SecurityDataCollector(null, null, null, null, $firewallMap, $firewall);
$collector->collect($request, $response);
$this->assertNotEmpty($collected = $collector->getListeners()[0]);
@@ -260,7 +261,7 @@ public function dispatch(object $event, ?string $eventName = null): object
],
]]);
- $dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager, null, null, true);
+ $dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager, null, null);
$dataCollector->collect(new Request(), new Response());
@@ -271,8 +272,8 @@ public function dispatch(object $event, ?string $eventName = null): object
'object' => new \stdClass(),
'result' => true,
'voter_details' => [
- ['class' => $voter1::class, 'attributes' => ['view'], 'vote' => VoterInterface::ACCESS_ABSTAIN],
- ['class' => $voter2::class, 'attributes' => ['view'], 'vote' => VoterInterface::ACCESS_ABSTAIN],
+ ['class' => $voter1::class, 'attributes' => ['view'], 'vote' => VoterInterface::ACCESS_ABSTAIN, 'reasons' => []],
+ ['class' => $voter2::class, 'attributes' => ['view'], 'vote' => VoterInterface::ACCESS_ABSTAIN, 'reasons' => []],
],
]];
@@ -348,7 +349,7 @@ public function dispatch(object $event, ?string $eventName = null): object
],
]);
- $dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager, null, null, true);
+ $dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager, null, null);
$dataCollector->collect(new Request(), new Response());
@@ -360,10 +361,10 @@ public function dispatch(object $event, ?string $eventName = null): object
'object' => new \stdClass(),
'result' => false,
'voter_details' => [
- ['class' => $voter1::class, 'attributes' => ['view'], 'vote' => VoterInterface::ACCESS_DENIED],
- ['class' => $voter1::class, 'attributes' => ['edit'], 'vote' => VoterInterface::ACCESS_DENIED],
- ['class' => $voter2::class, 'attributes' => ['view'], 'vote' => VoterInterface::ACCESS_GRANTED],
- ['class' => $voter2::class, 'attributes' => ['edit'], 'vote' => VoterInterface::ACCESS_GRANTED],
+ ['class' => $voter1::class, 'attributes' => ['view'], 'vote' => VoterInterface::ACCESS_DENIED, 'reasons' => []],
+ ['class' => $voter1::class, 'attributes' => ['edit'], 'vote' => VoterInterface::ACCESS_DENIED, 'reasons' => []],
+ ['class' => $voter2::class, 'attributes' => ['view'], 'vote' => VoterInterface::ACCESS_GRANTED, 'reasons' => []],
+ ['class' => $voter2::class, 'attributes' => ['edit'], 'vote' => VoterInterface::ACCESS_GRANTED, 'reasons' => []],
],
],
[
@@ -371,8 +372,8 @@ public function dispatch(object $event, ?string $eventName = null): object
'object' => new \stdClass(),
'result' => true,
'voter_details' => [
- ['class' => $voter1::class, 'attributes' => ['update'], 'vote' => VoterInterface::ACCESS_GRANTED],
- ['class' => $voter2::class, 'attributes' => ['update'], 'vote' => VoterInterface::ACCESS_GRANTED],
+ ['class' => $voter1::class, 'attributes' => ['update'], 'vote' => VoterInterface::ACCESS_GRANTED, 'reasons' => []],
+ ['class' => $voter2::class, 'attributes' => ['update'], 'vote' => VoterInterface::ACCESS_GRANTED, 'reasons' => []],
],
],
];
@@ -420,11 +421,11 @@ public function testGetVotersIfAccessDecisionManagerHasNoVoters()
'voterDetails' => [],
]]);
- $dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager, null, null, true);
+ $dataCollector = new SecurityDataCollector(null, null, null, $accessDecisionManager, null, null);
$dataCollector->collect(new Request(), new Response());
- $this->assertEmpty($dataCollector->getVoters());
+ $this->assertSame([], $dataCollector->getVoters());
}
public static function provideRoles(): array
@@ -461,7 +462,7 @@ private function getRoleHierarchy()
final class DummyVoter implements VoterInterface
{
- public function vote(TokenInterface $token, mixed $subject, array $attributes): int
+ public function vote(TokenInterface $token, mixed $subject, array $attributes, ?Vote $vote = null): int
{
}
}
diff --git a/Tests/Debug/TraceableFirewallListenerTest.php b/Tests/Debug/TraceableFirewallListenerTest.php
index cdf53c20..4ab483a2 100644
--- a/Tests/Debug/TraceableFirewallListenerTest.php
+++ b/Tests/Debug/TraceableFirewallListenerTest.php
@@ -19,6 +19,7 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticatorManager;
@@ -89,7 +90,7 @@ public function testOnKernelRequestRecordsAuthenticatorsInfo()
$supportingAuthenticator
->expects($this->once())
->method('createToken')
- ->willReturn($this->createMock(TokenInterface::class));
+ ->willReturn(new class extends AbstractToken {});
$notSupportingAuthenticator = $this->createMock(DummyAuthenticator::class);
$notSupportingAuthenticator
diff --git a/Tests/DependencyInjection/CompleteConfigurationTestCase.php b/Tests/DependencyInjection/CompleteConfigurationTestCase.php
index 04fba9fe..dcb67011 100644
--- a/Tests/DependencyInjection/CompleteConfigurationTestCase.php
+++ b/Tests/DependencyInjection/CompleteConfigurationTestCase.php
@@ -726,6 +726,41 @@ public function testFirewallPatterns()
$this->assertSame('(?:^/register$|^/documentation$)', $container->getDefinition($requestMatcherId)->getArgument(0));
}
+ public function testAccessTokenOidc()
+ {
+ $container = $this->getContainer('access_token_oidc');
+
+ $this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
+ $this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
+
+ $def = $container->getDefinition('security.access_token_handler.firewall1');
+ $this->assertSame('audience', $def->getArgument(2));
+ $this->assertSame(['https://www.example.com'], $def->getArgument(3));
+ $this->assertSame('sub', $def->getArgument(4));
+ }
+
+ public function testAccessTokenOidcWithEncryption()
+ {
+ $container = $this->getContainer('access_token_oidc_encryption');
+
+ $this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
+ $this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
+
+ $def = $container->getDefinition('security.access_token_handler.firewall1');
+ $this->assertSame(['RS256'], $def->getArgument(0)->getArgument(0));
+ }
+
+ public function testAccessTokenOidcUserInfoWithDiscovery()
+ {
+ if ('xml' === $this->getFileExtension()) {
+ $this->markTestSkipped('OIDC user info discovery is not supported by the XML schema.');
+ }
+ $container = $this->getContainer('access_token_oidc_user_info_discovery');
+
+ $this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
+ $this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
+ }
+
protected function getContainer($file)
{
$file .= '.'.$this->getFileExtension();
diff --git a/Tests/DependencyInjection/Fixtures/php/access_token_oidc.php b/Tests/DependencyInjection/Fixtures/php/access_token_oidc.php
new file mode 100644
index 00000000..b4631c4b
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/php/access_token_oidc.php
@@ -0,0 +1,25 @@
+loadFromExtension('security', [
+ 'providers' => [
+ 'default' => [
+ 'memory' => null,
+ ],
+ ],
+ 'firewalls' => [
+ 'firewall1' => [
+ 'provider' => 'default',
+ 'access_token' => [
+ 'token_handler' => [
+ 'oidc' => [
+ 'algorithms' => ['RS256'],
+ 'issuers' => ['https://www.example.com'],
+ 'audience' => 'audience',
+ 'keyset' => '{"keys":[{"kty":"RSA","n":"abc","e":"AQAB"}]}',
+ ],
+ ],
+ ],
+ ],
+ ],
+]);
+
diff --git a/Tests/DependencyInjection/Fixtures/php/access_token_oidc_encryption.php b/Tests/DependencyInjection/Fixtures/php/access_token_oidc_encryption.php
new file mode 100644
index 00000000..65bb9479
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/php/access_token_oidc_encryption.php
@@ -0,0 +1,30 @@
+loadFromExtension('security', [
+ 'providers' => [
+ 'default' => [
+ 'memory' => null,
+ ],
+ ],
+ 'firewalls' => [
+ 'firewall1' => [
+ 'provider' => 'default',
+ 'access_token' => [
+ 'token_handler' => [
+ 'oidc' => [
+ 'algorithms' => ['RS256'],
+ 'issuers' => ['https://www.example.com'],
+ 'audience' => 'audience',
+ 'keyset' => '{"keys":[{"kty":"RSA","n":"abc","e":"AQAB"}]}',
+ 'encryption' => [
+ 'enabled' => true,
+ 'keyset' => '{"keys":[{"kty":"RSA","n":"abc","e":"AQAB","d":"def"}]}',
+ 'algorithms' => ['RSA-OAEP'],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+]);
+
diff --git a/Tests/DependencyInjection/Fixtures/php/access_token_oidc_user_info_discovery.php b/Tests/DependencyInjection/Fixtures/php/access_token_oidc_user_info_discovery.php
new file mode 100644
index 00000000..f01b7263
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/php/access_token_oidc_user_info_discovery.php
@@ -0,0 +1,27 @@
+loadFromExtension('security', [
+ 'providers' => [
+ 'default' => [
+ 'memory' => null,
+ ],
+ ],
+ 'firewalls' => [
+ 'firewall1' => [
+ 'provider' => 'default',
+ 'access_token' => [
+ 'token_handler' => [
+ 'oidc_user_info' => [
+ 'base_uri' => 'https://www.example.com/realms/demo/protocol/openid-connect/userinfo',
+ 'discovery' => [
+ 'cache' => [
+ 'id' => 'oidc_cache',
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+]);
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/access_token_oidc.xml b/Tests/DependencyInjection/Fixtures/xml/access_token_oidc.xml
new file mode 100644
index 00000000..2b197ae4
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/xml/access_token_oidc.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ https://www.example.com
+ RS256
+
+
+
+
+
+
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/access_token_oidc_encryption.xml b/Tests/DependencyInjection/Fixtures/xml/access_token_oidc_encryption.xml
new file mode 100644
index 00000000..d21da9ca
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/xml/access_token_oidc_encryption.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ https://www.example.com
+ RS256
+
+ RSA-OAEP
+
+
+
+
+
+
+
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/access_token_oidc_user_info_discovery.xml b/Tests/DependencyInjection/Fixtures/xml/access_token_oidc_user_info_discovery.xml
new file mode 100644
index 00000000..91874379
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/xml/access_token_oidc_user_info_discovery.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/argon2i_hasher.xml b/Tests/DependencyInjection/Fixtures/xml/argon2i_hasher.xml
index 3dc2c685..133de4f8 100644
--- a/Tests/DependencyInjection/Fixtures/xml/argon2i_hasher.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/argon2i_hasher.xml
@@ -13,7 +13,7 @@
-
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/bcrypt_hasher.xml b/Tests/DependencyInjection/Fixtures/xml/bcrypt_hasher.xml
index d4c5d3de..0e790e25 100644
--- a/Tests/DependencyInjection/Fixtures/xml/bcrypt_hasher.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/bcrypt_hasher.xml
@@ -13,7 +13,7 @@
-
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/container1.xml b/Tests/DependencyInjection/Fixtures/xml/container1.xml
index f54c5064..fb5080de 100644
--- a/Tests/DependencyInjection/Fixtures/xml/container1.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/container1.xml
@@ -9,19 +9,19 @@
https://symfony.com/schema/dic/security/security-1.0.xsd">
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml b/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml
index e2f0e986..37e0b8af 100644
--- a/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/firewall_provider.xml
@@ -9,15 +9,11 @@
https://symfony.com/schema/dic/security/security-1.0.xsd">
-
-
-
+
-
-
-
-
-
+
+
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml b/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml
index e7f3e687..c1b51373 100644
--- a/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/firewall_undefined_provider.xml
@@ -9,15 +9,11 @@
https://symfony.com/schema/dic/security/security-1.0.xsd">
-
-
-
+
-
-
-
-
-
+
+
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml b/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml
index 462136c6..6d5e7149 100644
--- a/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/listener_provider.xml
@@ -9,15 +9,11 @@
https://symfony.com/schema/dic/security/security-1.0.xsd">
-
-
-
+
-
-
-
-
-
+
+
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml b/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml
index cb82f2cc..0cd1ab6d 100644
--- a/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/listener_undefined_provider.xml
@@ -9,15 +9,11 @@
https://symfony.com/schema/dic/security/security-1.0.xsd">
-
-
-
+
-
-
-
-
-
+
+
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/migrating_hasher.xml b/Tests/DependencyInjection/Fixtures/xml/migrating_hasher.xml
index a4a9d201..110868de 100644
--- a/Tests/DependencyInjection/Fixtures/xml/migrating_hasher.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/migrating_hasher.xml
@@ -13,9 +13,9 @@
-
+
bcrypt
-
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/remember_me_options.xml b/Tests/DependencyInjection/Fixtures/xml/remember_me_options.xml
index 767397ad..e051ce22 100644
--- a/Tests/DependencyInjection/Fixtures/xml/remember_me_options.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/remember_me_options.xml
@@ -9,9 +9,7 @@
https://symfony.com/schema/dic/security/security-1.0.xsd">
-
-
-
+
diff --git a/Tests/DependencyInjection/Fixtures/xml/sodium_hasher.xml b/Tests/DependencyInjection/Fixtures/xml/sodium_hasher.xml
index fd5cacef..eb26969a 100644
--- a/Tests/DependencyInjection/Fixtures/xml/sodium_hasher.xml
+++ b/Tests/DependencyInjection/Fixtures/xml/sodium_hasher.xml
@@ -13,7 +13,7 @@
-
+
diff --git a/Tests/DependencyInjection/Fixtures/yml/access_token_oidc.yml b/Tests/DependencyInjection/Fixtures/yml/access_token_oidc.yml
new file mode 100644
index 00000000..7da369de
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/yml/access_token_oidc.yml
@@ -0,0 +1,16 @@
+security:
+ providers:
+ default:
+ memory: ~
+
+ firewalls:
+ firewall1:
+ provider: default
+ access_token:
+ token_handler:
+ oidc:
+ algorithms: ['RS256']
+ issuers: ['https://www.example.com']
+ audience: 'audience'
+ keyset: '{"keys":[{"kty":"RSA","n":"abc","e":"AQAB"}]}'
+
diff --git a/Tests/DependencyInjection/Fixtures/yml/access_token_oidc_encryption.yml b/Tests/DependencyInjection/Fixtures/yml/access_token_oidc_encryption.yml
new file mode 100644
index 00000000..956b33f4
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/yml/access_token_oidc_encryption.yml
@@ -0,0 +1,20 @@
+security:
+ providers:
+ default:
+ memory: ~
+
+ firewalls:
+ firewall1:
+ provider: default
+ access_token:
+ token_handler:
+ oidc:
+ algorithms: ['RS256']
+ issuers: ['https://www.example.com']
+ audience: 'audience'
+ keyset: '{"keys":[{"kty":"RSA","n":"abc","e":"AQAB"}]}'
+ encryption:
+ enabled: true
+ keyset: '{"keys":[{"kty":"RSA","n":"abc","e":"AQAB","d":"def"}]}'
+ algorithms: ['RSA-OAEP']
+
diff --git a/Tests/DependencyInjection/Fixtures/yml/access_token_oidc_user_info_discovery.yml b/Tests/DependencyInjection/Fixtures/yml/access_token_oidc_user_info_discovery.yml
new file mode 100644
index 00000000..62e80d8d
--- /dev/null
+++ b/Tests/DependencyInjection/Fixtures/yml/access_token_oidc_user_info_discovery.yml
@@ -0,0 +1,16 @@
+security:
+ providers:
+ default:
+ memory: ~
+
+ firewalls:
+ firewall1:
+ provider: default
+ access_token:
+ token_handler:
+ oidc_user_info:
+ base_uri: 'https://www.example.com/realms/demo/protocol/openid-connect/userinfo'
+ discovery:
+ cache:
+ id: 'oidc_cache'
+
diff --git a/Tests/DependencyInjection/MainConfigurationTest.php b/Tests/DependencyInjection/MainConfigurationTest.php
index 8d3fed44..6904a21b 100644
--- a/Tests/DependencyInjection/MainConfigurationTest.php
+++ b/Tests/DependencyInjection/MainConfigurationTest.php
@@ -12,13 +12,17 @@
namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait;
use Symfony\Bundle\SecurityBundle\DependencyInjection\MainConfiguration;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Processor;
+use Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel;
class MainConfigurationTest extends TestCase
{
+ use ExpectUserDeprecationMessageTrait;
+
/**
* The minimal, required config needed to not have any required validation
* issues.
@@ -228,4 +232,69 @@ public function testFirewalls()
$configuration = new MainConfiguration(['stub' => $factory], []);
$configuration->getConfigTreeBuilder();
}
+
+ /**
+ * @dataProvider provideHideUserNotFoundData
+ */
+ public function testExposeSecurityErrors(array $config, ExposeSecurityLevel $expectedExposeSecurityErrors)
+ {
+ $config = array_merge(static::$minimalConfig, $config);
+
+ $processor = new Processor();
+ $configuration = new MainConfiguration([], []);
+ $processedConfig = $processor->processConfiguration($configuration, [$config]);
+
+ $this->assertEquals($expectedExposeSecurityErrors, $processedConfig['expose_security_errors']);
+ $this->assertArrayNotHasKey('hide_user_not_found', $processedConfig);
+ }
+
+ public static function provideHideUserNotFoundData(): iterable
+ {
+ yield [[], ExposeSecurityLevel::None];
+ yield [['expose_security_errors' => ExposeSecurityLevel::None], ExposeSecurityLevel::None];
+ yield [['expose_security_errors' => ExposeSecurityLevel::AccountStatus], ExposeSecurityLevel::AccountStatus];
+ yield [['expose_security_errors' => ExposeSecurityLevel::All], ExposeSecurityLevel::All];
+ yield [['expose_security_errors' => 'none'], ExposeSecurityLevel::None];
+ yield [['expose_security_errors' => 'account_status'], ExposeSecurityLevel::AccountStatus];
+ yield [['expose_security_errors' => 'all'], ExposeSecurityLevel::All];
+ }
+
+ /**
+ * @dataProvider provideHideUserNotFoundLegacyData
+ *
+ * @group legacy
+ */
+ public function testExposeSecurityErrorsWithLegacyConfig(array $config, ExposeSecurityLevel $expectedExposeSecurityErrors, ?bool $expectedHideUserNotFound)
+ {
+ $this->expectUserDeprecationMessage('Since symfony/security-bundle 7.3: The "hide_user_not_found" option is deprecated and will be removed in 8.0. Use the "expose_security_errors" option instead.');
+
+ $config = array_merge(static::$minimalConfig, $config);
+
+ $processor = new Processor();
+ $configuration = new MainConfiguration([], []);
+ $processedConfig = $processor->processConfiguration($configuration, [$config]);
+
+ $this->assertEquals($expectedExposeSecurityErrors, $processedConfig['expose_security_errors']);
+ $this->assertEquals($expectedHideUserNotFound, $processedConfig['hide_user_not_found']);
+ }
+
+ public static function provideHideUserNotFoundLegacyData(): iterable
+ {
+ yield [['hide_user_not_found' => true], ExposeSecurityLevel::None, true];
+ yield [['hide_user_not_found' => false], ExposeSecurityLevel::All, false];
+ }
+
+ public function testCannotUseHideUserNotFoundAndExposeSecurityErrorsAtTheSameTime()
+ {
+ $processor = new Processor();
+ $configuration = new MainConfiguration([], []);
+
+ $this->expectException(InvalidConfigurationException::class);
+ $this->expectExceptionMessage('You cannot use both "hide_user_not_found" and "expose_security_errors" at the same time.');
+
+ $processor->processConfiguration($configuration, [static::$minimalConfig + [
+ 'hide_user_not_found' => true,
+ 'expose_security_errors' => ExposeSecurityLevel::None,
+ ]]);
+ }
}
diff --git a/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php b/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php
index ce105759..88b78236 100644
--- a/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php
+++ b/Tests/DependencyInjection/Security/Factory/AccessTokenFactoryTest.php
@@ -13,6 +13,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\CasTokenHandlerFactory;
+use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OAuth2TokenHandlerFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcTokenHandlerFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\OidcUserInfoTokenHandlerFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\AccessToken\ServiceTokenHandlerFactory;
@@ -113,7 +114,7 @@ public function testInvalidOidcTokenHandlerConfigurationKeyMissing()
$factory = new AccessTokenFactory($this->createTokenHandlerFactories());
$this->expectException(InvalidConfigurationException::class);
- $this->expectExceptionMessage('The child config "keyset" under "access_token.token_handler.oidc" must be configured: JSON-encoded JWKSet used to sign the token (must contain a list of valid keys).');
+ $this->expectExceptionMessage('You must set either "discovery" or "key" or "keyset".');
$this->processConfig($config, $factory);
}
@@ -257,6 +258,140 @@ public function testOidcTokenHandlerConfigurationWithMultipleAlgorithms()
$this->assertEquals($expected, $container->getDefinition('security.access_token_handler.firewall1')->getArguments());
}
+ public function testOidcTokenHandlerConfigurationWithEncryption()
+ {
+ $container = new ContainerBuilder();
+ $jwkset = '{"keys":[{"kty":"EC","crv":"P-256","x":"FtgMtrsKDboRO-Zo0XC7tDJTATHVmwuf9GK409kkars","y":"rWDE0ERU2SfwGYCo1DWWdgFEbZ0MiAXLRBBOzBgs_jY","d":"4G7bRIiKih0qrFxc0dtvkHUll19tTyctoCR3eIbOrO0"},{"kty":"EC","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220"}]}';
+ $config = [
+ 'token_handler' => [
+ 'oidc' => [
+ 'algorithms' => ['RS256', 'ES256'],
+ 'issuers' => ['https://www.example.com'],
+ 'audience' => 'audience',
+ 'keyset' => $jwkset,
+ 'encryption' => [
+ 'enabled' => true,
+ 'keyset' => $jwkset,
+ 'algorithms' => ['RSA-OAEP', 'RSA1_5'],
+ ],
+ ],
+ ],
+ ];
+
+ $factory = new AccessTokenFactory($this->createTokenHandlerFactories());
+ $finalizedConfig = $this->processConfig($config, $factory);
+
+ $factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider');
+
+ $this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
+ $this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
+ }
+
+ public function testInvalidOidcTokenHandlerConfigurationMissingEncryptionKeyset()
+ {
+ $jwkset = '{"keys":[{"kty":"EC","crv":"P-256","x":"FtgMtrsKDboRO-Zo0XC7tDJTATHVmwuf9GK409kkars","y":"rWDE0ERU2SfwGYCo1DWWdgFEbZ0MiAXLRBBOzBgs_jY","d":"4G7bRIiKih0qrFxc0dtvkHUll19tTyctoCR3eIbOrO0"},{"kty":"EC","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220"}]}';
+ $config = [
+ 'token_handler' => [
+ 'oidc' => [
+ 'algorithms' => ['RS256', 'ES256'],
+ 'issuers' => ['https://www.example.com'],
+ 'audience' => 'audience',
+ 'keyset' => $jwkset,
+ 'encryption' => [
+ 'enabled' => true,
+ 'algorithms' => ['RSA-OAEP', 'RSA1_5'],
+ ],
+ ],
+ ],
+ ];
+
+ $factory = new AccessTokenFactory($this->createTokenHandlerFactories());
+
+ $this->expectException(InvalidConfigurationException::class);
+ $this->expectExceptionMessage('The child config "keyset" under "access_token.token_handler.oidc.encryption" must be configured: JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys).');
+
+ $this->processConfig($config, $factory);
+ }
+
+ public function testInvalidOidcTokenHandlerConfigurationMissingAlgorithm()
+ {
+ $jwkset = '{"keys":[{"kty":"EC","crv":"P-256","x":"FtgMtrsKDboRO-Zo0XC7tDJTATHVmwuf9GK409kkars","y":"rWDE0ERU2SfwGYCo1DWWdgFEbZ0MiAXLRBBOzBgs_jY","d":"4G7bRIiKih0qrFxc0dtvkHUll19tTyctoCR3eIbOrO0"},{"kty":"EC","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220"}]}';
+ $config = [
+ 'token_handler' => [
+ 'oidc' => [
+ 'algorithms' => ['RS256', 'ES256'],
+ 'issuers' => ['https://www.example.com'],
+ 'audience' => 'audience',
+ 'keyset' => $jwkset,
+ 'encryption' => [
+ 'enabled' => true,
+ 'keyset' => $jwkset,
+ 'algorithms' => [],
+ ],
+ ],
+ ],
+ ];
+
+ $factory = new AccessTokenFactory($this->createTokenHandlerFactories());
+
+ $this->expectException(InvalidConfigurationException::class);
+ $this->expectExceptionMessage('The path "access_token.token_handler.oidc.encryption.algorithms" should have at least 1 element(s) defined.');
+
+ $this->processConfig($config, $factory);
+ }
+
+ public function testOidcTokenHandlerConfigurationWithDiscovery()
+ {
+ $container = new ContainerBuilder();
+ $jwkset = '{"keys":[{"kty":"EC","crv":"P-256","x":"FtgMtrsKDboRO-Zo0XC7tDJTATHVmwuf9GK409kkars","y":"rWDE0ERU2SfwGYCo1DWWdgFEbZ0MiAXLRBBOzBgs_jY","d":"4G7bRIiKih0qrFxc0dtvkHUll19tTyctoCR3eIbOrO0"},{"kty":"EC","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220"}]}';
+ $config = [
+ 'token_handler' => [
+ 'oidc' => [
+ 'discovery' => [
+ 'base_uri' => 'https://www.example.com/realms/demo/',
+ 'cache' => [
+ 'id' => 'oidc_cache',
+ ],
+ ],
+ 'algorithms' => ['RS256', 'ES256'],
+ 'issuers' => ['https://www.example.com'],
+ 'audience' => 'audience',
+ ],
+ ],
+ ];
+
+ $factory = new AccessTokenFactory($this->createTokenHandlerFactories());
+ $finalizedConfig = $this->processConfig($config, $factory);
+
+ $factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider');
+
+ $this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
+ $this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
+
+ $expectedArgs = [
+ 'index_0' => (new ChildDefinition('security.access_token_handler.oidc.signature'))
+ ->replaceArgument(0, ['RS256', 'ES256']),
+ 'index_1' => null,
+ 'index_2' => 'audience',
+ 'index_3' => ['https://www.example.com'],
+ 'index_4' => 'sub',
+ ];
+ $expectedCalls = [
+ [
+ 'enableDiscovery',
+ [
+ new Reference('oidc_cache'),
+ (new ChildDefinition('security.access_token_handler.oidc_discovery.http_client'))
+ ->replaceArgument(0, ['base_uri' => 'https://www.example.com/realms/demo/']),
+ 'security.access_token_handler.firewall1.oidc_configuration',
+ 'security.access_token_handler.firewall1.oidc_jwk_set',
+ ],
+ ],
+ ];
+ $this->assertEquals($expectedArgs, $container->getDefinition('security.access_token_handler.firewall1')->getArguments());
+ $this->assertEquals($expectedCalls, $container->getDefinition('security.access_token_handler.firewall1')->getMethodCalls());
+ }
+
public function testOidcUserInfoTokenHandlerConfigurationWithExistingClient()
{
$container = new ContainerBuilder();
@@ -324,6 +459,48 @@ public static function getOidcUserInfoConfiguration(): iterable
yield ['https://www.example.com/realms/demo/protocol/openid-connect/userinfo'];
}
+ public function testOidcUserInfoTokenHandlerConfigurationWithDiscovery()
+ {
+ $container = new ContainerBuilder();
+ $config = [
+ 'token_handler' => [
+ 'oidc_user_info' => [
+ 'discovery' => [
+ 'cache' => [
+ 'id' => 'oidc_cache',
+ ],
+ ],
+ 'base_uri' => 'https://www.example.com/realms/demo/protocol/openid-connect/userinfo',
+ ],
+ ],
+ ];
+
+ $factory = new AccessTokenFactory($this->createTokenHandlerFactories());
+ $finalizedConfig = $this->processConfig($config, $factory);
+
+ $factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider');
+
+ $this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
+ $this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
+
+ $expectedArgs = [
+ 'index_0' => (new ChildDefinition('security.access_token_handler.oidc_user_info.http_client'))
+ ->replaceArgument(0, ['base_uri' => 'https://www.example.com/realms/demo/protocol/openid-connect/userinfo']),
+ 'index_2' => 'sub',
+ ];
+ $expectedCalls = [
+ [
+ 'enableDiscovery',
+ [
+ new Reference('oidc_cache'),
+ 'security.access_token_handler.firewall1.oidc_configuration',
+ ],
+ ],
+ ];
+ $this->assertEquals($expectedArgs, $container->getDefinition('security.access_token_handler.firewall1')->getArguments());
+ $this->assertEquals($expectedCalls, $container->getDefinition('security.access_token_handler.firewall1')->getMethodCalls());
+ }
+
public function testMultipleTokenHandlersSet()
{
$config = [
@@ -341,6 +518,22 @@ public function testMultipleTokenHandlersSet()
$this->processConfig($config, $factory);
}
+ public function testOAuth2TokenHandlerConfiguration()
+ {
+ $container = new ContainerBuilder();
+ $config = [
+ 'token_handler' => ['oauth2' => true],
+ ];
+
+ $factory = new AccessTokenFactory($this->createTokenHandlerFactories());
+ $finalizedConfig = $this->processConfig($config, $factory);
+
+ $factory->createAuthenticator($container, 'firewall1', $finalizedConfig, 'userprovider');
+
+ $this->assertTrue($container->hasDefinition('security.authenticator.access_token.firewall1'));
+ $this->assertTrue($container->hasDefinition('security.access_token_handler.firewall1'));
+ }
+
public function testNoTokenHandlerSet()
{
$this->expectException(InvalidConfigurationException::class);
@@ -400,6 +593,7 @@ private function createTokenHandlerFactories(): array
new OidcUserInfoTokenHandlerFactory(),
new OidcTokenHandlerFactory(),
new CasTokenHandlerFactory(),
+ new OAuth2TokenHandlerFactory(),
];
}
}
diff --git a/Tests/DependencyInjection/SecurityExtensionTest.php b/Tests/DependencyInjection/SecurityExtensionTest.php
index d0f3549a..8607e45a 100644
--- a/Tests/DependencyInjection/SecurityExtensionTest.php
+++ b/Tests/DependencyInjection/SecurityExtensionTest.php
@@ -986,7 +986,7 @@ public function checkPreAuth(UserInterface $user): void
{
}
- public function checkPostAuth(UserInterface $user): void
+ public function checkPostAuth(UserInterface $user, ?TokenInterface $token = null): void
{
}
}
diff --git a/Tests/Fixtures/Descriptor/firewall_main_output.txt b/Tests/Fixtures/Descriptor/firewall_main_output.txt
new file mode 100644
index 00000000..d2241625
--- /dev/null
+++ b/Tests/Fixtures/Descriptor/firewall_main_output.txt
@@ -0,0 +1,30 @@
+Firewall "main"
+===============
+
+ ----------------------- -------------------------------
+ Option Value
+ ----------------------- -------------------------------
+ Name main
+ Context main
+ Lazy No
+ Stateless No
+ User Checker user_checker_service
+ Provider user_provider_service
+ Entry Point entry_point_service
+ Access Denied URL /access-denied
+ Access Denied Handler access_denied_handler_service
+ ----------------------- -------------------------------
+
+Event listeners for firewall "main"
+===================================
+
+ No event dispatcher has been registered for this firewall.
+
+Authenticators for firewall "main"
+==================================
+
+ -----------------------------------------------------------------
+ Classname
+ -----------------------------------------------------------------
+ Symfony\Bundle\SecurityBundle\Tests\Fixtures\DummyAuthenticator
+ -----------------------------------------------------------------
diff --git a/Tests/Fixtures/Descriptor/firewall_main_with_events_output.txt b/Tests/Fixtures/Descriptor/firewall_main_with_events_output.txt
new file mode 100644
index 00000000..2d02f34b
--- /dev/null
+++ b/Tests/Fixtures/Descriptor/firewall_main_with_events_output.txt
@@ -0,0 +1,39 @@
+Firewall "main"
+===============
+
+ ----------------------- -------------------------------
+ Option Value
+ ----------------------- -------------------------------
+ Name main
+ Context main
+ Lazy No
+ Stateless No
+ User Checker user_checker_service
+ Provider user_provider_service
+ Entry Point entry_point_service
+ Access Denied URL /access-denied
+ Access Denied Handler access_denied_handler_service
+ ----------------------- -------------------------------
+
+Event listeners for firewall "main"
+===================================
+
+"security.event" event
+----------------------
+
+ ------- ----------- ----------
+ Order Callable Priority
+ ------- ----------- ----------
+ #1 Closure() 42
+ #2 Closure() 42
+ ------- ----------- ----------
+
+Authenticators for firewall "main"
+==================================
+
+ -----------------------------------------------------------------
+ Classname
+ -----------------------------------------------------------------
+ Symfony\Bundle\SecurityBundle\Tests\Fixtures\DummyAuthenticator
+ Symfony\Bundle\SecurityBundle\Tests\Fixtures\DummyAuthenticator
+ -----------------------------------------------------------------
diff --git a/Tests/Fixtures/Descriptor/firewall_main_with_switch_user.txt b/Tests/Fixtures/Descriptor/firewall_main_with_switch_user.txt
new file mode 100644
index 00000000..4843b86f
--- /dev/null
+++ b/Tests/Fixtures/Descriptor/firewall_main_with_switch_user.txt
@@ -0,0 +1,36 @@
+Firewall "main"
+===============
+
+ ----------------------- ------------------------------------
+ Option Value
+ ----------------------- ------------------------------------
+ Name main
+ Context main
+ Lazy No
+ Stateless No
+ User Checker user_checker_service_test
+ Provider user_provider_service_test
+ Entry Point entry_point_service_test
+ Access Denied URL /access-denied-test
+ Access Denied Handler access_denied_handler_service_test
+ ----------------------- ------------------------------------
+
+User switching
+--------------
+
+ ----------- ------------------------
+ Option Value
+ ----------- ------------------------
+ Parameter _switch_user_test
+ Provider custom_provider_test
+ User Role ROLE_ALLOWED_TO_SWITCH
+ ----------- ------------------------
+
+Authenticators for firewall "main"
+==================================
+
+ -----------------------------------------------------------------
+ Classname
+ -----------------------------------------------------------------
+ Symfony\Bundle\SecurityBundle\Tests\Fixtures\DummyAuthenticator
+ -----------------------------------------------------------------
diff --git a/Tests/Fixtures/DummyAuthenticator.php b/Tests/Fixtures/DummyAuthenticator.php
new file mode 100644
index 00000000..8ac51a1e
--- /dev/null
+++ b/Tests/Fixtures/DummyAuthenticator.php
@@ -0,0 +1,50 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\SecurityBundle\Tests\Fixtures;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Exception\AuthenticationException;
+use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
+use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
+use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
+
+class DummyAuthenticator implements AuthenticatorInterface
+{
+ public function supports(Request $request): ?bool
+ {
+ return null;
+ }
+
+ public function authenticate(Request $request): Passport
+ {
+ }
+
+ public function createToken(Passport $passport, string $firewallName): TokenInterface
+ {
+ }
+
+ public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
+ {
+ return null;
+ }
+
+ public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
+ {
+ return null;
+ }
+
+ public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
+ {
+ }
+}
diff --git a/Tests/Functional/AccessTokenTest.php b/Tests/Functional/AccessTokenTest.php
index 8e87cd54..75adf296 100644
--- a/Tests/Functional/AccessTokenTest.php
+++ b/Tests/Functional/AccessTokenTest.php
@@ -13,9 +13,13 @@
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
+use Jose\Component\Encryption\Algorithm\ContentEncryption\A128GCM;
+use Jose\Component\Encryption\Algorithm\KeyEncryption\ECDHES;
+use Jose\Component\Encryption\JWEBuilder;
+use Jose\Component\Encryption\Serializer\CompactSerializer as JweCompactSerializer;
use Jose\Component\Signature\Algorithm\ES256;
use Jose\Component\Signature\JWSBuilder;
-use Jose\Component\Signature\Serializer\CompactSerializer;
+use Jose\Component\Signature\Serializer\CompactSerializer as JwsCompactSerializer;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
@@ -348,34 +352,17 @@ public function testCustomUserLoader()
}
/**
+ * @dataProvider validAccessTokens
+ *
* @requires extension openssl
*/
- public function testOidcSuccess()
+ public function testOidcSuccess(callable $tokenFactory)
{
- $time = time();
- $claims = [
- 'iat' => $time,
- 'nbf' => $time,
- 'exp' => $time + 3600,
- 'iss' => 'https://www.example.com',
- 'aud' => 'Symfony OIDC',
- 'sub' => 'e21bf182-1538-406e-8ccb-e25a17aba39f',
- 'username' => 'dunglas',
- ];
- $token = (new CompactSerializer())->serialize((new JWSBuilder(new AlgorithmManager([
- new ES256(),
- ])))->create()
- ->withPayload(json_encode($claims))
- // tip: use https://mkjwk.org/ to generate a JWK
- ->addSignature(new JWK([
- 'kty' => 'EC',
- 'crv' => 'P-256',
- 'x' => '0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4',
- 'y' => 'KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo',
- 'd' => 'iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220',
- ]), ['alg' => 'ES256'])
- ->build()
- );
+ try {
+ $token = $tokenFactory();
+ } catch (\RuntimeException $e) {
+ $this->markTestSkipped($e->getMessage());
+ }
$client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_oidc.yml']);
$client->request('GET', '/foo', [], [], ['HTTP_AUTHORIZATION' => \sprintf('Bearer %s', $token)]);
@@ -386,6 +373,51 @@ public function testOidcSuccess()
$this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true));
}
+ /**
+ * @dataProvider invalidAccessTokens
+ *
+ * @requires extension openssl
+ */
+ public function testOidcFailure(callable $tokenFactory)
+ {
+ try {
+ $token = $tokenFactory();
+ } catch (\RuntimeException $e) {
+ $this->markTestSkipped($e->getMessage());
+ }
+
+ $client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_oidc.yml']);
+ $client->request('GET', '/foo', [], [], ['HTTP_AUTHORIZATION' => \sprintf('Bearer %s', $token)]);
+ $response = $client->getResponse();
+
+ $this->assertInstanceOf(Response::class, $response);
+ $this->assertSame(401, $response->getStatusCode());
+ $this->assertSame('Bearer realm="My API",error="invalid_token",error_description="Invalid credentials."', $response->headers->get('WWW-Authenticate'));
+ }
+
+ /**
+ * @requires extension openssl
+ */
+ public function testOidcFailureWithJweEnforced()
+ {
+ $client = $this->createClient(['test_case' => 'AccessToken', 'root_config' => 'config_oidc_jwe.yml']);
+ $token = self::createJws([
+ 'iat' => time() - 1,
+ 'nbf' => time() - 1,
+ 'exp' => time() + 3600,
+ 'iss' => 'https://www.example.com',
+ 'aud' => 'Symfony OIDC',
+ 'sub' => 'e21bf182-1538-406e-8ccb-e25a17aba39f',
+ 'username' => 'dunglas',
+ ]);
+ $client->request('GET', '/foo', [], [], ['HTTP_AUTHORIZATION' => \sprintf('Bearer %s', $token)]);
+ $response = $client->getResponse();
+
+ $this->assertInstanceOf(Response::class, $response);
+ $this->assertSame(401, $response->getStatusCode());
+ $this->assertSame('Bearer realm="My API",error="invalid_token",error_description="Invalid credentials."', $response->headers->get('WWW-Authenticate'));
+ }
+
public function testCasSuccess()
{
$casResponse = new MockResponse(<<assertSame(200, $response->getStatusCode());
$this->assertSame(['message' => 'Welcome @dunglas!'], json_decode($response->getContent(), true));
}
+
+ public static function validAccessTokens(): array
+ {
+ if (!\extension_loaded('openssl')) {
+ return [];
+ }
+ $time = time();
+ $claims = [
+ 'iat' => $time,
+ 'nbf' => $time,
+ 'exp' => $time + 3600,
+ 'iss' => 'https://www.example.com',
+ 'aud' => 'Symfony OIDC',
+ 'sub' => 'e21bf182-1538-406e-8ccb-e25a17aba39f',
+ 'username' => 'dunglas',
+ ];
+
+ return [
+ [fn () => self::createJws($claims)],
+ [fn () => self::createJwe(self::createJws($claims))],
+ ];
+ }
+
+ public static function invalidAccessTokens(): array
+ {
+ if (!\extension_loaded('openssl')) {
+ return [];
+ }
+ $time = time();
+ $claims = [
+ 'iat' => $time,
+ 'nbf' => $time,
+ 'exp' => $time + 3600,
+ 'iss' => 'https://www.example.com',
+ 'aud' => 'Symfony OIDC',
+ 'sub' => 'e21bf182-1538-406e-8ccb-e25a17aba39f',
+ 'username' => 'dunglas',
+ ];
+
+ return [
+ [fn () => self::createJws([...$claims, 'aud' => 'Invalid Audience'])],
+ [fn () => self::createJws([...$claims, 'iss' => 'Invalid Issuer'])],
+ [fn () => self::createJws([...$claims, 'exp' => $time - 3600])],
+ [fn () => self::createJws([...$claims, 'nbf' => $time + 3600])],
+ [fn () => self::createJws([...$claims, 'iat' => $time + 3600])],
+ [fn () => self::createJws([...$claims, 'username' => 'Invalid Username'])],
+ [fn () => self::createJwe(self::createJws($claims), ['exp' => $time - 3600])],
+ [fn () => self::createJwe(self::createJws($claims), ['cty' => 'x-specific'])],
+ ];
+ }
+
+ private static function createJws(array $claims, array $header = []): string
+ {
+ return (new JwsCompactSerializer())->serialize((new JWSBuilder(new AlgorithmManager([
+ new ES256(),
+ ])))->create()
+ ->withPayload(json_encode($claims))
+ // tip: use https://mkjwk.org/ to generate a JWK
+ ->addSignature(new JWK([
+ 'kty' => 'EC',
+ 'crv' => 'P-256',
+ 'x' => '0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4',
+ 'y' => 'KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo',
+ 'd' => 'iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220',
+ ]), [...$header, 'alg' => 'ES256'])
+ ->build()
+ );
+ }
+
+ private static function createJwe(string $input, array $header = []): string
+ {
+ $jwk = new JWK([
+ 'kty' => 'EC',
+ 'use' => 'enc',
+ 'crv' => 'P-256',
+ 'kid' => 'enc-1720876375',
+ 'x' => '4P27-OB2s5ZP3Zt5ExxQ9uFrgnGaMK6wT1oqd5bJozQ',
+ 'y' => 'CNh-ZbKJBvz6hJ8JOulXclACP2OuoO2PtqT6WC8tLcU',
+ ]);
+
+ return (new JweCompactSerializer())->serialize(
+ (new JWEBuilder(new AlgorithmManager([
+ new ECDHES(), new A128GCM(),
+ ]), null))->create()
+ ->withPayload($input)
+ ->withSharedProtectedHeader(['alg' => 'ECDH-ES', 'enc' => 'A128GCM', ...$header])
+ // tip: use https://mkjwk.org/ to generate a JWK
+ ->addRecipient($jwk)
+ ->build()
+ );
+ }
}
diff --git a/Tests/Functional/Bundle/JsonLdapLoginBundle/Controller/TestController.php b/Tests/Functional/Bundle/JsonLdapLoginBundle/Controller/TestController.php
new file mode 100644
index 00000000..3bf5e6e4
--- /dev/null
+++ b/Tests/Functional/Bundle/JsonLdapLoginBundle/Controller/TestController.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLdapLoginBundle\Controller;
+
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\Security\Core\User\UserInterface;
+
+class TestController
+{
+ public function loginCheckAction(UserInterface $user)
+ {
+ return new JsonResponse([
+ 'message' => \sprintf('Welcome @%s!', $user->getUserIdentifier()),
+ 'roles' => $user->getRoles(),
+ ]);
+ }
+}
diff --git a/Tests/Functional/Bundle/JsonLdapLoginBundle/Security/Ldap/DummyRoleFetcher.php b/Tests/Functional/Bundle/JsonLdapLoginBundle/Security/Ldap/DummyRoleFetcher.php
new file mode 100644
index 00000000..417c8afe
--- /dev/null
+++ b/Tests/Functional/Bundle/JsonLdapLoginBundle/Security/Ldap/DummyRoleFetcher.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLdapLoginBundle\Security\Ldap;
+
+use Symfony\Component\Ldap\Entry;
+use Symfony\Component\Ldap\Security\RoleFetcherInterface;
+
+class DummyRoleFetcher implements RoleFetcherInterface
+{
+ public function fetchRoles(Entry $entry): array
+ {
+ if ($entry->getAttribute('uid') === ['spomky']) {
+ return ['ROLE_SUPER_ADMIN', 'ROLE_USER'];
+ }
+
+ return ['ROLE_LDAP_USER_42', 'ROLE_USER'];
+ }
+}
diff --git a/Tests/Functional/JsonLoginLdapTest.php b/Tests/Functional/JsonLoginLdapTest.php
index 583e1536..f1190829 100644
--- a/Tests/Functional/JsonLoginLdapTest.php
+++ b/Tests/Functional/JsonLoginLdapTest.php
@@ -11,7 +11,14 @@
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
+use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Kernel;
+use Symfony\Component\Ldap\Adapter\AdapterInterface;
+use Symfony\Component\Ldap\Adapter\CollectionInterface;
+use Symfony\Component\Ldap\Adapter\ConnectionInterface;
+use Symfony\Component\Ldap\Adapter\ExtLdap\Adapter;
+use Symfony\Component\Ldap\Adapter\QueryInterface;
+use Symfony\Component\Ldap\Entry;
class JsonLoginLdapTest extends AbstractWebTestCase
{
@@ -22,4 +29,45 @@ public function testKernelBoot()
$this->assertInstanceOf(Kernel::class, $kernel);
}
+
+ public function testDefaultJsonLdapLoginSuccess()
+ {
+ if (!interface_exists(\Symfony\Component\Ldap\Security\RoleFetcherInterface::class)) {
+ $this->markTestSkipped('The "LDAP" component does not support LDAP roles.');
+ }
+ // Given
+ $client = $this->createClient(['test_case' => 'JsonLoginLdap', 'root_config' => 'config.yml', 'debug' => true]);
+ $container = $client->getContainer();
+ $connectionMock = $this->createMock(ConnectionInterface::class);
+ $collection = new class([new Entry('', ['uid' => ['spomky']])]) extends \ArrayObject implements CollectionInterface {
+ public function toArray(): array
+ {
+ return $this->getArrayCopy();
+ }
+ };
+ $queryMock = $this->createMock(QueryInterface::class);
+ $queryMock
+ ->method('execute')
+ ->willReturn($collection)
+ ;
+ $ldapAdapterMock = $this->createMock(AdapterInterface::class);
+ $ldapAdapterMock
+ ->method('getConnection')
+ ->willReturn($connectionMock)
+ ;
+ $ldapAdapterMock
+ ->method('createQuery')
+ ->willReturn($queryMock)
+ ;
+ $container->set(Adapter::class, $ldapAdapterMock);
+
+ // When
+ $client->request('POST', '/login', [], [], ['CONTENT_TYPE' => 'application/json'], '{"user": {"login": "spomky", "password": "foo"}}');
+ $response = $client->getResponse();
+
+ // Then
+ $this->assertInstanceOf(JsonResponse::class, $response);
+ $this->assertSame(200, $response->getStatusCode());
+ $this->assertSame(['message' => 'Welcome @spomky!', 'roles' => ['ROLE_SUPER_ADMIN', 'ROLE_USER']], json_decode($response->getContent(), true));
+ }
}
diff --git a/Tests/Functional/SecurityTest.php b/Tests/Functional/SecurityTest.php
index dadd0d69..76987173 100644
--- a/Tests/Functional/SecurityTest.php
+++ b/Tests/Functional/SecurityTest.php
@@ -47,6 +47,24 @@ public function testServiceIsFunctional()
$this->assertSame('main', $firewallConfig->getName());
}
+ public function testUserAuthorizationChecker()
+ {
+ $kernel = self::createKernel(['test_case' => 'SecurityHelper', 'root_config' => 'config.yml']);
+ $kernel->boot();
+ $container = $kernel->getContainer();
+
+ $loggedInUser = new InMemoryUser('foo', 'pass', ['ROLE_USER', 'ROLE_FOO']);
+ $offlineUser = new InMemoryUser('bar', 'pass', ['ROLE_USER', 'ROLE_BAR']);
+ $token = new UsernamePasswordToken($loggedInUser, 'provider', $loggedInUser->getRoles());
+ $container->get('functional.test.security.token_storage')->setToken($token);
+
+ $security = $container->get('functional_test.security.helper');
+ $this->assertTrue($security->isGranted('ROLE_FOO'));
+ $this->assertFalse($security->isGranted('ROLE_BAR'));
+ $this->assertTrue($security->isGrantedForUser($offlineUser, 'ROLE_BAR'));
+ $this->assertFalse($security->isGrantedForUser($offlineUser, 'ROLE_FOO'));
+ }
+
/**
* @dataProvider userWillBeMarkedAsChangedIfRolesHasChangedProvider
*/
@@ -232,6 +250,7 @@ public function isEnabled(): bool
return $this->enabled;
}
+ #[\Deprecated]
public function eraseCredentials(): void
{
}
diff --git a/Tests/Functional/app/AccessToken/config_oauth2.yml b/Tests/Functional/app/AccessToken/config_oauth2.yml
new file mode 100644
index 00000000..9e4f6cce
--- /dev/null
+++ b/Tests/Functional/app/AccessToken/config_oauth2.yml
@@ -0,0 +1,34 @@
+imports:
+ - { resource: ./../config/framework.yml }
+
+framework:
+ http_method_override: false
+ serializer: ~
+ http_client:
+ scoped_clients:
+ oauth2.client:
+ scope: 'https://authorization-server\.example\.com'
+ headers:
+ Authorization: 'Basic Y2xpZW50OnBhc3N3b3Jk'
+
+security:
+ password_hashers:
+ Symfony\Component\Security\Core\User\InMemoryUser: plaintext
+
+ providers:
+ in_memory:
+ memory:
+ users:
+ dunglas: { password: foo, roles: [ROLE_USER] }
+
+ firewalls:
+ main:
+ pattern: ^/
+ access_token:
+ token_handler:
+ oauth2: ~
+ token_extractors: 'header'
+ realm: 'My API'
+
+ access_control:
+ - { path: ^/foo, roles: ROLE_USER }
diff --git a/Tests/Functional/app/AccessToken/config_oidc.yml b/Tests/Functional/app/AccessToken/config_oidc.yml
index 68f8a1f9..a0876047 100644
--- a/Tests/Functional/app/AccessToken/config_oidc.yml
+++ b/Tests/Functional/app/AccessToken/config_oidc.yml
@@ -24,9 +24,13 @@ security:
claim: 'username'
audience: 'Symfony OIDC'
issuers: [ 'https://www.example.com' ]
- algorithm: 'ES256'
+ algorithms: [ 'ES256' ]
# tip: use https://mkjwk.org/ to generate a JWK
keyset: '{"keys":[{"kty":"EC","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo"}]}'
+ encryption:
+ enabled: true
+ algorithms: ['ECDH-ES', 'A128GCM']
+ keyset: '{"keys": [{"kty": "EC","d": "YG0HnRsaYv2cUj7TpgHcRX1poL9l4cskIuOi1gXv0Dg","use": "enc","crv": "P-256","kid": "enc-1720876375","x": "4P27-OB2s5ZP3Zt5ExxQ9uFrgnGaMK6wT1oqd5bJozQ","y": "CNh-ZbKJBvz6hJ8JOulXclACP2OuoO2PtqT6WC8tLcU","alg": "ECDH-ES"}]}'
token_extractors: 'header'
realm: 'My API'
diff --git a/Tests/Functional/app/AccessToken/config_oidc_jwe.yml b/Tests/Functional/app/AccessToken/config_oidc_jwe.yml
new file mode 100644
index 00000000..7d17d073
--- /dev/null
+++ b/Tests/Functional/app/AccessToken/config_oidc_jwe.yml
@@ -0,0 +1,39 @@
+imports:
+ - { resource: ./../config/framework.yml }
+
+framework:
+ http_method_override: false
+ serializer: ~
+
+security:
+ password_hashers:
+ Symfony\Component\Security\Core\User\InMemoryUser: plaintext
+
+ providers:
+ in_memory:
+ memory:
+ users:
+ dunglas: { password: foo, roles: [ROLE_USER] }
+
+ firewalls:
+ main:
+ pattern: ^/
+ access_token:
+ token_handler:
+ oidc:
+ claim: 'username'
+ audience: 'Symfony OIDC'
+ issuers: [ 'https://www.example.com' ]
+ algorithm: 'ES256'
+ # tip: use https://mkjwk.org/ to generate a JWK
+ keyset: '{"keys":[{"kty":"EC","d":"iA_TV2zvftni_9aFAQwFO_9aypfJFCSpcCyevDvz220","crv":"P-256","x":"0QEAsI1wGI-dmYatdUZoWSRWggLEpyzopuhwk-YUnA4","y":"KYl-qyZ26HobuYwlQh-r0iHX61thfP82qqEku7i0woo"}]}'
+ encryption:
+ enabled: true
+ enforce: true
+ algorithms: ['ECDH-ES', 'A128GCM']
+ keyset: '{"keys": [{"kty": "EC","d": "YG0HnRsaYv2cUj7TpgHcRX1poL9l4cskIuOi1gXv0Dg","use": "enc","crv": "P-256","kid": "enc-1720876375","x": "4P27-OB2s5ZP3Zt5ExxQ9uFrgnGaMK6wT1oqd5bJozQ","y": "CNh-ZbKJBvz6hJ8JOulXclACP2OuoO2PtqT6WC8tLcU","alg": "ECDH-ES"}]}'
+ token_extractors: 'header'
+ realm: 'My API'
+
+ access_control:
+ - { path: ^/foo, roles: ROLE_USER }
diff --git a/Tests/Functional/app/FirewallEntryPoint/config.yml b/Tests/Functional/app/FirewallEntryPoint/config.yml
index 9d6b4cae..31b0af34 100644
--- a/Tests/Functional/app/FirewallEntryPoint/config.yml
+++ b/Tests/Functional/app/FirewallEntryPoint/config.yml
@@ -17,7 +17,9 @@ framework:
cookie_samesite: lax
php_errors:
log: true
- profiler: { only_exceptions: false }
+ profiler:
+ only_exceptions: false
+ collect_serializer_data: true
services:
logger: { class: Psr\Log\NullLogger }
diff --git a/Tests/Functional/app/JsonLoginLdap/config.yml b/Tests/Functional/app/JsonLoginLdap/config.yml
index 71e107b1..c75c1a79 100644
--- a/Tests/Functional/app/JsonLoginLdap/config.yml
+++ b/Tests/Functional/app/JsonLoginLdap/config.yml
@@ -3,6 +3,9 @@ imports:
services:
Symfony\Component\Ldap\Ldap:
arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
+ tags: [ 'ldap' ]
+ dummy_role_fetcher:
+ class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLdapLoginBundle\Security\Ldap\DummyRoleFetcher
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
@@ -19,9 +22,8 @@ security:
base_dn: 'dc=onfroy,dc=net'
search_dn: ''
search_password: ''
- default_roles: ROLE_USER
+ role_fetcher: dummy_role_fetcher
uid_key: uid
- extra_fields: ['email']
firewalls:
main:
diff --git a/Tests/Functional/app/JsonLoginLdap/routing.yml b/Tests/Functional/app/JsonLoginLdap/routing.yml
new file mode 100644
index 00000000..bbec958c
--- /dev/null
+++ b/Tests/Functional/app/JsonLoginLdap/routing.yml
@@ -0,0 +1,3 @@
+login_check:
+ path: /login
+ defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLdapLoginBundle\Controller\TestController::loginCheckAction }
diff --git a/Tests/Functional/app/config/framework.yml b/Tests/Functional/app/config/framework.yml
index c197fcaa..0f2e1344 100644
--- a/Tests/Functional/app/config/framework.yml
+++ b/Tests/Functional/app/config/framework.yml
@@ -18,7 +18,9 @@ framework:
cookie_samesite: lax
php_errors:
log: true
- profiler: { only_exceptions: false }
+ profiler:
+ only_exceptions: false
+ collect_serializer_data: true
services:
logger: { class: Psr\Log\NullLogger }
diff --git a/Tests/SecurityTest.php b/Tests/SecurityTest.php
index 82a444ef..9a126ae3 100644
--- a/Tests/SecurityTest.php
+++ b/Tests/SecurityTest.php
@@ -154,7 +154,7 @@ public function testLogin()
->method('getProvidedServices')
->willReturn([
'security.authenticator.custom.dev' => $authenticator,
- 'security.authenticator.remember_me.main' => $authenticator
+ 'security.authenticator.remember_me.main' => $authenticator,
])
;
$firewallAuthenticatorLocator
@@ -287,7 +287,7 @@ public function testLoginFailsWhenTooManyAuthenticatorsFound()
->method('getProvidedServices')
->willReturn([
'security.authenticator.custom.main' => $authenticator,
- 'security.authenticator.other.main' => $authenticator
+ 'security.authenticator.other.main' => $authenticator,
])
;
diff --git a/composer.json b/composer.json
index 8660196a..7459b017 100644
--- a/composer.json
+++ b/composer.json
@@ -20,15 +20,15 @@
"composer-runtime-api": ">=2.1",
"ext-xml": "*",
"symfony/clock": "^6.4|^7.0",
- "symfony/config": "^6.4|^7.0",
+ "symfony/config": "^7.3",
"symfony/dependency-injection": "^6.4.11|^7.1.4",
"symfony/event-dispatcher": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/password-hasher": "^6.4|^7.0",
- "symfony/security-core": "^7.2",
+ "symfony/security-core": "^7.3",
"symfony/security-csrf": "^6.4|^7.0",
- "symfony/security-http": "^7.2",
+ "symfony/security-http": "^7.3",
"symfony/service-contracts": "^2.5|^3"
},
"require-dev": {