[Symfony 2] Security Bundle: Set User Locale on Form Login

[UPDATE]
A recent update to this article may be found here: https://nerdpress.org/symfony-2-set-default-locale-on-form-login-2/

Das Security Bundle ist ein wenig magisch. Da muss man eine HTML-Form definieren, der Rest wird irgendwie konfiguriert (Namen der Post-Parameter wie “_username”, “_password” etc., den Redirect zum Referrer, Remember-Me Funktion und so weiter, das alles wird von der Firewall intern geregelt. Man muss nur eine Login-Route definieren, einen Stub-Controller + Action-Callable (der aber nie ausgeführt wird, weil die Firefall sich davorhängt), fertig.

Das ist angenehm einfach, solange man keine Fragen stellt. Aber wie führe ich zusätzliche Aktionen direkt nach erfolgtem Login aus, ohne Einfuss auf den Code des Security Bundles zu haben?

Meist sucht man dazu einen Hook, Einstiegspunkt oder eine Konfigurationsvariable. In Symfony 2 sucht man natürlich Events, doch die Doku schweigt sich hierzu noch ein wenig aus, also wieder ‘rein in den Sourcecode. Irgendwo muss doch eine Möglichkeit zu finden sein, und tatsächlich, im AbstractAuthentificationListener werden wir fündig:

vendorsymfonysrcSymfonyComponentSecurityHttpFirewallAbstractAuthentificationListener.php

namespace SymfonyComponentSecurityHttpFirewall;
//...
abstract class AbstractAuthenticationListener implements ListenerInterface
{
// ...
private function onSuccess(GetResponseEvent $event, Request $request, TokenInterface $token)
{
if (null !== $this->logger) {
$this->logger->info(sprintf('User "%s" has been authenticated successfully', $token->getUsername()));
}
$this->securityContext->setToken($token);
$session = $request->getSession();
$session->remove(SecurityContextInterface::AUTHENTICATION_ERROR);
$session->remove(SecurityContextInterface::LAST_USERNAME);
if (null !== $this->dispatcher) {
$loginEvent = new InteractiveLoginEvent($request, $token);
$this->dispatcher->dispatch(SecurityEvents::INTERACTIVE_LOGIN, $loginEvent);
}
if (null !== $this->successHandler) {
$response = $this->successHandler->onAuthenticationSuccess($request, $token);
} else {
$response = $this->httpUtils->createRedirectResponse($request, $this->determineTargetUrl($request));
}
if (null !== $this->rememberMeServices) {
$this->rememberMeServices->loginSuccess($request, $response, $token);
}
return $response;
}

Die Klasse ist nur auszugsweise abgedruckt. Interessant sind die Zeilen 233 & 238 im Original-Sourcecode.

Zeile 233 ff. benachrichtigt alle Eventlistener, die auf SecurityEvents::INTERACTIVE_LOGIN hören, wenn ein Event-Dispatcher vorhanden ist (im Falle der Symfony Standard Distribution kann man das voraussetzen).

Zeile 238 ist eine Sonderlocke, sozusagen ein “Security-Bundle-eigenes” Event. Man hat die Möglichkeit, in der jeweiligen Firewall-Einstellung für den Form-Login in der security.yml mit success_handler einen eigenen DIC-Service anzugeben, der SymfonyComponentSecurityHttpAuthenticationAuthenticationSuccessHandlerInterface implementiert.

Das sieht dann in etwa so aus:

secured_area:
pattern:    ^/demo/secured/
form_login:
check_path: /demo/secured/login_check
login_path: /demo/secured/login
success_handler: dvlp_core.login_success_handler

Die Implementierung der Callback-Methode onAuthenticationSuccess(Request $request, TokenInterface $token) des Interfaces ist aber nicht ganz trivial, da als Rückgabewert eben eine Instanz von SymfonyComponentHttpFoundationResponse erwartet wird, die man dann selbsttätig konfigurieren muss (inkl. Rücksprung-Adresse, Referer-Logik und Auswertung der Benutzerkonfiguration). Daher lassen wir das erstmal sein und nutzen den Event-Dispatcher, um uns in den Login hineinzukrallen. Dazu definieren wir einen neuen Service in einem unserer Bundles und taggen ihn als Eventlistener:

Resourcesconfigservices.xml:

<parameters>
<parameter key="dvlp_core.event_listener.class">DvlpCoreBundleEventListener</parameter>
</parameters>
<service id="dvlp_core.event_listener" class="%dvlp_core.event_listener.class%">
<tag name="kernel.event_listener" event="security.interactive_login" method="onSecurityInteractiveLogin" />
</service>

Dann implementieren wir den Listener. Dieser Listener soll die Session-Locale “on login” auf die Lokalisierungs-Einstellung des aktuellen (Datenbank-)-Users setzen.

DvlpCoreBundleEventListener.php:

class EventListener
{
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$token = $event->getAuthenticationToken();
$session = $event->getRequest()->getSession();
$session->setLocale($token->getUser()->getLocale());
}
}

Das war´s auch schon. Events to the Rescue. Leider scheinen diese wirklich noch nicht allzu ausführlich dokumentiert zu sein, ich wäre froh, wenn mir jemand das Gegenteil aufzeigen kann!

Viel Spaß beim Ausprobieren.

4 Replies to “[Symfony 2] Security Bundle: Set User Locale on Form Login”

  1. Hey,

    Max was kind enough translating the article, thanks for the effort! Feel free to reuse it in any way, and thanks for your inquiry :)

    The Security Bundle is a bit magical. You have to define a HTML-Form, the rest gets configured somehow elsewhere (post-parameter names like “_username”, “_password” etc., the referrer redirect, Remember-Me functionality etc., all that is handled by the firewall. You just have to define a Login-Route, a Stub-Controller + Action-Callable (which actually never gets executed, as the firewall comes first), done.

    Very comfortable – as long as you don’t start asking questions. But how do you execute functions directly after the login, without altering the Security-Bundle Code?

    Most of the time you’ll try to find a Hook, Entry-Point or Config-Variable.
    In Symfony 2 of course, you’ll be looking for events, but the Documentation does not say anything about that.
    So back to the source-code.
    There has to be a possibility – and there is. It’s to be found in the AbstractAuthentificationListener:

    vendorsymfonysrcSymfonyComponentSecurityHttpFirewallAbstractAuthentificationListener.php

    namespace SymfonyComponentSecurityHttpFirewall;
    //...
    abstract class AbstractAuthenticationListener implements ListenerInterface
    {
    // ...
    private function onSuccess(GetResponseEvent $event, Request $request, TokenInterface $token)
    {
    if (null !== $this->logger) {
    $this->logger->info(sprintf('User "%s" has been authenticated successfully', $token->getUsername()));
    }
    $this->securityContext->setToken($token);
    $session = $request->getSession();
    $session->remove(SecurityContextInterface::AUTHENTICATION_ERROR);
    $session->remove(SecurityContextInterface::LAST_USERNAME);
    if (null !== $this->dispatcher) {
    $loginEvent = new InteractiveLoginEvent($request, $token);
    $this->dispatcher->dispatch(SecurityEvents::INTERACTIVE_LOGIN, $loginEvent);
    }
    if (null !== $this->successHandler) {
    $response = $this->successHandler->onAuthenticationSuccess($request, $token);
    } else {
    $response = $this->httpUtils->createRedirectResponse($request, $this->determineTargetUrl($request));
    }
    if (null !== $this->rememberMeServices) {
    $this->rememberMeServices->loginSuccess($request, $response, $token);
    }
    return $response;
    }
    

    The class is just printed as an excerpt here, interesting are lines 233 and 238 in the original source.

    line 233 and the following notifies all AbstractAuthentificationListener which listen to SecurityEvents::INTERACTIVE_LOGIN, if a Event-Dispatcher is defined. (Which can be expected in the sf2 standard distribution)

    line 238 is something special, a “Security-Bundle-specific” Event.
    there’s a possibility to set a DIC-Service in the firewall settings in the security.yml via “success_handler”.

    Man hat die Möglichkeit, in der jeweiligen Firewall-Einstellung für den Form-Login in der security.yml mit success_handler which implements SymfonyComponentSecurityHttpAuthenticationAuthenticationSuccessHandlerInterface.

    This looks like this:

    secured_area:
    pattern:    ^/demo/secured/
    form_login:
    check_path: /demo/secured/login_check
    login_path: /demo/secured/login
    success_handler: dvlp_core.login_success_handler
    

    Implementation of the Callback Method onAuthenticationSuccess(Request $request, TokenInterface $token) of the Interface is not that easy though, as it expects an instance of SymfonyComponentHttpFoundationResponse as return value,
    which you have to configure by yourself (including Redirect-Adress, referrer-logic and processing of the user config)

    That’s why we’ll leave that alone for the moment and use the Event-Dispatcher, to hook into the login.
    Therefor we define a new Server in one of our Bundles and tag it as Eventlistener:

    Resourcesconfigservices.xml:

    <parameters>
    <parameter key="dvlp_core.event_listener.class">DvlpCoreBundleEventListener</parameter>
    </parameters>
    <service id="dvlp_core.event_listener" class="%dvlp_core.event_listener.class%">
    <tag name="kernel.event_listener" event="security.interactive_login" method="onSecurityInteractiveLogin" />
    </service>
    

    Now implement the listener. The listener should set the Session-Locale “on login” in the locale-settings of the current (DB)-User.

    DvlpCoreBundleEventListener.php:

    class EventListener
    {
    public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
    {
    $token = $event->getAuthenticationToken();
    $session = $event->getRequest()->getSession();
    $session->setLocale($token->getUser()->getLocale());
    }
    }
    

    And That’s that -Events to the Rescue.
    Unfortunately there seems to be not so much documentation on that atm.
    But I’ll be happy to be proven wrong.

    Have fun trying that at home.

Leave a Reply

Your email address will not be published. Required fields are marked *