📜 ⬆️ ⬇️

Integration of Symfony2 authentication and Jira tracker

Hello, Habrosoobschestvu. In this article I want to tell you how you can make friends with the well-known Symfony2 framework and the equally well-known Jira tracker.

Why bind Jira and Symfony2?


In the company where I work, it became necessary to link the support system and the task tracker through the API so that applications from customers can be easily converted into tickets. The primary problem that stood in our way was the integration of Jira authentication (using the “Basic Authentication” mechanism) and the Symfony2 security system. To understand the framework authentication and authorization mechanisms, you should read the official documentation: http://symfony.com/doc/current/book/security.html .


What do you need to create a new type of authorization in Symfony2?


  1. Token, which will store user-entered information during authentication.
  2. Listener, required to verify user authorization.
  3. Provider directly implementing authentication through Jira.
  4. User Provider, which will be requested by Symfony2 Security to retrieve user information
  5. Factory, which will register a new authentication and authorization method.

')

Create Token


Symfony uses tokens that are inherited from the AbstractToken class to save information entered during user authentication and its subsequent use. In the task in question, it is necessary to store 2 fields - this is the username and password of the user, on the basis of which the authorization check in Jira will be performed. The implementation code for the token class is shown below.

<?php namespace DG\JiraAuthBundle\Security\Authentication\Token; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; class JiraToken extends AbstractToken { protected $jira_login; protected $jira_password; public function __construct(array $roles = array('ROLE_USER')){ parent::__construct($roles); $this->setAuthenticated(count($roles) > 0); } public function getJiraLogin(){ return $this->jira_login; } public function setJiraLogin($jira_login){ $this->jira_login = $jira_login; } public function getJiraPassword(){ return $this->jira_password; } public function setJiraPassword($jira_password){ $this->jira_password = $jira_password; } public function serialize() { return serialize(array($this->jira_login, $this->jira_password, parent::serialize())); } public function unserialize($serialized) { list($this->jira_login, $this->jira_password, $parent_data) = unserialize($serialized); parent::unserialize($parent_data); } public function getCredentials(){ return ''; } } 


Listener implementation


Now that we have user data stored, it should be possible to check for correctness. If the data is outdated, you need to inform the framework about this. To do this, you must implement a Listener inherited from AbstractAuthenticationListener.

 <?php namespace DG\JiraAuthBundle\Security\Firewall; use DG\JiraAuthBundle\Security\Authentication\Token\JiraToken; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener; class JiraListener extends AbstractAuthenticationListener { protected function attemptAuthentication(Request $request){ if ($this->options['post_only'] && 'post' !== strtolower($request->getMethod())) { if (null !== $this->logger) { $this->logger->debug(sprintf('Authentication method not supported: %s.', $request->getMethod())); } return null; } $username = trim($request->get($this->options['username_parameter'], null, true)); $password = $request->get($this->options['password_parameter'], null, true); $request->getSession()->set(SecurityContextInterface::LAST_USERNAME, $username); $request->getSession()->set('jira_auth', base64_encode($username.':'.$password)); $token = new JiraToken(); $token->setJiraLogin($username); $token->setJiraPassword($password); return $this->authenticationManager->authenticate($token); } } 


Log in to Jira. Provider


The time has come for the most important thing - to send data directly to Jira. To work with the rest api tracker, a simple class is written that connects as a service. The Buzz library is used to work with the Jira API.

 <?php namespace DG\JiraAuthBundle\Jira; use Buzz\Message; use Buzz\Client\Curl; class JiraRest { private $jiraUrl = ''; public function __construct($jiraUrl){ $this->jiraUrl = $jiraUrl; } public function getUserInfo($username, $password){ $request = new Message\Request( 'GET', '/rest/api/2/user?username=' . $username, $this->jiraUrl ); $request->addHeader('Authorization: Basic ' . base64_encode($username . ':' . $password) ); $request->addHeader('Content-Type: application/json'); $response = new Message\Response(); $client = new Curl(); $client->setTimeout(10); $client->send($request, $response); return $response; } } 


The provider must implement the AuthenticationProviderInterface interface and look like this:

 <?php namespace DG\JiraAuthBundle\Security\Authentication\Provider; use DG\JiraAuthBundle\Entity\User; use DG\JiraAuthBundle\Jira\JiraRest; use DG\JiraAuthBundle\Security\Authentication\Token\JiraToken; use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserProviderInterface; class JiraProvider implements AuthenticationProviderInterface { private $userProvider; private $jiraRest; public function __construct(UserProviderInterface $userProvider, $providerKey, JiraRest $jiraRest) { $this->userProvider = $userProvider; $this->jiraRest = $jiraRest; } public function supports(TokenInterface $token) { return $token instanceof JiraToken; } public function authenticate(TokenInterface $token) { $user = $this->checkUserAuthentication($token); $token->setUser($user); return $token; } public function checkUserAuthentication(JiraToken $token){ $response = $this->jiraRest->getUserInfo($token->getJiraLogin(), $token->getJiraPassword()); if(!in_array('HTTP/1.1 200 OK', $response->getHeaders())){ throw new AuthenticationException( 'Incorrect email and/or password' ); } $userInfo = json_decode($response->getContent()); $user = new User(); $user->setUsername($userInfo->name); $user->setBase64Hash(base64_encode($token->getJiraLogin() . ':' . $token->getJiraPassword())); $user->setEmail($userInfo->emailAddress); $user->addRole('ROLE_USER'); return $user; } } 


As you can see from the implementation, user data is stored in the User entity. You can not do this, so that Doctrine does not create an extra table in the database, but in the future you can add information about users from Jira to this table in order to protect yourself against the temporary unavailability of the tracker. Such “insurance” is beyond the scope of the article, but can be very useful.

Providing information about the authorized user


Security in the framework requests user information to verify authorization. It is clear that such information is in Jira, so we should receive it from the tracker. You can, of course, cache the answers from Jira, but for now we will not take this into account. The provider code is shown below.

 <?php namespace DG\JiraAuthBundle\User; use DG\JiraAuthBundle\Entity\User; use DG\JiraAuthBundle\Jira\JiraRest; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\SecurityContextInterface; class JiraUserProvider implements UserProviderInterface { private $jiraRest; public function __construct(JiraRest $jiraRest){ $this->jiraRest = $jiraRest; } public function loadUserByUsername($username) { } public function refreshUser(UserInterface $user) { if (!$user instanceof User) { throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); } $decodedUserData = base64_decode($user->getBase64Hash()); list($username, $password) = explode(':', $decodedUserData); $userInfoResponse = $this->jiraRest->getUserInfo($username, $password); $userInfo = json_decode($userInfoResponse->getContent()); $user = new User(); $user->setUsername($user->getUsername()); $user->setEmail($userInfo->emailAddress); $user->setBase64Hash($user->getBase64Hash()); $user->addRole('ROLE_USER'); return $user; } public function supportsClass($class) { return $class === 'DG\JiraAuthBundle\Entity\User'; } } 


Filling configuration


To use the created classes, you must register them in the configuration as services. An example of services.yml is shown below. I note that the jira_url parameter must be defined in parameters.yml and contain the url address before Jira.
 parameters: dg_jira_auth.user_provider.class: DG\JiraAuthBundle\User\JiraUserProvider dg_jira_auth.listener.class: DG\JiraAuthBundle\Security\Firewall\JiraListener dg_jira_auth.provider.class: DG\JiraAuthBundle\Security\Authentication\Provider\JiraProvider dg_jira_auth.handler.class: DG\JiraAuthBundle\Security\Authentication\Handler\JiraAuthenticationHandler dg_jira.rest.class: DG\JiraAuthBundle\Jira\JiraRest services: dg_jira.rest: class: %dg_jira.rest.class% arguments: - '%jira_url%' dg_jira_auth.user_provider: class: %dg_jira_auth.user_provider.class% arguments: - @dg_jira.rest dg_jira_auth.authentication_success_handler: class: %dg_jira_auth.handler.class% dg_jira_auth.authentication_failure_handler: class: %dg_jira_auth.handler.class% dg_jira_auth.authentication_provider: class: %dg_jira_auth.provider.class% arguments: [@dg_jira_auth.user_provider, '', @dg_jira.rest] dg_jira_auth.authentication_listener: class: %dg_jira_auth.listener.class% arguments: - @security.context - @security.authentication.manager - @security.authentication.session_strategy - @security.http_utils - '' - @dg_jira_auth.authentication_success_handler - @dg_jira_auth.authentication_failure_handler - '' - @logger - @event_dispatcher 


Registering a new authentication and authorization method in symfony


For all of the above to work, you need to describe the authentication behavior in the form of a factory and register it in a bundle.

 <?php namespace DG\JiraAuthBundle\DependencyInjection\Security\Factory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\DependencyInjection\Reference; class JiraFactory extends AbstractFactory { public function __construct(){ $this->addOption('username_parameter', '_username'); $this->addOption('password_parameter', '_password'); $this->addOption('intention', 'authenticate'); $this->addOption('post_only', true); } protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId) { $provider = 'dg_jira_auth.authentication_provider.'.$id; $container ->setDefinition($provider, new DefinitionDecorator('dg_jira_auth.authentication_provider')) ->replaceArgument(1, $id) ; return $provider; } protected function getListenerId() { return 'dg_jira_auth.authentication_listener'; } public function getPosition() { return 'form'; } public function getKey() { return 'jira-form'; } protected function createListener($container, $id, $config, $userProvider) { $listenerId = parent::createListener($container, $id, $config, $userProvider); if (isset($config['csrf_provider'])) { $container ->getDefinition($listenerId) ->addArgument(new Reference($config['csrf_provider'])) ; } return $listenerId; } protected function createEntryPoint($container, $id, $config, $defaultEntryPoint) { $entryPointId = 'security.authentication.form_entry_point.'.$id; $container ->setDefinition($entryPointId, new DefinitionDecorator('security.authentication.form_entry_point')) ->addArgument(new Reference('security.http_utils')) ->addArgument($config['login_path']) ->addArgument($config['use_forward']) ; return $entryPointId; } } 


To register in a bundle, you need to add a line to the build method of the bundle class

 $extension->addSecurityListenerFactory(new JiraFactory()); 


Final implementation


Everything, now we are ready to test work with Jira. Add the created JiraUserProvider to security.yml in the providers section as strings

  jira_auth_provider: id: dg_jira_auth.user_provider 


Next, you need to add a new section to the firewalls, assuming that all pages whose addresses begin with / jira / are closed from unauthorized users by default:

  jira_secured: provider: jira_auth_provider switch_user: false context: user pattern: /jira/.* jira_form: check_path: dg_jira_auth_check_path login_path: dg_jira_auth_login_path default_target_path: dg_jira_auth_private logout: path: dg_jira_auth_logout target: dg_jira_auth_public anonymous: true 


The final touch is adding lines to the access_controls section, defining the user roles needed to view the pages. An approximate view of the lines may be

 - { path: ^/jira/public, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/jira/private/login$, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/jira/private(.*)$, role: ROLE_USER } 


PS


All the code given in the article can be installed as a bundle from the “dg / jira-auth-bundle” package in the composer or with github . To work the bundle, you need to register it in AppKernel.php and add a section

 _jira_auth: resource: "@DGJiraAuthBundle/Resources/config/routing.yml" prefix: /jira/ 


in routing.yml. After that, you can go to the / jira / public page and test authorization via Jira.

To secure the material


In Symfony Cookbook, there is also an instruction on how to implement authentication through a third-party web service.

I hope the article will be useful to you!

Source: https://habr.com/ru/post/206888/


All Articles