Add custom auth encoder & provider

These custom classes allow Wallabag v2 to be compatible with Wallabag v1 salted password
This commit is contained in:
Jeremy 2015-02-08 21:47:36 +01:00
parent 7812f508bc
commit d91691573f
5 changed files with 215 additions and 33 deletions

View file

@ -1,6 +1,6 @@
security:
encoders:
Wallabag\CoreBundle\Entity\Users:
Wallabag\CoreBundle\Entity\User:
algorithm: sha1
encode_as_base64: false
iterations: 1
@ -11,7 +11,7 @@ security:
providers:
administrators:
entity: { class: WallabagCoreBundle:Users, property: username }
entity: { class: WallabagCoreBundle:User, property: username }
# the main part of the security, where you can set up firewalls
# for specific sections of your app
@ -23,35 +23,35 @@ security:
pattern: ^/login$
anonymous: ~
# secured_area:
# pattern: ^/
# anonymous: ~
# form_login:
# login_path: /login
#
# use_forward: false
#
# check_path: /login_check
#
# post_only: true
#
# always_use_default_target_path: true
# default_target_path: /
# target_path_parameter: redirect_url
# use_referer: true
#
# failure_path: null
# failure_forward: false
#
# username_parameter: _username
# password_parameter: _password
#
# csrf_parameter: _csrf_token
# intention: authenticate
#
# logout:
# path: /logout
# target: /
secured_area:
pattern: ^/
anonymous: ~
form_login:
login_path: /login
use_forward: false
check_path: /login_check
post_only: true
always_use_default_target_path: true
default_target_path: /
target_path_parameter: redirect_url
use_referer: true
failure_path: null
failure_forward: false
username_parameter: _username
password_parameter: _password
csrf_parameter: _csrf_token
intention: authenticate
logout:
path: /logout
target: /
access_control:
- { path: ^/api/doc, roles: IS_AUTHENTICATED_ANONYMOUSLY }

View file

@ -1,7 +1,8 @@
# Learn more about services, parameters and containers at
# http://symfony.com/doc/current/book/service_container.html
parameters:
# parameter_name: value
security.authentication.provider.dao.class: Wallabag\CoreBundle\Security\Authentication\Provider\WallabagAuthenticationProvider
security.encoder.digest.class: Wallabag\CoreBundle\Security\Authentication\Encoder\WallabagPasswordEncoder
services:
# service_name:

View file

@ -161,7 +161,11 @@ class User implements AdvancedUserInterface, \Serializable
*/
public function setPassword($password)
{
$this->password = $password;
if (!$password && 0 === strlen($password)) {
return;
}
$this->password = sha1($password.$this->getUsername().$this->getSalt());
return $this;
}

View file

@ -0,0 +1,88 @@
<?php
namespace Wallabag\CoreBundle\Security\Authentication\Encoder;
use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
/**
* This override just add en extra variable (username) to be able to salt the password
* the way Wallabag v1 does. It will avoid to break compatibility with Wallabag v1
*
*/
class WallabagPasswordEncoder extends BasePasswordEncoder
{
private $algorithm;
private $encodeHashAsBase64;
private $iterations;
private $username = null;
/**
* Constructor.
*
* @param string $algorithm The digest algorithm to use
* @param bool $encodeHashAsBase64 Whether to base64 encode the password hash
* @param int $iterations The number of iterations to use to stretch the password hash
*/
public function __construct($algorithm = 'sha512', $encodeHashAsBase64 = true, $iterations = 5000)
{
$this->algorithm = $algorithm;
$this->encodeHashAsBase64 = $encodeHashAsBase64;
$this->iterations = $iterations;
}
public function setUsername($username)
{
$this->username = $username;
}
/**
* {@inheritdoc}
*/
public function encodePassword($raw, $salt)
{
if (null === $this->username) {
throw new \LogicException('We can not check the password without a username.');
}
if ($this->isPasswordTooLong($raw)) {
throw new BadCredentialsException('Invalid password.');
}
if (!in_array($this->algorithm, hash_algos(), true)) {
throw new \LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm));
}
$salted = $this->mergePasswordAndSalt($raw, $salt);
$digest = hash($this->algorithm, $salted, true);
// "stretch" hash
for ($i = 1; $i < $this->iterations; $i++) {
$digest = hash($this->algorithm, $digest.$salted, true);
}
return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest);
}
/**
* {@inheritdoc}
*
* We inject the username inside the salted password
*/
protected function mergePasswordAndSalt($password, $salt)
{
if (empty($salt)) {
return $password;
}
return $password.$this->username.$salt;
}
/**
* {@inheritdoc}
*/
public function isPasswordValid($encoded, $raw, $salt)
{
return !$this->isPasswordTooLong($raw) && $this->comparePasswords($encoded, $this->encodePassword($raw, $salt));
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Wallabag\CoreBundle\Security\Authentication\Provider;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider;
class WallabagAuthenticationProvider extends UserAuthenticationProvider
{
private $encoderFactory;
private $userProvider;
/**
* Constructor.
*
* @param UserProviderInterface $userProvider An UserProviderInterface instance
* @param UserCheckerInterface $userChecker An UserCheckerInterface instance
* @param string $providerKey The provider key
* @param EncoderFactoryInterface $encoderFactory An EncoderFactoryInterface instance
* @param bool $hideUserNotFoundExceptions Whether to hide user not found exception or not
*/
public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, $providerKey, EncoderFactoryInterface $encoderFactory, $hideUserNotFoundExceptions = true)
{
parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions);
$this->encoderFactory = $encoderFactory;
$this->userProvider = $userProvider;
}
/**
* {@inheritdoc}
*/
protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
{
$currentUser = $token->getUser();
if ($currentUser instanceof UserInterface) {
if ($currentUser->getPassword() !== $user->getPassword()) {
throw new BadCredentialsException('The credentials were changed from another session.');
}
} else {
if ("" === ($presentedPassword = $token->getCredentials())) {
throw new BadCredentialsException('The presented password cannot be empty.');
}
// give username, it's used to hash the password
$encoder = $this->encoderFactory->getEncoder($user);
$encoder->setUsername($user->getUsername());
if (!$encoder->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) {
throw new BadCredentialsException('The presented password is invalid.');
}
}
}
/**
* {@inheritdoc}
*/
protected function retrieveUser($username, UsernamePasswordToken $token)
{
$user = $token->getUser();
if ($user instanceof UserInterface) {
return $user;
}
try {
$user = $this->userProvider->loadUserByUsername($username);
if (!$user instanceof UserInterface) {
throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
}
return $user;
} catch (UsernameNotFoundException $notFound) {
$notFound->setUsername($username);
throw $notFound;
} catch (\Exception $repositoryProblem) {
$ex = new AuthenticationServiceException($repositoryProblem->getMessage(), 0, $repositoryProblem);
$ex->setToken($token);
throw $ex;
}
}
}