Недавно встала задача повышения безопасности при создании токена, а также поддержки sha512. Статья получилась узконаправленная, но я уверен, что сталкиваюсь с подобным не только я.
Для решения текущих задач при программировании API интернет-магазина на Symfony2 решил подружить FOSUserBundle и WSSEAuthenticationBundle c алгоритмом sha512 и вскоре выяснил, что для этого потребуется небольшая доработка. Об этом и пойдет речь в моей статье.
Очень хотелось, чтобы такая конфигурация заработала из коробки, но так не случилось. Разберемся почему. Выяснилось, что в стандартном провайдере от Escapestudios есть такие строки:
Интерес привлекают кавычки в предпоследней строке, если вместо них добавить соль, то все чудесным образом начинает работать. Давайте перепишем этот провайдер в своем бандле и подправим ситуацию:
Хочу заметить, что в последней, на момент написания статьи, версии бандла отключить использование nonces в конфигурации не представляется возможным, и полученный токен валиден только один раз. Чтобы это изменить строки проверки и добавления nonce можно просто удалить.
Добавим этот класс в настройки:
Теперь давайте немножко улучшим защиту. В настройках энкодера есть такой параметр iterations:
Этот параметр отвечает за количество итераций хэширования при кодировании/декодировании токена. По умолчанию он равен «1». Для сравнения, при хэшировании пароля в Symfony2 он составляет «5000» (Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder).
Для реализации подобного функционала внесем некоторые изменения в контроллер и конфигурацию:
Фактически, основные моменты в этой статье сводятся к замене одной строки в провайдере, однако, некоторые дополнения и их описание тоже вполне, на мой взгляд, к месту. Надеюсь кому-то пригодится.
Приглашаю на курсы по веб-разработке от бизнес-школы Digitov, которые веду Я: Хочу стать Junior PHP Developer! (для новичков), Symfony 2. Гибкая разработка (для специалистов), а также, которые ведут мои коллеги: Разработка веб-приложений на Python / Django (для новичков) и Ruby on Rails. По рельсам к профессиональной разработке (для новичков). Подписывайтесь на курсы сейчас и сможете купить их со скидкой
Автор: Сергей Харланчук, Senior PHP Developer / Team Lead, компания «SECL GROUP» / «Internet Sales Technologies»
Для решения текущих задач при программировании 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»