Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony2 extending DefaultAuthenticationSuccessHandler

I want to alter default authentication process just after authentication success. I made a service that is called after authentication success and before redirect.

namespace Pkr\BlogUserBundle\Handler;
use Doctrine\ORM\EntityManager;
use Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authentication\Response;

class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{

    protected $entityManager = null;
    protected $logger = null;
    protected $encoder = null;

    public function __construct(EntityManager $entityManager, LoggerInterface $logger, WpTransitionalEncoder $encoder)
    {
        $this->entityManager = $entityManager;
        $this->logger = $logger;
        $this->encoder = $encoder;
    }

    /**
    * This is called when an interactive authentication attempt succeeds. This
    * is called by authentication listeners inheriting from
    * AbstractAuthenticationListener.
    *
    * @param Request $request
    * @param TokenInterface $token
    *
    * @return Response never null
    */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        $user = $token->getUser();
        $newPass = $request->get('_password');
        $user->setUserPassword($this->encoder->encodePassword($newPass, null));
        $this->entityManager->persist($user);
        $this->entityManager->flush();
        //do redirect
    }
}

in services.yml

services:
    pkr_blog_user.wp_transitional_encoder:
        class: "%pkr_blog_user.wp_transitional_encoder.class%"
        arguments:
            cost: "%pkr_blog_user.wp_transitional_encoder.cost%"
            logger: @logger
    pkr_blog_user.login_success_handler:
        class: Pkr\BlogUserBundle\Handler\AuthenticationSuccessHandler
        arguments:
            entity_manager: @doctrine.orm.entity_manager
            logger: @logger
            encoder: @pkr_blog_user.wp_transitional_encoder

and in security.yml

firewalls:
    dev:
        pattern:  ^/(_(profiler|wdt)|css|images|js)/
        security: false

    secured_area:
        pattern:   ^/
        anonymous: ~
        form_login:
            login_path:  pkr_blog_admin_login
            check_path:  pkr_blog_admin_login_check
            success_handler: pkr_blog_user.login_success_handler
        logout:
            path: pkr_blog_admin_logout
            target: /

What I'm trying achieve is to just alter default behavior a little so I think why not to extend DefaultAuthenticationSuccessHandler, add something to onSuccessHandler() and call parent::onSucessHandler(). I tried and the problem is that I have no clue how to add security parameters (set in security.yml) to my extended class constructor. DefaultAuthenticationSuccessHandler uses HttpUtils and $options array:

/**
 * Constructor.
 *
 * @param HttpUtils $httpUtils
 * @param array     $options   Options for processing a successful authentication attempt.
 */
public function __construct(HttpUtils $httpUtils, array $options)
{
    $this->httpUtils   = $httpUtils;

    $this->options = array_merge(array(
        'always_use_default_target_path' => false,
        'default_target_path'            => '/',
        'login_path'                     => '/login',
        'target_path_parameter'          => '_target_path',
        'use_referer'                    => false,
    ), $options);
}

So my extended class constructor should look like:

    // class extends DefaultAuthenticationSuccessHandler
    protected $entityManager = null;
    protected $logger = null;
    protected $encoder = null;

    public function __construct(HttpUtils $httpUtils, array $options, EntityManager $entityManager, LoggerInterface $logger, WpTransitionalEncoder $encoder)
    {
        $this->entityManager = $entityManager;
        $this->logger = $logger;
        $this->encoder = $encoder;
    }

It's quite easy to add HttpUtils service to my services.yml, but what with options argument?

services:
    pkr_blog_user.wp_transitional_encoder:
        class: "%pkr_blog_user.wp_transitional_encoder.class%"
        arguments:
            cost: "%pkr_blog_user.wp_transitional_encoder.cost%"
            logger: @logger
    pkr_blog_user.login_success_handler:
        class: Pkr\BlogUserBundle\Handler\AuthenticationSuccessHandler
        arguments:
            httputils: @security.http_utils
            options: [] #WHAT TO ADD HERE ?
            entity_manager: @doctrine.orm.entity_manager
            logger: @logger
            encoder: @pkr_blog_user.wp_transitional_encoder
like image 607
piotrekkr Avatar asked Apr 10 '13 06:04

piotrekkr


3 Answers

If you only have one success / failure handler defined for your application, there's a slightly easier way to do this. Rather than define a new service for the success_handler and failure_handler, you can override security.authentication.success_handler and security.authentication.failure_handler instead.

Example:

services.yml

services:     security.authentication.success_handler:         class:  StatSidekick\UserBundle\Handler\AuthenticationSuccessHandler         arguments:  ["@security.http_utils", {}]         tags:             - { name: 'monolog.logger', channel: 'security' }      security.authentication.failure_handler:         class:  StatSidekick\UserBundle\Handler\AuthenticationFailureHandler         arguments:  ["@http_kernel", "@security.http_utils", {}, "@logger"]         tags:             - { name: 'monolog.logger', channel: 'security' } 

AuthenticationSuccessHandler.php

<?php namespace StatSidekick\UserBundle\Handler;  use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler; use Symfony\Component\Security\Http\HttpUtils;  class AuthenticationSuccessHandler extends DefaultAuthenticationSuccessHandler {      public function __construct( HttpUtils $httpUtils, array $options ) {         parent::__construct( $httpUtils, $options );     }      public function onAuthenticationSuccess( Request $request, TokenInterface $token ) {         if( $request->isXmlHttpRequest() ) {             $response = new JsonResponse( array( 'success' => true, 'username' => $token->getUsername() ) );         } else {             $response = parent::onAuthenticationSuccess( $request, $token );         }         return $response;     } } 

AuthenticationFailureHandler.php

<?php namespace StatSidekick\UserBundle\Handler;  use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler; use Symfony\Component\Security\Http\HttpUtils;  class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler {      public function __construct( HttpKernelInterface $httpKernel, HttpUtils $httpUtils, array $options, LoggerInterface $logger = null ) {         parent::__construct( $httpKernel, $httpUtils, $options, $logger );     }      public function onAuthenticationFailure( Request $request, AuthenticationException $exception ) {         if( $request->isXmlHttpRequest() ) {             $response = new JsonResponse( array( 'success' => false, 'message' => $exception->getMessage() ) );         } else {             $response = parent::onAuthenticationFailure( $request, $exception );         }         return $response;     } } 

In my case, I was just trying to set something up so that I could get a JSON response when I try to authenticate using AJAX, but the principle is the same.

The benefit of this approach is that without any additional work, all of the options that are normally passed into the default handlers should get injected correctly. This happens because of how SecurityBundle\DependencyInjection\Security\Factory is setup in the framework:

protected function createAuthenticationSuccessHandler($container, $id, $config) {     ...     $successHandler = $container->setDefinition($successHandlerId, new DefinitionDecorator('security.authentication.success_handler'));         $successHandler->replaceArgument(1, array_intersect_key($config, $this->defaultSuccessHandlerOptions));     ... }  protected function createAuthenticationFailureHandler($container, $id, $config) {     ...     $failureHandler = $container->setDefinition($id, new DefinitionDecorator('security.authentication.failure_handler'));     $failureHandler->replaceArgument(2, array_intersect_key($config, $this->defaultFailureHandlerOptions));     ... } 

It specifically looks for security.authentication.success_handler and security.authentication.failure_handler in order to merge options from your config into the arrays passed in. I'm sure there's a way to setup something similar for your own service, but I haven't looked into it yet.

Hope that helps.

like image 68
dmccabe Avatar answered Oct 13 '22 00:10

dmccabe


You can easily see how default security listeners are manage in this file :

vendor/symfony/symfony/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml

For example, DefaultAuthenticationSuccessHandler is registered like that:

    <!-- Parameter -->      <parameter key="security.authentication.success_handler.class">Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler</parameter>      <!-- Service -->      <service id="security.authentication.success_handler" class="%security.authentication.success_handler.class%" abstract="true" public="false">         <argument type="service" id="security.http_utils" />         <argument type="collection" /> <!-- Options -->     </service> 

So finally we can see that the option collection is empty by default !

options: {} will do the job ^^ (Think a collection is represent by {} in yaml)

like image 26
Sybio Avatar answered Oct 12 '22 22:10

Sybio


For the best solution so far scroll to bottom of this answer

OK I finally got it working in a way I wanted. The problem was that Symfony2 was not passing config array from security.yml to constructor when custom handler is set. So what I did was:

1) I removed custom handler declaration from security.yml

firewalls:
    dev:
      pattern:  ^/(_(profiler|wdt)|css|images|js)/
      security: false

secured_area:
    pattern:   ^/
    anonymous: ~
    form_login:
        login_path:  pkr_blog_admin_login
        check_path:  pkr_blog_admin_login_check
    logout:
        path: pkr_blog_admin_logout
        target: /

2) AuthenticationSuccessHandler extends default handler class, rehash user password and finally let default handler do the rest. Two new arguments was added in constructor:

#/src/Pkr/BlogUserBundle/Handler/AuthenticationSuccessHandler.php
namespace Pkr\BlogUserBundle\Handler;
use Doctrine\ORM\EntityManager;
use Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
use Symfony\Component\Security\Http\Authentication\Response;
use Symfony\Component\Security\Http\HttpUtils;

class AuthenticationSuccessHandler extends DefaultAuthenticationSuccessHandler
{

    protected $entityManager = null;
    protected $logger = null;
    protected $encoder = null;

    public function __construct(
        HttpUtils $httpUtils,
        array $options,
        // new arguments below
        EntityManager $entityManager = null, # entity manager
        WpTransitionalEncoder $encoder = null
    )
    {
        $this->entityManager = $entityManager;
        $this->encoder = $encoder;
        parent::__construct($httpUtils, $options);
    }

    /**
    * This is called when an interactive authentication attempt succeeds. This
    * is called by authentication listeners inheriting from
    * AbstractAuthenticationListener.
    *
    * @param Request $request
    * @param TokenInterface $token
    *
    * @return Response never null
    */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        $user = $token->getUser();
        if (preg_match('^\$P\$', $user->getUserPassword())) {
            $newPass = $request->get('_password');
            $user->setUserPassword($this->encoder->encodePassword($newPass, null));
            $this->entityManager->persist($user);
            $this->entityManager->flush();
        }
        return parent::onAuthenticationSuccess($request, $token);
    }
}

3) added and changed some parameters in my services.yml so I could use them in my compiler pass class:

#/src/Pkr/BlogUserBundle/Resources/config/services.yml
parameters:
    pkr_blog_user.wp_transitional_encoder.cost: 20
    # password encoder class
    pkr_blog_user.wp_transitional_encoder.class: Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder
    # authentication success handler class
    pkr_blog_user.login_success_handler.class: Pkr\BlogUserBundle\Handler\AuthenticationSuccessHandler
    # entity manager service name
    pkr_blog_user.login_success_handler.arg.entity_manager: doctrine.orm.entity_manager
    # encoder service name
    pkr_blog_user.login_success_handler.arg.encoder: pkr_blog_user.wp_transitional_encoder

services:
    pkr_blog_user.wp_transitional_encoder:
        class: "%pkr_blog_user.wp_transitional_encoder.class%"
        arguments:
            cost: "%pkr_blog_user.wp_transitional_encoder.cost%"
            logger: @logger
    pkr_blog_user.login_success_handler:
        class: "%pkr_blog_user.login_success_handler.class%"

4) created a compiler pass class RehashPasswordPass that changes default authentication success handler and adds some parameters to constructor:

#/src/Pkr/BlogUserBundle/DependencyInjection/Compiler/RehashPasswordPass.php
namespace Pkr\BlogUserBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class RehashPasswordPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if ($container->hasDefinition('security.authentication.success_handler')) {
            // definition of default success handler
            $def = $container->getDefinition('security.authentication.success_handler');
            // changing default class
            $def->setClass($container->getParameter('pkr_blog_user.login_success_handler.class'));
            $entityMngRef = new Reference(
                $container->getParameter("pkr_blog_user.login_success_handler.arg.entity_manager")
            );
            // adding entity manager as third param to constructor
            $def->addArgument($entityMngRef);
            $encoderRef = new Reference(
                $container->getParameter("pkr_blog_user.login_success_handler.arg.encoder")
            );
            // adding encoder as fourth param to constructor
            $def->addArgument($encoderRef);
        }
    }
}

5) added compiler pass to container builder:

#/src/Pkr/BlogUserBundle/PkrBlogUserBundle.php
namespace Pkr\BlogUserBundle;

use Pkr\BlogUserBundle\DependencyInjection\Compiler\RehashPasswordPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class PkrBlogUserBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        $container->addCompilerPass(new RehashPasswordPass());
    }
}

Now default handler class was changed but symfony will still pass configuration from security.yml to constructor plus two new arguments added by compiler pass.

The better way

Event handler as a service with setters

#/src/Pkr/BlogUserBundle/Resources/config/services.yml
parameters:
    pkr_blog_user.wp_transitional_encoder.cost: 15
    # password encoder class
    pkr_blog_user.wp_transitional_encoder.class: Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder
    # authentication success handler class
    pkr_blog_user.authentication_success_handler.class: Pkr\BlogUserBundle\EventHandler\AuthenticationSuccessHandler


services:
    pkr_blog_user.wp_transitional_encoder:
        class: "%pkr_blog_user.wp_transitional_encoder.class%"
        arguments:
            cost: "%pkr_blog_user.wp_transitional_encoder.cost%"
            logger: @logger

    pkr_blog_user.authentication_success_handler:
        class: "%pkr_blog_user.authentication_success_handler.class%"
        calls:
            - [ setRequest, [ @request ]]
            - [ setEntityManager, [ @doctrine.orm.entity_manager ]]
            - [ setEncoder, [ @pkr_blog_user.wp_transitional_encoder ]]
        tags:
            - { name: kernel.event_listener, event: security.authentication.success , method: handleAuthenticationSuccess }

Event handler class

# /src/Pkr/BlogUserBundle/EventHandler/AuthenticationSuccessHandler.php
namespace Pkr\BlogUserBundle\EventHandler;
use Doctrine\ORM\EntityManager;
use Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Event\AuthenticationEvent;

class AuthenticationSuccessHandler {

    protected $entityManager = null;
    protected $encoder = null;

    public function setRequest(Request $request)
    {
        $this->request = $request;
    }

    public function setEntityManager(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function setEncoder(WpTransitionalEncoder $encoder)
    {
        $this->encoder = $encoder;
    }

    public function handleAuthenticationSuccess(AuthenticationEvent $event)
    {
        $token = $event->getAuthenticationToken();
        $user = $token->getUser();
        if (preg_match('^\$P\$', $user->getUserPassword())) {
            $newPass = $this->request->get('_password');
            $user->setUserPassword($this->encoder->encodePassword($newPass, null));
            $this->entityManager->persist($user);
            $this->entityManager->flush();
        }
    }

}

And it's all working, no compiler pass needed. Why didn't I thought of that from the begining...

Uhh it stopped working after symfony update

Now I get exception:

ScopeWideningInjectionException: Scope Widening Injection detected: The definition "pkr_blog_user.authentication_success_handler" references the service "request" which belongs to a narrower scope. Generally, it is safer to either move "pkr_blog_user.authentication_success_handler" to scope "request" or alternatively rely on the provider pattern by injecting the container itself, and requesting the service "request" each time it is needed. In rare, special cases however that might not be necessary, then you can set the reference to strict=false to get rid of this error.

It seems that I need to pass full container to my service. So I modified services.yml and event handler class.

#/src/Pkr/BlogUserBundle/Resources/config/services.yml
parameters:
    pkr_blog_user.wp_transitional_encoder.cost: 15
    # password encoder class
    pkr_blog_user.wp_transitional_encoder.class: Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder
    # authentication success handler class
    pkr_blog_user.authentication_success_handler.class: Pkr\BlogUserBundle\EventHandler\AuthenticationSuccessHandler


services:
    pkr_blog_user.wp_transitional_encoder:
        class: "%pkr_blog_user.wp_transitional_encoder.class%"
        arguments:
            secure: @security.secure_random
            cost: "%pkr_blog_user.wp_transitional_encoder.cost%"

    pkr_blog_user.authentication_success_handler:
        class: "%pkr_blog_user.authentication_success_handler.class%"
        arguments:
            container: @service_container
        tags:
            - { name: kernel.event_listener, event: security.authentication.success , method: handleAuthenticationSuccess }

And event handler

# /src/Pkr/BlogUserBundle/EventHandler/AuthenticationSuccessHandler.php
namespace Pkr\BlogUserBundle\EventHandler;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Event\AuthenticationEvent;

class AuthenticationSuccessHandler
{

    /**
     * @var ContainerInterface
     */
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function handleAuthenticationSuccess(AuthenticationEvent $event)
    {
        $request = $this->container->get('request');
        $em = $this->container->get('doctrine.orm.entity_manager');
        $encoder = $this->container->get('pkr_blog_user.wp_transitional_encoder');
        $token = $event->getAuthenticationToken();
        $user = $token->getUser();
        if (preg_match('/^\$P\$/', $user->getUserPassword())) {
            $newPass = $request->get('_password');
            $user->setUserPassword($encoder->encodePassword($newPass, null));
            $em->persist($user);
            $em->flush();
        }
    }

}

And it works again.

Best way so far

The solution above was best I knew until @dmccabe wrote his solution.

like image 35
piotrekkr Avatar answered Oct 13 '22 00:10

piotrekkr