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);
}
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:
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.
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.
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.