Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multisites with only one authentication point

Tags:

symfony

For a future project, I'm looking for a way to manage multisites development with Symfony2. In fact, each site will be on a different subdomain but will works the same way ; only the style will changed a little.

The thing is : the authentication is common to all subsites, and is managed by the main site (www.mydomain.com). Each multisites will then have its own database.

Is it possible to do so with Symfony2 ? I know it's possible to use multidomains, but I don't how about the authentication system. Do you have ideas on how to proceed ?

Thanks !

like image 238
Jérémy Dutheil Avatar asked Dec 15 '11 15:12

Jérémy Dutheil


1 Answers

Actually I've managed to do this in one of projects I'm working on.

It's a bit tricky but once you understand the basic concept behind the symfony's security layer it's extremely easy to integrate into your existing project.

First off, be sure to read this: http://symfony.com/doc/current/book/security.html. I'd also recommend taking a look at the cookbook's security section.

You won't find a straight anwer in the manual but it helps to understand the code I'm going to paste here.

The basic idea is to share the session id across the subdomains.

Note: for the sake of space, I'll be omitting the use and namespace tags in PHP. Don't forget to import and specify appropriate namespaces.

class LoginListener
{

    public function onLogin(InteractiveLoginEvent $event)
    {
        $token = $event->getAuthenticationToken();

        //multisite log-in
        if ($token->getUser() instanceof User)
        {
            $_SESSION['_user_id'] = $token->getUser()->getId();
        }
    }

}

class LogoutListener implements LogoutHandlerInterface
{
    public function logout(Request $request, Response $response, TokenInterface $token)
    {
        if (isset($_SESSION['_user_id']))
        {
            unset($_SESSION['_user_id']);
        }
    }
}

class SessionMatcher implements RequestMatcherInterface
{
    public function matches(Request $request)
    {
        $request->getSession()->start();
        return isset($_SESSION['_user_id']);
    }
}

class CrossLoginUserToken extends AbstractToken
{

    private $id;

    public function getId()
    {
        return $this->id;
    }

    public function __construct($id, array $roles = array())
    {
        parent::__construct($roles);

        $this->id = $id;

        parent::setAuthenticated(count($roles) > 0);
    }

    public function getCredentials()
    {
        return '';
    }

}

class CrossLoginProvider implements AuthenticationProviderInterface
{

    private $userProvider;

    public function __construct(UserProviderInterface $userProvider)
    {
        $this->userProvider = $userProvider;
    }

    public function authenticate(TokenInterface $token)
    {
        $user = $this->userProvider->loadUserByUsername($token->getId());

        if ($user)
        {
            $authenticatedToken = new CrossLoginUserToken($token->getId(),$user->getRoles());
            $authenticatedToken->setUser($user);

            return $authenticatedToken;
        }

        throw new AuthenticationException('The CrossSite authentication failed.');
    }

    public function supports(TokenInterface $token)
    {
        return $token instanceof CrossLoginUserToken;
    }

}

class CrossLoginListener implements ListenerInterface
{

    protected $securityContext;
    protected $authenticationManager;
    protected $session;

    public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, Session $session)
    {
        $this->securityContext = $securityContext;
        $this->authenticationManager = $authenticationManager;
        $this->session = $session;
    }

    public function handle(GetResponseEvent $event)
    {
        $this->session->start();
        if (!is_null($this->securityContext->getToken()) && $this->securityContext->getToken()->isAuthenticated())
        {
            return;
        }
        if (isset($_SESSION['_user_id']))
        {
            try
            {
                $token = $this->authenticationManager->authenticate(new CrossLoginUserToken($_SESSION['_user_id']));
                $this->securityContext->setToken($token);
            }
            catch (AuthenticationException $e)
            {
                throw $e;
            }
        }
    }

}

class CrossLoginFactory implements SecurityFactoryInterface
{
    public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
    {
        $providerId = 'security.authentication.provider.crosslogin.' . $id;
        $container
                ->setDefinition($providerId, new DefinitionDecorator('crosslogin.security.authentication.provider'))
                ->replaceArgument(0, new Reference($userProvider))
        ;

        $listenerId = 'security.authentication.listener.crosslogin.' . $id;
        $listener = $container->setDefinition($listenerId, new DefinitionDecorator('crosslogin.security.authentication.listener'));

        return array($providerId, $listenerId, $defaultEntryPoint);
    }

    public function getPosition()
    {
        return 'pre_auth';
    }

    public function getKey()
    {
        return 'crosslogin';
    }

    public function addConfiguration(NodeDefinition $node)
    {

    }

}

security_factories.yml:

   <?xml version="1.0" encoding="UTF-8"?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

        <services>
            <service id="security.authentication.factory.crosslogin" class="MyBundle\Security\Factory\CrossLoginFactory">
                <tag name="security.listener.factory" />
            </service>
        </services>
    </container>

config.xml:

<service id="crosslogin.security.authentication.provider" class="MyBundle\Security\Authentication\Provider\CrossLoginProvider">
    <argument />
</service>

<service id="crosslogin.security.authentication.listener" class="MyBundle\Security\Firewall\CrossLoginListener">
    <argument type="service" id="security.context" />
    <argument type="service" id="security.authentication.manager" />
    <argument type="service" id="session" />
</service>

<service id="crosslogin.session.matcher" class="MyBundle\Security\Matcher\SessionMatcher">

</service>

<service id="crosslogin.handler.logout" class="MyBundle\Listener\LogoutListener">
    <service id="listener.login" class="Backend\CmsBundle\Listener\LoginListener">
        <tag name="kernel.event_listener" event="security.interactive_login" method="onLogin" />
 </service>

And finally - the security.yml:

firewalls:

    ...

    crosslogin:
        crosslogin: true
        provider: dao_provider_by_id
        request_matcher: crosslogin.session.matcher
        logout:
            path: /secured/logout
            target: /
            invalidate_session: true
            handlers: [crosslogin.handler.logout]

providers:

    ...

    dao_provider_by_id:
        entity: { class: YOUR_SECURITY_CLASS_NAME, property: id }

factories:
  CrossLoginFactory: "%kernel.root_dir%/../src/MyBundle/Resources/config/security_factories.xml"

This is the simpliest and as neat as possible thing I could think of. The only "misused" class here is the SessionMatcher which only checks for the availbility of the session id in the session.

Good luck, and feel free to ask question in the comments section. I know this can be pretty confusing at the beginning.

like image 56
Itako Avatar answered Oct 17 '22 22:10

Itako