📜 ⬆️ ⬇️

Sha512 support in wsse-authentication-bundle from Escape Studios, Symfony2

Recently, there was a task to increase security when creating a token, as well as support sha512. The article turned out to be narrowly focused, but I am sure that I am not confronted with a similar one.

To solve current problems while programming the API of the online store on Symfony2, I decided to make friends with FOSUserBundle and WSSEAuthenticationBundle with the sha512 algorithm and soon found out that this would require a slight improvement. This will be discussed in my article.

Basic settings:


app/config/config.yml fos_user: db_driver: orm firewall_name: wsse_secured user_class: Acme\DemoBundle\Entity\User # Escape WSSE authentication configuration escape_wsse_authentication: authentication_provider_class: Escape\WSSEAuthenticationBundle\Security\Core\Authentication\Provider\Provider authentication_listener_class: Escape\WSSEAuthenticationBundle\Security\Http\Firewall\Listener authentication_entry_point_class: Escape\WSSEAuthenticationBundle\Security\Http\EntryPoint\EntryPoint authentication_encoder_class: Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder app/config/security.yml security: providers: fos_userbundle: id: fos_user.user_provider.username encoders: FOS\UserBundle\Model\UserInterface: sha512 firewalls: wsse_secured: pattern: ^/api/.* wsse: lifetime: 300 #lifetime of nonce realm: "Secured API" #identifies the set of resources to which the authentication information will apply (WWW-Authenticate) profile: "UsernameToken" #WSSE profile (WWW-Authenticate) encoder: #digest algorithm algorithm: sha512 encodeHashAsBase64: true iterations: 1 anonymous: true 


Token generation code in the controller:


 src\Acme\DemoBundle\Controller\SecurityController.php //... $created = date('c'); $nonce = substr(md5(uniqid('nonce_', true)), 0, 16); $nonceHigh = base64_encode($nonce); $salted = $nonce . $created . $user->getPassword() . "{" . $user->getSalt() . "}"; $passwordDigest = hash('sha512', $salted, true); $header = "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\""; $view->setHeader("Authorization", 'WSSE profile="UsernameToken"'); $view->setHeader("X-WSSE", "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\""); $data = array('WSSE' => $header); //... 

I really wanted this configuration to work out of the box, but it didn’t happen. We will understand why. It turned out that in the standard provider from Escapestudios there are such lines:
 WSSEAuthenticationBundle/Security/Core/Authentication/Provider/Provider.php //... //validate secret $expected = $this->encoder->encodePassword( sprintf( '%s%s%s', base64_decode($nonce), $created, $secret ), "" ); 

Interest is attracted by quotes in the penultimate line, if salt is added instead of them, then everything miraculously begins to work. Let's rewrite this provider in our bundle and correct the situation:
')
 src\Acme\DemoBundle\Security\Authentication\Provider\WsseProvider.php namespace Acme\DemoBundle\Security\Authentication\Provider; use Escape\WSSEAuthenticationBundle\Security\Core\Authentication\Provider\Provider; use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; use Symfony\Component\Security\Core\Exception\CredentialsExpiredException; use Symfony\Component\Security\Core\Exception\NonceExpiredException; /** * Class WsseProvider * @package Acme\DemoBundle\Security\Authentication\Provider */ class WsseProvider extends Provider implements AuthenticationProviderInterface { /** * @param $user \Symfony\Component\Security\Core\User\UserInterface * @param $digest * @param $nonce * @param $created * @param $secret * * @return bool * @throws \Symfony\Component\Security\Core\Exception\CredentialsExpiredException * @throws \Symfony\Component\Security\Core\Exception\NonceExpiredException */ protected function validateDigest($user, $digest, $nonce, $created, $secret) { //check whether timestamp is not in the future if (strtotime($created) > time()) { throw new CredentialsExpiredException('Future token detected.'); } //expire timestamp after specified lifetime if (time() - strtotime($created) > $this->getLifetime()) { throw new CredentialsExpiredException('Token has expired.'); } //validate that nonce is unique within specified lifetime //if it is not, this could be a replay attack if ($this->getNonceCache()->contains($nonce)) { throw new NonceExpiredException('Previously used nonce detected.'); } $this->getNonceCache()->save($nonce, time(), $this->getLifetime()); //validate secret $expected = $this->getEncoder()->encodePassword( sprintf( '%s%s%s', base64_decode($nonce), $created, $secret ), $user->getSalt() ); return $digest === $expected; } } 

I want to note that in the latest, at the time of writing, version of the bundle, it is not possible to disable the use of nonces in the configuration, and the resulting token was validated only once. To change this, check and add nonce lines can be simply deleted.

Add this class to the settings:
 app/config/config.yml # Escape WSSE authentication configuration escape_wsse_authentication: authentication_provider_class: Acme\DemoBundle\Security\Authentication\Provider\WsseProvider authentication_listener_class: Escape\WSSEAuthenticationBundle\Security\Http\Firewall\Listener authentication_entry_point_class: Escape\WSSEAuthenticationBundle\Security\Http\EntryPoint\EntryPoint authentication_encoder_class: Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder 

Now let's improve the protection a little bit. In the encoder settings there is such an iterations parameter:
 app/config/security.yml security: firewalls: wsse_secured: wsse: encoder: #digest algorithm iterations: 1 

This parameter is responsible for the number of hash iterations when encoding / decoding a token. By default it is equal to "1". For comparison, when password hashing in Symfony2 it is “5000” (Symfony \ Component \ Security \ Core \ Encoder \ MessageDigestPasswordEncoder).

To implement this functionality, we will make some changes to the controller and configuration:
 app/config/security.yml parameters: wsse_iterations: 300 security: firewalls: wsse_secured: wsse: encoder: #digest algorithm iterations: %wsse_iterations% src\Acme\DemoBundle\Controller\SecurityController.php //... $created = date('c'); $nonce = substr(md5(uniqid('nonce_', true)), 0, 16); $nonceHigh = base64_encode($nonce); $container = $this->get('service_container'); $iterations = $container->getParameter('wsse_iterations'); $salted = $nonce . $created . $user->getPassword() . "{" . $user->getSalt() . "}"; $passwordDigest = hash('sha512', $salted, true); for ($i = 1; $i < $iterations; $i++) { $passwordDigest = hash('sha512', $passwordDigest . $salted, true); } $passwordDigest = base64_encode($passwordDigest); $header = "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\""; $view->setHeader("Authorization", 'WSSE profile="UsernameToken"'); $view->setHeader( "X-WSSE", "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"" ); $data = array('WSSE' => $header); //... 

In fact, the main points in this article come down to replacing one line in the provider, however, some additions and their description are also, in my opinion, quite appropriate. I hope someone will come in handy.

I invite to courses on web development from business school Digitov, which I lead: I want to become Junior PHP Developer! (for beginners), Symfony 2. Agile development (for specialists), as well as my colleagues: Python / Django development (for beginners) and Ruby on Rails. On rails to professional development (for beginners). Subscribe to courses now and be able to buy them at a discount.

Author : Sergey Kharlanchuk, Senior PHP Developer / Team Lead, SECL GROUP / Internet Sales Technologies

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


All Articles