Mugo Web main content.

Authenticating user login with external systems using Symfony’s Guard Component

By: Thiago Campos Viana | October 25, 2021 | ibexadxp, development, ez platform, and integration

One of the most common features in any website is the login system which validates a user’s credentials and grants role-based access to certain content and functions. The Symfony-based Ibexa DXP includes a standard login system which checks encrypted login credentials against user information stored in the CMS’s database.

However, this approach may not be sufficient for the business needs of your website. For example, you might want to use the Amazon Cognito service to externally authenticate user credentials — note that Cognito is an authentication solution, as distinct from an entitlement solution. Many of our clients use a variety of external authentication systems and thus our CMS needs to have a general way to accomplish such an integration. 

The goal of a custom authentication handler is to create code that checks externally stored user credentials and then maps them to Ibexa users. In this tutorial, we’ll present an efficient way to implement a custom authentication handler using the Guard component. You can use this approach to provide users with access to internal or external systems such as customer relationship management or e-commerce.

Symfony’s Guard component

The Guard component is a simple authentication system for Symfony. Keep in mind, the component has been deprecated in version 5.3 and is being replaced by something that is still considered experimental. As Guard is expected to remain part of Symfony for some time, most likely until version 6.0, it is still an important component to understand and use within the context of custom login system development.

Guard vs. Kernel Events

Ibexa DXP is based on the Symfony development framework, and its login system can be extended in various ways.

In a previous post, we covered single sign-ons in Ibexa DXP’s multisite installations. In that case, we check an authentication cookie set externally on every page load, making use of Symfony Kernel Events, and depending on the case, authenticate the user.

Theoretically, we could develop something similar for a custom login handler using the same approach, but this would generate code that is very difficult to maintain. Adding code to the kernel event handler would require extra work because it would need to contain all the necessary login code. Guard gives us an easy-to-understand code framework.

For example, in our single sign-on code we call the following piece of code:

$repository = $this->container->get( 'ezpublish.api.repository' ); $ezUser = $repository->getUserService()->loadUserByLogin($cookieUser->getUsername()); $repository->getPermissionResolver()->setCurrentUserReference($ezUser); $user = new \eZ\Publish\Core\MVC\Symfony\Security\User($ezUser, ['ROLE_USER']); $token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken($user, null,'ezpublish_front', $user->getRoles()); $this->container->get('security.token_storage')->setToken($token); $this->container->get('session')->set('_security_ezpublish_front', serialize($token)); self::$isPrivate = true;

 

We would need to update this code when there are API changes.

We don't need to call, or even care, about this kind of update with the Guard workflow because it is already integrated with the Ibexa DXP login system. And, as we stressed in the single sign-on blog post, you need to be careful about what you do on kernel requests and on kernel response.

We’ll also note that while reviewing other solutions for this problem, we found suggestions for the use of Symfony Interactive Login event. However, this approach is not useful here because the event is only triggered if the user is successfully authenticated by Ibexa, which does not apply in our use case.

Using Guard to authenticate with Cognito

Without further delay, let's show the main steps needed to implement a custom login handler with Guard.

In our scenario, all Amazon Cognito users will share a single Ibexa DXP user, identified by “common_aws_cognito_user”. Since any change in the user authentication data will be stored in Amazon Cognito, can use a single Ibexa user to control the permissions/entitlements related to all Cognito users. Please note that this is mostly proof of concept code showing how the process works — you can replace the Cognito code to integrate with any external login service.

First, you need to update your security package settings:

# config/packages/security.yaml security:     firewalls:         ezpublish_front:             # define the provider for ezpublish_front, ezpublish_rest_session and form_login.             ezpublish_rest_session:                 provider: ezplatform             form_login:                 provider: ezplatform             provider: 'ezplatform'             # add the guard authenticator configuration             guard:                 authenticators:                     - custom_platform.security.token_authenticator

Define the guard authenticator service:

# bundles/Mugo/CustomBundle/Resources/config/app.yml services:     # Custom Login     custom_platform.security.token_authenticator:         class: Mugo\CustomBundle\Security\TokenAuthenticator         arguments: ['@service_container', '@ezpublish.api.service.user', '@eZ\Publish\API\Repository\PermissionResolver']

Finally, create the code responsible for the guard authentication service:

# bundles/Mugo/CustomBundle/Security/TokenAuthenticator.php

<?php namespace Mugo\CustomBundle\Security; use eZ\Publish\API\Repository\UserService; use eZ\Publish\API\Repository\PermissionResolver; use \Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Contracts\Cache\TagAwareCacheInterface; class TokenAuthenticator extends AbstractGuardAuthenticator {     /** @var Container */     protected $container;     /** @var \eZ\Publish\API\Repository\PermissionResolver */     protected $permissionResolver;     /** @var \eZ\Publish\API\Repository\UserService */     protected $userService;     public function __construct(Container $container, UserService $userService, PermissionResolver $permissionResolver)     {         $this->container    = $container;         $this->permissionResolver = $permissionResolver;         $this->userService = $userService;     }     /**      * Called on every request to decide if this authenticator should be      * used for the request. Returning `false` will cause this authenticator      * to be skipped.      */     public function supports(Request $request): bool     {         return $request->server->get('SCRIPT_URL') == '/login_check';     }     /**      * Called on every request. Return whatever credentials you want to      * be passed to getUser() as $credentials.      */     public function getCredentials(Request $request)     {         // we are using this just for the custom_use_name         return 'custom_use_name';     }     public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface     {         if (null === $credentials) {             // The token header was empty, authentication fails with HTTP Status             // Code 401 "Unauthorized"             return null;         }         // The user identifier in this case is the apiToken, see the key `property`         // of `your_db_provider` in `security.yaml`.         // If this returns a user, checkCredentials() is called next:         return $userProvider->loadUserByUsername($credentials);     }     public function checkCredentials($credentials, UserInterface $user): bool     {         // Check credentials - e.g. make sure the password is valid.         // In case of an API token, no credential check is needed.         $request = $this->container->get('request_stack')->getCurrentRequest();         $username = $request->request->get('_username', '');         $password = $request->request->get('_password', '');         // special condition where we check if the user is going to authenticate after finishing the          if( …... )         {             // Return `true` to cause authentication success             return true;         }         // Return `false` so we continue with the default login handler         return false;     }     public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response     {         $request = $this->container->get('request_stack')->getCurrentRequest();         $redirectPath = $request->request->get('_target_path', '/');         $response = new RedirectResponse($redirectPath);         return $response;     }     public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response     {         return null;     }     /**      * Called when authentication is needed, but it's not sent      */     public function start(Request $request, AuthenticationException $authException = null): Response     {         $data = [             // you might translate this message             'message' => 'Authentication Required'         ];         return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);     }     public function supportsRememberMe(): bool     {         return false;     } }

 

This is the very basic structure required to start your own custom login handler in Ibexa DXP.

Please note, TokenAuthenticator::getCredentials should map the request username action to a username that exists in the Ibexa database. It’s always possible to create, update, and even disable users during the authentication process.

Also, note the onAuthenticationFailure -- we want to return “null”, which means we want the login flow to fall back to the default login handler.

An easy approach to simplifying login for users

The Symfony Guard component is a simple and versatile way to implement an authentication system that checks and maps external credentials against Ibexa DXP user records. Guard avoids many of the maintenance issues that can accompany other approaches to integrating an external login and makes it easy for users to access the features and resources they want.

 

loading form....
Thanks for reaching out. We will get back to you right away.

Comments

blog comments powered by Disqus