chalasr chalasr - 7 months ago 35
PHP Question

Two firewalls with two login_form and two different AuthenticationSuccessHandlers but only one used for both

Pretty much all is in the title.

I come here before to be sure that is a bug before report it as an issue on symfony/symfony.

I have the following security.yml:

security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext

providers:
in_memory:
memory:
users:
chalasr: { password: chalasr, roles: [ 'ROLE_USER' ] }

firewalls:
admin:
pattern: ^/admin
form_login:
provider: in_memory
login_path: /admin/login
check_path: /admin/login_check
failure_path: null
success_handler: admin.authentication_success_handler
logout:
path: /admin/logout
anonymous: true

login_api:
pattern: ^/v1/login
stateless: true
anonymous: true
form_login:
provider: in_memory
check_path: /v1/login_check
require_previous_session: false
username_parameter: username
password_parameter: password
success_handler: api.authentication_success_handler

api:
pattern: ^/v1/
stateless: true
lexik_jwt: ~

access_control:
- { path: ^/admin/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: ROLE_USER }
- { path: ^/v1/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/v1/, role: ROLE_USER }


As you can see, I have two login firewalls, one for routes matching with
^/admin
and one for those matching with
^/v1
.

The two
form_login
have one respective
authentication_success_handler
set.

The api handler:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class ApiAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
/**
* {@inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
die('interecepted by api');
}
}


The admin handler:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class AdminAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
/**
* {@inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
die('interecepted by admin');
}
}


Services:

admin.authentication_success_handler:
class: AppBundle\EventListener\AdminAuthenticationSuccessHandler
tags:
- { name: kernel.event_listener, event: security.on_authentication_success, method: onAuthenticationSuccess }

api.authentication_success_handler:
class: AppBundle\EventListener\ApiAuthenticationSuccessHandler
tags:
- { name: kernel.event_listener, event: security.on_authentication_success, method: onAuthenticationSuccess }


The problem is that the
admin.authentication_success_handler
is the one used for both firewalls, it says "intercepted by admin" at all, for the api login route, and for the admin login route (form).

My first goal is too make stuffs that totally depend on the firewall used, and this behavior causes a bug in my application, because of the separation admin/api.

At start when I discovered the bug, I used LexikJWTAuthenticationBundle and FOSUserBundle for the api, and SonataAdminBundle (with FOSUB) for the admin, on Symfony 2.8.

Then I upgraded to 3.0 plus removed any 3rd party security bundles, in order to avoid any doubt about these and so to be sure about that is coming from symfony. (this is why I use a simple plaintext encoder, as simple as possible).

Am I right when I assume it's a bug?
Or I just do something wrong?

Because the thing is apparently not documented yet and we can find lots of different (working or not) implementations.

EDIT

Hi @tftd, thank's for your work.

I'm really sorry but I just too shortened my code to make the question readable, so the missing
anonymous: true
in the two is a simple typo from my side, I added them.

In fact, the implementation works fine in a project that is finished, it is separated in an api and an admin, both on different hosts, the admin firewall with a login form provided by FOSUserBundle and the api firewall with a simple /login_check endpoint that provides a JWT, then users send authenticated requests to
/v1/*
routes with the token as header (bearer).

I upgraded to 3.0 to be in phase, and I used a simple
in_memory
to avoid any doubt about 3rd party bundles (FOSUserBundle success handling, LexikJWT success handling or anything else that can be the cause of the error), and I hope I really avoid any doubt about them.

You can see my simple routing.yml (keep in mind that I'm presenting the issue with a sample that is just used to see how the login success behave, it's absolutely not a real-world usage, but I come from the same behavior on a real-world usage).

// app/config/routing.yml
fos_user:
prefix: /admin
resource: "@FOSUserBundle/Resources/config/routing/all.xml"

api_login_check:
path: /v1/login_check


The routes
/admin/*
are managed by FOSUserBundle which provide the login form, my goal is just to see if the two login success are intercepted, and in fact, they are.

The problem was coming from that my two listeners was listening on
security.interactive_login
, it looked like this:

class ApiAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{

return $this->onAuthenticationSuccess($event->getRequest(), $event->getAuthenticationToken());
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
die('Interecepted by api');
}
}


And the two services declarations looked like:

api.authentication_success_handler:
class: AppBundle\EventListener\AuthenticationSuccessHandlerApi ]
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onSecurityInteractiveLogin }


With the tags set, only one listener is fired.

By removing the tags, respective handlers are properly called on each firewall.

Conclusion, no need of tag an AuthenticationSuccessListener (except for any other reason than throw the onAuthenticationSuccess), and if you tag it, tag it as a
security.on_authentication_success
event, not on
security.interactive_login
, because, no matter of the handler defined in security.yml, the first will be used onInteractiveLogin and so the second will be totally ignored.

Thank's @Federico for the good answer, and thanks again @tdtd.

Answer

I think that the problem lie when you subscribe AdminAuthenticationSuccessHandler's onAuthenticationSuccess method to the event security.on_authentication_success. It will be trigger every time a user is authenticated by one provider, it does not matter which. Since you have register admin.authentication_success_handler first, it will be run first.

I'm not quite sure of that, because the event name is security.authentication.success, but try to change your service definition with

admin.authentication_success_handler:
    class: AppBundle\EventListener\AdminAuthenticationSuccessHandler

api.authentication_success_handler:
    class: AppBundle\EventListener\ApiAuthenticationSuccessHandler