Search code examples
phpsymfonysymfony4api-platform.comsymfony-security

API Platform disable redirect to login


I am developing a website using Symfony 4 and API Platform. My user resource is protected by standard Symfony security ("security": "is_granted('ROLE_USER')"):

/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
* @ApiResource(
*     collectionOperations={
*         "get_user_profile": {
*             "method": "GET",
*             "path": "/users/profile",
*             "controller": GetUserProfile::class,
*             "pagination_enabled": false,
*             "openapi_context": {"summary": "Retrieves the current User's profile."},
*             "security": "is_granted('ROLE_USER')"
*         },
*     },
*     itemOperations={},
*     normalizationContext={"groups": {"read"}},
* )
*/
class User implements UserInterface
{
}

The problem is that if I try to access this API the response is a redirect to the login page. How can I disable the redirect and leave the response as a 401?

The redirect is really useful for website users but API users should not receive an HTML response.

This is my security.yml:

security:
    encoders:
        App\Entity\User:
            algorithm: auto

    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            entity:
                class: App\Entity\User
                property: username
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|img|js)/
            security: false
        main:
            anonymous: lazy
            guard:
                authenticators:
                    - App\Security\LoginFormAuthenticator
            logout:
                path: app_logout
                # where to redirect after logout
                # target: app_any_route

            remember_me:
                secret:   '%kernel.secret%'
                lifetime: 604800 # 1 week in seconds
                path:     /

            # activate different ways to authenticate
            # https://symfony.com/doc/current/security.html#firewalls-authentication

            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }

SOLUTION: I needed to override the start method with something like this:

/**
 * Override to control what happens when the user hits a secure page
 * but isn't logged in yet.
 *
 * @return RedirectResponse
 */
public function start(Request $request, AuthenticationException $authException = null)
{
    /** @var string */
    $format = $request->getRequestFormat();

    // API request
    if (false !== strpos($format, 'json')) {
        throw new HttpException(Response::HTTP_UNAUTHORIZED);
    }

    $url = $this->getLoginUrl();

    return new RedirectResponse($url);
}

Solution

  • The problem is that you have only one guard authenticator, and that authenticator is LoginFormAuthenticator, which extends on AbstractFormLoginAuthenticator.

    This abstract class is handy, but it's thought out to be used for regular web clients, not for API flows.

    If you check it's start() method you'll find this:

        /**
         * Override to control what happens when the user hits a secure page
         * but isn't logged in yet.
         *
         * @return RedirectResponse
         */
        public function start(Request $request, AuthenticationException $authException = null)
        {
            $url = $this->getLoginUrl();
            return new RedirectResponse($url);
        }
    

    So any time an authenticated request hits, it gets this redirect response.

    Your options are:

    Cheap and easy:

    Override start() on LoginFormAuthenticator so it returns something like:

    return new JsonReponse(
        ['status' => 'KO', 'message' => 'Unauthorized'],
        Response::HTTP_UNAUTHORIZED]
    );
    

    This would will return a JSON with 401 an all unauthorized requests. But "regular" web requests would not get redirected to the login form. Which is not ideal.

    Slightly better:

    Check on start() if the request has an application/* content-type, and return a 401 on those, but redirect the rest to the login form.

    What you should actually do down the road:

    The API URLs should be protected by a different guard authenticator than regular web paths. Reserve the login-form for traffic not related to the API. The Api-Platform package has built-in support for JWT authentication. Just use that, it is very easy to implement.