Для решения текущих задач при программировании API интернет-магазина на Symfony2 решил подружить FOSUserBundle и WSSEAuthenticationBundle c алгоритмом sha512 и вскоре выяснил, что для этого потребуется небольшая доработка. Об этом и пойдет речь в моей статье.
Базовые настройки:
app/config/config.ymlfos_user:db_driver: ormfirewall_name: wsse_secureduser_class: Acme\DemoBundle\Entity\User# Escape WSSE authentication configurationescape_wsse_authentication:authentication_provider_class: Escape\WSSEAuthenticationBundle\Security\Core\Authentication\Provider\Providerauthentication_listener_class: Escape\WSSEAuthenticationBundle\Security\Http\Firewall\Listenerauthentication_entry_point_class: Escape\WSSEAuthenticationBundle\Security\Http\EntryPoint\EntryPointauthentication_encoder_class: Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder
app/config/security.ymlsecurity:providers:fos_userbundle:id: fos_user.user_provider.usernameencoders:FOS\UserBundle\Model\UserInterface: sha512
firewalls:wsse_secured:pattern: ^/api/.*wsse:lifetime: 300 #lifetime of noncerealm: "Secured API" #identifies the set of resources to which the authentication information will apply (WWW-Authenticate)profile: "UsernameToken" #WSSE profile (WWW-Authenticate)encoder: #digest algorithmalgorithm: sha512encodeHashAsBase64: trueiterations: 1anonymous: 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 futureif (strtotime($created) > time()) {throw new CredentialsExpiredException('Future token detected.');}
//expire timestamp after specified lifetimeif (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 attackif ($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 configurationescape_wsse_authentication:authentication_provider_class: Acme\DemoBundle\Security\Authentication\Provider\WsseProviderauthentication_listener_class: Escape\WSSEAuthenticationBundle\Security\Http\Firewall\Listenerauthentication_entry_point_class: Escape\WSSEAuthenticationBundle\Security\Http\EntryPoint\EntryPointauthentication_encoder_class: Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder
Теперь давайте немножко улучшим защиту.
В настройках энкодера есть такой параметр iterations:
app/config/security.ymlsecurity:firewalls:wsse_secured:wsse:encoder: #digest algorithmiterations: 1
Этот параметр отвечает за количество итераций хэширования при кодировании/декодировании токена. По умолчанию он равен «1». Для сравнения, при хэшировании пароля в Symfony2 он составляет «5000» (Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder). Для реализации подобного функционала внесем некоторые изменения в контроллер и конфигурацию:
app/config/security.ymlparameters:wsse_iterations: 300security:firewalls:wsse_secured:wsse:encoder: #digest algorithmiterations: %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);//...
Фактически, основные моменты в этой статье сводятся к замене одной строки в провайдере, однако, некоторые дополнения и их описание тоже вполне, на мой взгляд, к месту. Надеюсь кому-то пригодится.
This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий