Skip to content

Content Negotiation should be performed before Authentication / FirewallListener #3577

@kralos

Description

@kralos

API Platform version(s) affected: 2.5.5

Description
Currently, AddFormatListener (priority 7) is executed after FirewallListener (priority 8). See registered kernel.request event listeners. This means the Security system cannot rely on Symfony\HttpFoundation\Request::getRequestFormat() as it has not yet been initialized.

When setting a response for the kernel.request event, the propagation is stopped. This means listeners with lower priority won't be executed.

How to reproduce
What if we had a Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface which relied on the request format to decide if it should return a HTTP 302 (see https://tools.ietf.org/html/rfc6749#section-4.1) redirect a user to login. But also supports https://tools.ietf.org/html/rfc6750#section-3 - notify an API client they must authenticate with a Bearer token?

    public function start(
        HttpFoundation\Request $request,
        AuthenticationException $authException = null
    ): HttpFoundation\Response {
        if ($request->getRequestFormat() === 'html') {
            /**
             * @see https://tools.ietf.org/html/rfc6749#section-4.1.1
             */
            $state = sha1(openssl_random_pseudo_bytes(1000));
            $stateKey = AuthorizationCodeAuthenticator::getStateKey($state);
            $this->cache->save($stateKey, $state, 300);

            $uri = $this->router->generate('auth_client_authorization_v2_authorize', [
                'response_type' => 'code',
                'client_id' => $this->oAuthClientId,
                'state' => $state,
            ], UrlGeneratorInterface::ABSOLUTE_URL);

            return new HttpFoundation\RedirectResponse($uri);
        } else {
            /**
             * @see https://tools.ietf.org/html/rfc6750#section-3
             */
            $data = [
                'message' => 'Authentication Required',
            ];
            $wwwAuthenticateHeader = sprintf(
                'Bearer realm="%s"',
                $this->realm
            );
            if (
                $request->headers->has('Authorization')
                && 'Bearer' === substr($request->headers->get('Authorization'), 0, 6)
            ) {
                $wwwAuthenticateHeader .= ', error="invalid_token"';
            }
            return new HttpFoundation\JsonResponse($data, HttpFoundation\Response::HTTP_UNAUTHORIZED, [
                'WWW-Authenticate' => $wwwAuthenticateHeader,
            ]);
        }
    }

Possible Solution
Move AddFormatListener to a priority > 8

Additional Context

3b3f10d#r39354527

but we must double check that it has no bad side effects in term of security before changing this.

The side effects of running ApiPlatform\Core\EventListener\AddFormatListener are:

  • the Request::$formats are registered (Map from format names to mime types e.g. 'json' => ['application/json'])
  • the Request::$format is set (The format of the current request)
  • A NotAcceptableHttpException could be thrown (I think this is a good thing, content negotiation is generally cheaper than authentication)

When priority is < 8 the Request::$format is left null (unless manipulated outside of api-platform/core by another higher priority listener).

Since event dispatcher priorities are not strictly defined by Symfony, we should look to other projects for consensus on a good priority to use for AddFormatListener.

We should start a list of notable bundles that register kernel.request event listeners which might affect the chosen priority for AddFormatListener. Interoperability with other libraries is an important consideration.

Example order of event listeners in a standard symfony 5 project:

Listener Priority
Symfony\Component\HttpKernel\EventListener\DebugHandlersListener::configure() 2048
Symfony\Component\HttpKernel\EventListener\ValidateRequestListener::onKernelRequest() 256
Symfony\Component\HttpKernel\EventListener\SessionListener::onKernelRequest() 128
Symfony\Component\HttpKernel\EventListener\LocaleListener::setDefaultLocale() 100
Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelRequest() 32
Symfony\Component\HttpKernel\EventListener\LocaleListener::onKernelRequest() 16
Symfony\Component\HttpKernel\EventListener\LocaleAwareListener::onKernelRequest() 15
Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener::configureLogoutUrlGenerator() 8
Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener::onKernelRequest() 8
Sentry\SentryBundle\EventListener\RequestListener::onKernelRequest() 1
Sentry\SentryBundle\EventListener\SubRequestListener::onKernelRequest() 1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions