Поддержка sha512 в wsse-authentication-bundle от Escape Studios, Symfony2

  • Tutorial
Недавно встала задача повышения безопасности при создании токена, а также поддержки sha512. Статья получилась узконаправленная, но я уверен, что сталкиваюсь с подобным не только я.

Для решения текущих задач при программировании API интернет-магазина на Symfony2 решил подружить FOSUserBundle и WSSEAuthenticationBundle c алгоритмом sha512 и вскоре выяснил, что для этого потребуется небольшая доработка. Об этом и пойдет речь в моей статье.

Базовые настройки:


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


Код генерации токена в контроллере:


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);
//...

Очень хотелось, чтобы такая конфигурация заработала из коробки, но так не случилось. Разберемся почему. Выяснилось, что в стандартном провайдере от Escapestudios есть такие строки:
WSSEAuthenticationBundle/Security/Core/Authentication/Provider/Provider.php
//...
        //validate secret
        $expected = $this->encoder->encodePassword(
           sprintf(
             '%s%s%s',
               base64_decode($nonce),
               $created,
               $secret
            ),
           ""
        );

Интерес привлекают кавычки в предпоследней строке, если вместо них добавить соль, то все чудесным образом начинает работать. Давайте перепишем этот провайдер в своем бандле и подправим ситуацию:

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;
    }
}

Хочу заметить, что в последней, на момент написания статьи, версии бандла отключить использование nonces в конфигурации не представляется возможным, и полученный токен валиден только один раз. Чтобы это изменить строки проверки и добавления nonce можно просто удалить.

Добавим этот класс в настройки:
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

Теперь давайте немножко улучшим защиту. В настройках энкодера есть такой параметр iterations:
app/config/security.yml
security:
    firewalls:
        wsse_secured:
            wsse:
                encoder: #digest algorithm
                    iterations: 1

Этот параметр отвечает за количество итераций хэширования при кодировании/декодировании токена. По умолчанию он равен «1». Для сравнения, при хэшировании пароля в Symfony2 он составляет «5000» (Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder).

Для реализации подобного функционала внесем некоторые изменения в контроллер и конфигурацию:
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);
//...

Фактически, основные моменты в этой статье сводятся к замене одной строки в провайдере, однако, некоторые дополнения и их описание тоже вполне, на мой взгляд, к месту. Надеюсь кому-то пригодится.

Приглашаю на курсы по веб-разработке от бизнес-школы Digitov, которые веду Я: Хочу стать Junior PHP Developer! (для новичков), Symfony 2. Гибкая разработка (для специалистов), а также, которые ведут мои коллеги: Разработка веб-приложений на Python / Django (для новичков) и Ruby on Rails. По рельсам к профессиональной разработке (для новичков). Подписывайтесь на курсы сейчас и сможете купить их со скидкой

Автор: Сергей Харланчук, Senior PHP Developer / Team Lead, компания «SECL GROUP» / «Internet Sales Technologies»

SECL Group

46,00

Делаем стартапы успешными!

Поделиться публикацией
Комментарии 7
    0
    Отформатируйте, пожалуйста, код. В таком виде его очень сложно воспринимать.
      0
      Поправил. Так лучше?
        0
        Еще бы отступы и было бы вообще шикарно :)
          +1
          <source lang="php"></source> вроди решает все
            0
            Да, забыл про него. Вот, готово.
        +1
        спасибо
          0
          Всегда пожалуйста!

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое