diff --git a/CHANGELOG.md b/CHANGELOG.md
index 658640176..c2aa40f54 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -55,6 +55,7 @@ CHANGELOG
in 3.0
* deprecated the following options:
+ * `fos_rest.access_denied_listener`
* `fos_rest.exception.exception_controller`
* `fos_rest.exception.exception_listener`
* `fos_rest.exception.service`
@@ -75,6 +76,7 @@ CHANGELOG
* `FOS\RestBundle\Controller\ExceptionController`
* `FOS\RestBundle\Controller\TemplatingExceptionController`
* `FOS\RestBundle\Controller\TwigExceptionController`
+ * `FOS\RestBundle\EventListener\AccessDeniedListener`
* `FOS\RestBundle\EventListener\ExceptionListener`
* `FOS\RestBundle\Inflector\DoctrineInflector`
* `FOS\RestBundle\Inflector\InflectorInterface`
@@ -92,6 +94,7 @@ CHANGELOG
* the following services and aliases are marked as `deprecated`, they will be removed in 3.0:
+ * `fos_rest.access_denied_listener`
* `fos_rest.exception_listener`
* `fos_rest.exception.controller`
* `fos_rest.exception.twig_controller`
diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php
index fce89cb41..099102eae 100644
--- a/DependencyInjection/Configuration.php
+++ b/DependencyInjection/Configuration.php
@@ -58,6 +58,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->scalarNode('disable_csrf_role')->defaultNull()->end()
->arrayNode('access_denied_listener')
->canBeEnabled()
+ ->setDeprecated('The "%path%.%node%" option is deprecated since FOSRestBundle 2.8.')
->beforeNormalization()
->ifArray()->then(function ($v) {
if (!empty($v) && empty($v['formats'])) {
diff --git a/EventListener/AccessDeniedListener.php b/EventListener/AccessDeniedListener.php
index 7f21ec605..d8ab4b8f5 100644
--- a/EventListener/AccessDeniedListener.php
+++ b/EventListener/AccessDeniedListener.php
@@ -11,6 +11,8 @@
namespace FOS\RestBundle\EventListener;
+@trigger_error(sprintf('The %s\AccessDeniedListener class is deprecated since FOSRestBundle 2.8.', __NAMESPACE__), E_USER_DEPRECATED);
+
use FOS\RestBundle\FOSRestBundle;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
diff --git a/Resources/config/access_denied_listener.xml b/Resources/config/access_denied_listener.xml
index 32fa61826..ca370d7cf 100644
--- a/Resources/config/access_denied_listener.xml
+++ b/Resources/config/access_denied_listener.xml
@@ -11,6 +11,7 @@
+ The "%service_id%" service is deprecated since FOSRestBundle 2.8.
diff --git a/Tests/EventListener/AccessDeniedListenerTest.php b/Tests/EventListener/AccessDeniedListenerTest.php
index 7f3068ca5..04f575609 100644
--- a/Tests/EventListener/AccessDeniedListenerTest.php
+++ b/Tests/EventListener/AccessDeniedListenerTest.php
@@ -29,6 +29,8 @@
/**
* AccessDeniedListenerTest.
*
+ * @group legacy
+ *
* @author Boris Guéry
*/
class AccessDeniedListenerTest extends TestCase
diff --git a/Tests/Functional/AbstractAuthenticatorTestCase.php b/Tests/Functional/AbstractAuthenticatorTestCase.php
new file mode 100644
index 000000000..43c8fab95
--- /dev/null
+++ b/Tests/Functional/AbstractAuthenticatorTestCase.php
@@ -0,0 +1,82 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\RestBundle\Tests\Functional;
+
+use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
+
+abstract class AbstractAuthenticatorTestCase extends WebTestCase
+{
+ protected static $client;
+
+ public static function setUpBeforeClass()
+ {
+ if (!interface_exists(ErrorRendererInterface::class)) {
+ self::markTestSkipped();
+ }
+
+ parent::setUpBeforeClass();
+
+ self::$client = self::createClient(['test_case' => static::getTestCase()]);
+ }
+
+ public static function tearDownAfterClass()
+ {
+ self::deleteTmpDir(static::getTestCase());
+
+ parent::tearDownAfterClass();
+ }
+
+ public function testNoCredentialsGives401()
+ {
+ self::$client->request('POST', '/api/login', [], [], ['CONTENT_TYPE' => 'application/json']);
+ $response = self::$client->getResponse();
+
+ $this->assertEquals(401, $response->getStatusCode());
+ $this->assertEquals('application/json', $response->headers->get('Content-Type'));
+ }
+
+ public function testWrongCredentialsGives401()
+ {
+ $this->sendRequestContainingInvalidCredentials('/api/login');
+
+ $response = self::$client->getResponse();
+
+ $this->assertEquals(401, $response->getStatusCode());
+ $this->assertEquals('application/json', $response->headers->get('Content-Type'));
+ }
+
+ public function testSuccessfulLogin()
+ {
+ $this->sendRequestContainingValidCredentials('/api/login');
+
+ $response = self::$client->getResponse();
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertEquals('application/json', $response->headers->get('Content-Type'));
+ }
+
+ public function testAccessDeniedExceptionGives403()
+ {
+ $this->sendRequestContainingValidCredentials('/api/comments');
+
+ $response = self::$client->getResponse();
+
+ $this->assertEquals(403, $response->getStatusCode());
+ $this->assertEquals('application/json', $response->headers->get('Content-Type'));
+ }
+
+ abstract protected static function getTestCase(): string;
+
+ abstract protected function sendRequestContainingInvalidCredentials(string $path): void;
+
+ abstract protected function sendRequestContainingValidCredentials(string $path): void;
+}
diff --git a/Tests/Functional/AccessDeniedListenerTest.php b/Tests/Functional/AccessDeniedListenerTest.php
index e0d35ed21..f907ae7f7 100644
--- a/Tests/Functional/AccessDeniedListenerTest.php
+++ b/Tests/Functional/AccessDeniedListenerTest.php
@@ -13,6 +13,9 @@
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
+/**
+ * @group legacy
+ */
class AccessDeniedListenerTest extends WebTestCase
{
private static $client;
diff --git a/Tests/Functional/BasicAuthTest.php b/Tests/Functional/BasicAuthTest.php
new file mode 100644
index 000000000..f9aa6c9be
--- /dev/null
+++ b/Tests/Functional/BasicAuthTest.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\RestBundle\Tests\Functional;
+
+class BasicAuthTest extends AbstractAuthenticatorTestCase
+{
+ protected static function getTestCase(): string
+ {
+ return 'BasicAuth';
+ }
+
+ protected function sendRequestContainingInvalidCredentials(string $path): void
+ {
+ self::$client->request('POST', $path, [], [], [
+ 'PHP_AUTH_USER' => 'restapi',
+ 'PHP_AUTH_PW' => 'wrongpw',
+ ]);
+ }
+
+ protected function sendRequestContainingValidCredentials(string $path): void
+ {
+ self::$client->request('POST', $path, [], [], [
+ 'PHP_AUTH_USER' => 'restapi',
+ 'PHP_AUTH_PW' => 'secretpw',
+ ]);
+ }
+}
diff --git a/Tests/Functional/Bundle/TestBundle/Security/ApiTokenAuthenticator.php b/Tests/Functional/Bundle/TestBundle/Security/ApiTokenAuthenticator.php
index 1a1532f2b..8c6afe78d 100644
--- a/Tests/Functional/Bundle/TestBundle/Security/ApiTokenAuthenticator.php
+++ b/Tests/Functional/Bundle/TestBundle/Security/ApiTokenAuthenticator.php
@@ -55,7 +55,7 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token,
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
- throw new AuthenticationException('Token not valid');
+ return new JsonResponse(null, 401);
}
/**
diff --git a/Tests/Functional/CustomGuardAuthenticatorTest.php b/Tests/Functional/CustomGuardAuthenticatorTest.php
new file mode 100644
index 000000000..262f4f967
--- /dev/null
+++ b/Tests/Functional/CustomGuardAuthenticatorTest.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\RestBundle\Tests\Functional;
+
+class CustomGuardAuthenticatorTest extends AbstractAuthenticatorTestCase
+{
+ protected static function getTestCase(): string
+ {
+ return 'CustomGuardAuthenticator';
+ }
+
+ protected function sendRequestContainingInvalidCredentials(string $path): void
+ {
+ self::$client->request('POST', $path, [], [], ['HTTP_X-FOO' => 'BAR', 'CONTENT_TYPE' => 'application/json']);
+ }
+
+ protected function sendRequestContainingValidCredentials(string $path): void
+ {
+ self::$client->request('POST', $path, [], [], ['HTTP_X-FOO' => 'FOOBAR', 'CONTENT_TYPE' => 'application/json']);
+ }
+}
diff --git a/Tests/Functional/app/BasicAuth/bundles.php b/Tests/Functional/app/BasicAuth/bundles.php
new file mode 100644
index 000000000..c507cc0e7
--- /dev/null
+++ b/Tests/Functional/app/BasicAuth/bundles.php
@@ -0,0 +1,17 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+return [
+ new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
+ new \Symfony\Bundle\SecurityBundle\SecurityBundle(),
+ new \FOS\RestBundle\FOSRestBundle(),
+ new \FOS\RestBundle\Tests\Functional\Bundle\TestBundle\TestBundle(),
+];
diff --git a/Tests/Functional/app/BasicAuth/config.yml b/Tests/Functional/app/BasicAuth/config.yml
new file mode 100644
index 000000000..742075488
--- /dev/null
+++ b/Tests/Functional/app/BasicAuth/config.yml
@@ -0,0 +1,17 @@
+imports:
+ - { resource: ../config/default.yml }
+ - { resource: security.php }
+
+framework:
+ serializer:
+ enabled: true
+ router: { resource: "%kernel.project_dir%/BasicAuth/routing.yml" }
+
+fos_rest:
+ body_listener: false
+ exception:
+ exception_listener: false
+ serialize_exceptions: false
+ routing_loader: false
+ zone:
+ - { path: ^/api/* }
diff --git a/Tests/Functional/app/BasicAuth/routing.yml b/Tests/Functional/app/BasicAuth/routing.yml
new file mode 100644
index 000000000..58aa480c5
--- /dev/null
+++ b/Tests/Functional/app/BasicAuth/routing.yml
@@ -0,0 +1,11 @@
+api:
+ path: /api/comments
+ defaults:
+ _controller: FOS\RestBundle\Tests\Functional\Bundle\TestBundle\Controller\Api\CommentController::getComments
+ _format: json
+
+api_login:
+ path: /api/login
+ defaults:
+ _controller: FOS\RestBundle\Tests\Functional\Bundle\TestBundle\Controller\Api\CommentController::loginAction
+ _format: json
diff --git a/Tests/Functional/app/BasicAuth/security.php b/Tests/Functional/app/BasicAuth/security.php
new file mode 100644
index 000000000..b38f2505e
--- /dev/null
+++ b/Tests/Functional/app/BasicAuth/security.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Component\Security\Core\Security;
+use Symfony\Component\Security\Http\Controller\UserValueResolver;
+
+$defaultFirewall = [];
+if (method_exists(Security::class, 'getUser') && !class_exists(UserValueResolver::class)) {
+ $defaultFirewall['logout_on_user_change'] = true;
+}
+
+$container->loadFromExtension('security', [
+ 'encoders' => ['Symfony\Component\Security\Core\User\User' => 'plaintext'],
+ 'providers' => [
+ 'in_memory' => [
+ 'memory' => [
+ 'users' => [
+ 'restapi' => ['password' => 'secretpw', 'roles' => ['ROLE_API']],
+ ],
+ ],
+ ],
+ ],
+ 'firewalls' => [
+ 'default' => array_merge($defaultFirewall, [
+ 'provider' => 'in_memory',
+ 'anonymous' => 'lazy',
+ 'stateless' => true,
+ 'http_basic' => null,
+ ]),
+ ],
+ 'access_control' => [
+ ['path' => '^/api/comments', 'roles' => 'ROLE_ADMIN'],
+ ['path' => '^/api', 'roles' => 'ROLE_API'],
+ ],
+]);
diff --git a/Tests/Functional/app/CustomGuardAuthenticator/bundles.php b/Tests/Functional/app/CustomGuardAuthenticator/bundles.php
new file mode 100644
index 000000000..c507cc0e7
--- /dev/null
+++ b/Tests/Functional/app/CustomGuardAuthenticator/bundles.php
@@ -0,0 +1,17 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+return [
+ new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
+ new \Symfony\Bundle\SecurityBundle\SecurityBundle(),
+ new \FOS\RestBundle\FOSRestBundle(),
+ new \FOS\RestBundle\Tests\Functional\Bundle\TestBundle\TestBundle(),
+];
diff --git a/Tests/Functional/app/CustomGuardAuthenticator/config.yml b/Tests/Functional/app/CustomGuardAuthenticator/config.yml
new file mode 100644
index 000000000..f9be773e4
--- /dev/null
+++ b/Tests/Functional/app/CustomGuardAuthenticator/config.yml
@@ -0,0 +1,21 @@
+imports:
+ - { resource: ../config/default.yml }
+ - { resource: security.php }
+
+framework:
+ serializer:
+ enabled: true
+ router: { resource: "%kernel.project_dir%/CustomGuardAuthenticator/routing.yml" }
+
+fos_rest:
+ body_listener: false
+ exception:
+ exception_listener: false
+ serialize_exceptions: false
+ routing_loader: false
+ zone:
+ - { path: ^/api/* }
+
+services:
+ api_token_authenticator:
+ class: FOS\RestBundle\Tests\Functional\Bundle\TestBundle\Security\ApiTokenAuthenticator
diff --git a/Tests/Functional/app/CustomGuardAuthenticator/routing.yml b/Tests/Functional/app/CustomGuardAuthenticator/routing.yml
new file mode 100644
index 000000000..58aa480c5
--- /dev/null
+++ b/Tests/Functional/app/CustomGuardAuthenticator/routing.yml
@@ -0,0 +1,11 @@
+api:
+ path: /api/comments
+ defaults:
+ _controller: FOS\RestBundle\Tests\Functional\Bundle\TestBundle\Controller\Api\CommentController::getComments
+ _format: json
+
+api_login:
+ path: /api/login
+ defaults:
+ _controller: FOS\RestBundle\Tests\Functional\Bundle\TestBundle\Controller\Api\CommentController::loginAction
+ _format: json
diff --git a/Tests/Functional/app/CustomGuardAuthenticator/security.php b/Tests/Functional/app/CustomGuardAuthenticator/security.php
new file mode 100644
index 000000000..7039766cc
--- /dev/null
+++ b/Tests/Functional/app/CustomGuardAuthenticator/security.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Component\Security\Core\Security;
+use Symfony\Component\Security\Http\Controller\UserValueResolver;
+
+$defaultFirewall = [];
+if (method_exists(Security::class, 'getUser') && !class_exists(UserValueResolver::class)) {
+ $defaultFirewall['logout_on_user_change'] = true;
+}
+
+$container->loadFromExtension('security', [
+ 'encoders' => ['Symfony\Component\Security\Core\User\User' => 'plaintext'],
+ 'providers' => [
+ 'in_memory' => [
+ 'memory' => [],
+ ],
+ ],
+ 'firewalls' => [
+ 'default' => array_merge($defaultFirewall, [
+ 'provider' => 'in_memory',
+ 'anonymous' => 'lazy',
+ 'stateless' => true,
+ 'guard' => [
+ 'authenticators' => [
+ 'api_token_authenticator',
+ ],
+ ],
+ ]),
+ ],
+ 'access_control' => [
+ ['path' => '^/api/comments', 'roles' => 'ROLE_ADMIN'],
+ ['path' => '^/api', 'roles' => 'ROLE_API'],
+ ],
+]);
diff --git a/UPGRADING-2.8.md b/UPGRADING-2.8.md
index e4ffd6e8c..654721a3e 100644
--- a/UPGRADING-2.8.md
+++ b/UPGRADING-2.8.md
@@ -47,6 +47,7 @@ Upgrading From 2.7 To 2.8
* The following options have been deprecated:
+ * `fos_rest.access_denied_listener`
* `fos_rest.exception.exception_controller`
* `fos_rest.exception.exception_listener`
* `fos_rest.exception.service`
@@ -67,6 +68,7 @@ Upgrading From 2.7 To 2.8
* `FOS\RestBundle\Controller\ExceptionController`
* `FOS\RestBundle\Controller\TemplatingExceptionController`
* `FOS\RestBundle\Controller\TwigExceptionController`
+ * `FOS\RestBundle\EventListener\AccessDeniedListener`
* `FOS\RestBundle\EventListener\ExceptionListener`
* `FOS\RestBundle\Inflector\DoctrineInflector`
* `FOS\RestBundle\Inflector\InflectorInterface`
@@ -84,6 +86,7 @@ Upgrading From 2.7 To 2.8
* The following services and aliases are marked as `deprecated`, they will be removed in 3.0:
+ * `fos_rest.access_denied_listener`
* `fos_rest.exception_listener`
* `fos_rest.exception.controller`
* `fos_rest.exception.twig_controller`
diff --git a/UPGRADING-3.0.md b/UPGRADING-3.0.md
index da88cf979..6fe598732 100644
--- a/UPGRADING-3.0.md
+++ b/UPGRADING-3.0.md
@@ -43,6 +43,7 @@ Upgrading From 2.x To 3.0
* The following options have been removed:
+ * `fos_rest.access_denied_listener`
* `fos_rest.exception.exception_controller`
* `fos_rest.exception.exception_listener`
* `fos_rest.exception.service`
@@ -63,6 +64,7 @@ Upgrading From 2.x To 3.0
* `FOS\RestBundle\Controller\ExceptionController`
* `FOS\RestBundle\Controller\TemplatingExceptionController`
* `FOS\RestBundle\Controller\TwigExceptionController`
+ * `FOS\RestBundle\EventListener\AccessDeniedListener`
* `FOS\RestBundle\EventListener\ExceptionListener`
* `FOS\RestBundle\Inflector\DoctrineInflector`
* `FOS\RestBundle\Inflector\InflectorInterface`
@@ -80,6 +82,7 @@ Upgrading From 2.x To 3.0
* The following services and aliases have been removed:
+ * `fos_rest.access_denied_listener`
* `fos_rest.exception_listener`
* `fos_rest.exception.controller`
* `fos_rest.exception.twig_controller`