summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFabien Potencier <fabien.potencier@gmail.com>2013-02-07 17:43:41 +0100
committerFabien Potencier <fabien.potencier@gmail.com>2013-02-07 17:43:41 +0100
commit4b78c06d5461d28884ca4181383bfa7b960426b6 (patch)
tree42c5d8918cf0014123fff8349b1c4da4ca1f25bb
parent49f2eb420b3835d1032dbad4c52e1caa70930cf5 (diff)
parentb570b85dcdea40e070678c08af85d94083111fd4 (diff)
downloadsymfony-security-4b78c06d5461d28884ca4181383bfa7b960426b6.zip
symfony-security-4b78c06d5461d28884ca4181383bfa7b960426b6.tar.gz
symfony-security-4b78c06d5461d28884ca4181383bfa7b960426b6.tar.bz2
Merge branch '2.2'
* 2.2: [HttpFoundation] fixed Request::create() method [HttpKernel] fixed the creation of the Profiler directory [HttpKernel] fixed the hinclude fragment renderer when the template is empty bumped Symfony version to 2.2.0-RC2-DEV [DependencyInjection] enhanced some error messages [FrameworkBundle] fixed typo fixed typo tweaked previous merge [Security] fixed interface implementation (closes #6974) Add "'property_path' => false" deprecation message for forms fixed CS Added BCrypt password encoder. updated VERSION for 2.2.0-RC1 Removed underscores from test method names to be consistent with other components. [Security] fixed session creation when none is needed (closes #6917) [FrameworkBundle] removed obsolete comment (see 2e356c1) Micro-optimization [FrameworkBundle] removed extra whitespaces [Security] renamed Constraint namespace to Constraints for validator classes in order to be consistent with the whole current validator API. [FrameworkBundle] fixed wrong indentation on route debug output
-rw-r--r--CHANGELOG.md1
-rw-r--r--Core/Encoder/BCryptPasswordEncoder.php148
-rw-r--r--Core/Validator/Constraint/UserPassword.php15
-rw-r--r--Core/Validator/Constraint/UserPasswordValidator.php31
-rw-r--r--Core/Validator/Constraints/UserPassword.php28
-rw-r--r--Core/Validator/Constraints/UserPasswordValidator.php46
-rw-r--r--Http/Firewall/ContextListener.php6
-rw-r--r--Tests/Core/Encoder/BCryptPasswordEncoderTest.php112
-rw-r--r--Tests/Core/Validator/Constraints/UserPasswordValidatorTest.php (renamed from Tests/Core/Validator/Constraint/UserPasswordValidatorTest.php)6
-rw-r--r--Tests/Http/Firewall/ContextListenerTest.php58
10 files changed, 387 insertions, 64 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83914b1..82c4312 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ CHANGELOG
implements EventSubscriberInterface
* added secure random number generator
* added PBKDF2 Password encoder
+ * added BCrypt password encoder
2.1.0
-----
diff --git a/Core/Encoder/BCryptPasswordEncoder.php b/Core/Encoder/BCryptPasswordEncoder.php
new file mode 100644
index 0000000..1b7572d
--- /dev/null
+++ b/Core/Encoder/BCryptPasswordEncoder.php
@@ -0,0 +1,148 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\Encoder;
+
+use Symfony\Component\Security\Core\Encoder\BasePasswordEncoder;
+use Symfony\Component\Security\Core\Util\SecureRandomInterface;
+
+/**
+ * @author Elnur Abdurrakhimov <elnur@elnur.pro>
+ * @author Terje BrĂ¥ten <terje@braten.be>
+ */
+class BCryptPasswordEncoder extends BasePasswordEncoder
+{
+ /**
+ * @var SecureRandomInterface
+ */
+ private $secureRandom;
+
+ /**
+ * @var string
+ */
+ private $cost;
+
+ private static $prefix = null;
+
+ /**
+ * Constructor.
+ *
+ * @param SecureRandomInterface $secureRandom A SecureRandomInterface instance
+ * @param integer $cost The algorithmic cost that should be used
+ *
+ * @throws \InvalidArgumentException if cost is out of range
+ */
+ public function __construct(SecureRandomInterface $secureRandom, $cost)
+ {
+ $this->secureRandom = $secureRandom;
+
+ $cost = (int) $cost;
+ if ($cost < 4 || $cost > 31) {
+ throw new \InvalidArgumentException('Cost must be in the range of 4-31.');
+ }
+ $this->cost = sprintf('%02d', $cost);
+
+ if (!self::$prefix) {
+ self::$prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=') ? '2y' : '2a').'$';
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function encodePassword($raw, $salt)
+ {
+ if (function_exists('password_hash')) {
+ return password_hash($raw, PASSWORD_BCRYPT, array('cost' => $this->cost));
+ }
+
+ $salt = self::$prefix.$this->cost.'$'.$this->encodeSalt($this->getRawSalt());
+ $encoded = crypt($raw, $salt);
+ if (!is_string($encoded) || strlen($encoded) <= 13) {
+ return false;
+ }
+
+ return $encoded;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isPasswordValid($encoded, $raw, $salt)
+ {
+ if (function_exists('password_verify')) {
+ return password_verify($raw, $encoded);
+ }
+
+ $crypted = crypt($raw, $encoded);
+ if (strlen($crypted) <= 13) {
+ return false;
+ }
+
+ return $this->comparePasswords($encoded, $crypted);
+ }
+
+ /**
+ * Encodes the salt to be used by Bcrypt.
+ *
+ * The blowfish/bcrypt algorithm used by PHP crypt expects a different
+ * set and order of characters than the usual base64_encode function.
+ * Regular b64: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
+ * Bcrypt b64: ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
+ * We care because the last character in our encoded string will
+ * only represent 2 bits. While two known implementations of
+ * bcrypt will happily accept and correct a salt string which
+ * has the 4 unused bits set to non-zero, we do not want to take
+ * chances and we also do not want to waste an additional byte
+ * of entropy.
+ *
+ * @param bytes $random a string of 16 random bytes
+ *
+ * @return string Properly encoded salt to use with php crypt function
+ *
+ * @throws \InvalidArgumentException if string of random bytes is too short
+ */
+ protected function encodeSalt($random)
+ {
+ $len = strlen($random);
+ if ($len < 16) {
+ throw new \InvalidArgumentException('The bcrypt salt needs 16 random bytes.');
+ }
+ if ($len > 16) {
+ $random = substr($random, 0, 16);
+ }
+
+ $base64raw = str_replace('+', '.', base64_encode($random));
+ $salt128bit = substr($base64raw, 0, 21);
+ $lastchar = substr($base64raw, 21, 1);
+ $lastchar = strtr($lastchar, 'AQgw', '.Oeu');
+ $salt128bit .= $lastchar;
+
+ return $salt128bit;
+ }
+
+ /**
+ * @return bytes 16 random bytes to be used in the salt
+ */
+ protected function getRawSalt()
+ {
+ $rawSalt = false;
+ $numBytes = 16;
+ if (function_exists('mcrypt_create_iv')) {
+ $rawSalt = mcrypt_create_iv($numBytes, MCRYPT_DEV_URANDOM);
+ }
+ if (!$rawSalt) {
+ $rawSalt = $this->secureRandom->nextBytes($numBytes);
+ }
+
+ return $rawSalt;
+ }
+}
diff --git a/Core/Validator/Constraint/UserPassword.php b/Core/Validator/Constraint/UserPassword.php
index e90d9af..93ca24d 100644
--- a/Core/Validator/Constraint/UserPassword.php
+++ b/Core/Validator/Constraint/UserPassword.php
@@ -11,18 +11,19 @@
namespace Symfony\Component\Security\Core\Validator\Constraint;
-use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Security\Core\Validator\Constraints\UserPassword as BaseUserPassword;
/**
* @Annotation
+ *
+ * @deprecated Deprecated since version 2.2, to be removed in 2.3.
*/
-class UserPassword extends Constraint
+class UserPassword extends BaseUserPassword
{
- public $message = 'This value should be the user current password.';
- public $service = 'security.validator.user_password';
-
- public function validatedBy()
+ public function __construct($options = null)
{
- return $this->service;
+ trigger_error('UserPassword class in Symfony\Component\Security\Core\Validator\Constraint namespace is deprecated since version 2.2 and will be removed in 2.3. Use the Symfony\Component\Security\Core\Validator\Constraints\UserPassword class instead.', E_USER_DEPRECATED);
+
+ parent::__construct($options);
}
}
diff --git a/Core/Validator/Constraint/UserPasswordValidator.php b/Core/Validator/Constraint/UserPasswordValidator.php
index a54906b..0195fe5 100644
--- a/Core/Validator/Constraint/UserPasswordValidator.php
+++ b/Core/Validator/Constraint/UserPasswordValidator.php
@@ -11,36 +11,19 @@
namespace Symfony\Component\Security\Core\Validator\Constraint;
-use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
-use Symfony\Component\Validator\Constraint;
-use Symfony\Component\Validator\ConstraintValidator;
-use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
+use Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator as BaseUserPasswordValidator;
-class UserPasswordValidator extends ConstraintValidator
+/**
+ * @deprecated Deprecated since version 2.2, to be removed in 2.3.
+ */
+class UserPasswordValidator extends BaseUserPasswordValidator
{
- private $securityContext;
- private $encoderFactory;
-
public function __construct(SecurityContextInterface $securityContext, EncoderFactoryInterface $encoderFactory)
{
- $this->securityContext = $securityContext;
- $this->encoderFactory = $encoderFactory;
- }
-
- public function validate($password, Constraint $constraint)
- {
- $user = $this->securityContext->getToken()->getUser();
-
- if (!$user instanceof UserInterface) {
- throw new ConstraintDefinitionException('The User must extend UserInterface');
- }
-
- $encoder = $this->encoderFactory->getEncoder($user);
+ trigger_error('UserPasswordValidator class in Symfony\Component\Security\Core\Validator\Constraint namespace is deprecated since version 2.2 and will be removed in 2.3. Use the Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator class instead.', E_USER_DEPRECATED);
- if (!$encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
- $this->context->addViolation($constraint->message);
- }
+ parent::__construct($securityContext, $encoderFactory);
}
}
diff --git a/Core/Validator/Constraints/UserPassword.php b/Core/Validator/Constraints/UserPassword.php
new file mode 100644
index 0000000..ed29b0c
--- /dev/null
+++ b/Core/Validator/Constraints/UserPassword.php
@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\Validator\Constraints;
+
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * @Annotation
+ */
+class UserPassword extends Constraint
+{
+ public $message = 'This value should be the user current password.';
+ public $service = 'security.validator.user_password';
+
+ public function validatedBy()
+ {
+ return $this->service;
+ }
+}
diff --git a/Core/Validator/Constraints/UserPasswordValidator.php b/Core/Validator/Constraints/UserPasswordValidator.php
new file mode 100644
index 0000000..a4e0f90
--- /dev/null
+++ b/Core/Validator/Constraints/UserPasswordValidator.php
@@ -0,0 +1,46 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\Validator\Constraints;
+
+use Symfony\Component\Security\Core\User\UserInterface;
+use Symfony\Component\Security\Core\SecurityContextInterface;
+use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
+
+class UserPasswordValidator extends ConstraintValidator
+{
+ private $securityContext;
+ private $encoderFactory;
+
+ public function __construct(SecurityContextInterface $securityContext, EncoderFactoryInterface $encoderFactory)
+ {
+ $this->securityContext = $securityContext;
+ $this->encoderFactory = $encoderFactory;
+ }
+
+ public function validate($password, Constraint $constraint)
+ {
+ $user = $this->securityContext->getToken()->getUser();
+
+ if (!$user instanceof UserInterface) {
+ throw new ConstraintDefinitionException('The User object must implement the UserInterface interface.');
+ }
+
+ $encoder = $this->encoderFactory->getEncoder($user);
+
+ if (!$encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
+ $this->context->addViolation($constraint->message);
+ }
+ }
+}
diff --git a/Http/Firewall/ContextListener.php b/Http/Firewall/ContextListener.php
index bfbf0ee..6c06ca8 100644
--- a/Http/Firewall/ContextListener.php
+++ b/Http/Firewall/ContextListener.php
@@ -70,7 +70,6 @@ class ContextListener implements ListenerInterface
}
$request = $event->getRequest();
-
$session = $request->hasPreviousSession() ? $request->getSession() : null;
if (null === $session || null === $token = $session->get('_security_'.$this->contextKey)) {
@@ -117,7 +116,10 @@ class ContextListener implements ListenerInterface
$this->logger->debug('Write SecurityContext in the session');
}
- if (null === $session = $event->getRequest()->getSession()) {
+ $request = $event->getRequest();
+ $session = $request->hasPreviousSession() ? $request->getSession() : null;
+
+ if (null === $session) {
return;
}
diff --git a/Tests/Core/Encoder/BCryptPasswordEncoderTest.php b/Tests/Core/Encoder/BCryptPasswordEncoderTest.php
new file mode 100644
index 0000000..bfaf5fc
--- /dev/null
+++ b/Tests/Core/Encoder/BCryptPasswordEncoderTest.php
@@ -0,0 +1,112 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Tests\Core\Encoder;
+
+use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder;
+
+/**
+ * @author Elnur Abdurrakhimov <elnur@elnur.pro>
+ */
+class BCryptPasswordEncoderTest extends \PHPUnit_Framework_TestCase
+{
+ const PASSWORD = 'password';
+ const BYTES = '0123456789abcdef';
+ const VALID_COST = '04';
+
+ const SECURE_RANDOM_INTERFACE = 'Symfony\Component\Security\Core\Util\SecureRandomInterface';
+
+ /**
+ * @var \PHPUnit_Framework_MockObject_MockObject
+ */
+ private $secureRandom;
+
+ protected function setUp()
+ {
+ $this->secureRandom = $this->getMock(self::SECURE_RANDOM_INTERFACE);
+
+ $this->secureRandom
+ ->expects($this->any())
+ ->method('nextBytes')
+ ->will($this->returnValue(self::BYTES))
+ ;
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testCostBelowRange()
+ {
+ new BCryptPasswordEncoder($this->secureRandom, 3);
+ }
+
+ /**
+ * @expectedException \InvalidArgumentException
+ */
+ public function testCostAboveRange()
+ {
+ new BCryptPasswordEncoder($this->secureRandom, 32);
+ }
+
+ public function testCostInRange()
+ {
+ for ($cost = 4; $cost <= 31; $cost++) {
+ new BCryptPasswordEncoder($this->secureRandom, $cost);
+ }
+ }
+
+ public function testResultLength()
+ {
+ $encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
+ $result = $encoder->encodePassword(self::PASSWORD, null);
+ $this->assertEquals(60, strlen($result));
+ }
+
+ public function testValidation()
+ {
+ $encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
+ $result = $encoder->encodePassword(self::PASSWORD, null);
+ $this->assertTrue($encoder->isPasswordValid($result, self::PASSWORD, null));
+ $this->assertFalse($encoder->isPasswordValid($result, 'anotherPassword', null));
+ }
+
+ public function testValidationKnownPassword()
+ {
+ $encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
+ $prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=')
+ ? '2y' : '2a').'$';
+
+ $encrypted = $prefix.'04$ABCDEFGHIJKLMNOPQRSTU.uTmwd4KMSHxbUsG7bng8x7YdA0PM1iq';
+ $this->assertTrue($encoder->isPasswordValid($encrypted, self::PASSWORD, null));
+ }
+
+ public function testSecureRandomIsUsed()
+ {
+ if (function_exists('mcrypt_create_iv')) {
+ return;
+ }
+
+ $this->secureRandom
+ ->expects($this->atLeastOnce())
+ ->method('nextBytes')
+ ;
+
+ $encoder = new BCryptPasswordEncoder($this->secureRandom, self::VALID_COST);
+ $result = $encoder->encodePassword(self::PASSWORD, null);
+
+ $prefix = '$'.(version_compare(phpversion(), '5.3.7', '>=')
+ ? '2y' : '2a').'$';
+ $salt = 'MDEyMzQ1Njc4OWFiY2RlZe';
+ $expected = crypt(self::PASSWORD, $prefix . self::VALID_COST . '$' . $salt);
+
+ $this->assertEquals($expected, $result);
+ }
+}
diff --git a/Tests/Core/Validator/Constraint/UserPasswordValidatorTest.php b/Tests/Core/Validator/Constraints/UserPasswordValidatorTest.php
index e3bcbf4..d9395ba 100644
--- a/Tests/Core/Validator/Constraint/UserPasswordValidatorTest.php
+++ b/Tests/Core/Validator/Constraints/UserPasswordValidatorTest.php
@@ -9,10 +9,10 @@
* file that was distributed with this source code.
*/
-namespace Symfony\Component\Security\Tests\Core\Validator\Constraint;
+namespace Symfony\Component\Security\Tests\Core\Validator\Constraints;
-use Symfony\Component\Security\Core\Validator\Constraint\UserPassword;
-use Symfony\Component\Security\Core\Validator\Constraint\UserPasswordValidator;
+use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
+use Symfony\Component\Security\Core\Validator\Constraints\UserPasswordValidator;
class UserPasswordValidatorTest extends \PHPUnit_Framework_TestCase
{
diff --git a/Tests/Http/Firewall/ContextListenerTest.php b/Tests/Http/Firewall/ContextListenerTest.php
index 620aa29..2a8a28e 100644
--- a/Tests/Http/Firewall/ContextListenerTest.php
+++ b/Tests/Http/Firewall/ContextListenerTest.php
@@ -82,36 +82,12 @@ class ContextListenerTest extends \PHPUnit_Framework_TestCase
$this->assertFalse($session->has('_security_session'));
}
- protected function runSessionOnKernelResponse($newToken, $original = null)
- {
- $session = new Session(new MockArraySessionStorage());
-
- if ($original !== null) {
- $session->set('_security_session', $original);
- }
-
- $this->securityContext->setToken($newToken);
-
- $request = new Request();
- $request->setSession($session);
-
- $event = new FilterResponseEvent(
- $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'),
- $request,
- HttpKernelInterface::MASTER_REQUEST,
- new Response()
- );
-
- $listener = new ContextListener($this->securityContext, array(), 'session');
- $listener->onKernelResponse($event);
-
- return $session;
- }
-
public function testOnKernelResponseWithoutSession()
{
$this->securityContext->setToken(new UsernamePasswordToken('test1', 'pass1', 'phpunit'));
$request = new Request();
+ $session = new Session(new MockArraySessionStorage());
+ $request->setSession($session);
$event = new FilterResponseEvent(
$this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'),
@@ -123,7 +99,7 @@ class ContextListenerTest extends \PHPUnit_Framework_TestCase
$listener = new ContextListener($this->securityContext, array(), 'session');
$listener->onKernelResponse($event);
- $this->assertFalse($request->hasSession());
+ $this->assertFalse($session->isStarted());
}
/**
@@ -168,4 +144,30 @@ class ContextListenerTest extends \PHPUnit_Framework_TestCase
array(null),
);
}
-}
+
+ protected function runSessionOnKernelResponse($newToken, $original = null)
+ {
+ $session = new Session(new MockArraySessionStorage());
+
+ if ($original !== null) {
+ $session->set('_security_session', $original);
+ }
+
+ $this->securityContext->setToken($newToken);
+
+ $request = new Request();
+ $request->setSession($session);
+ $request->cookies->set('MOCKSESSID', true);
+
+ $event = new FilterResponseEvent(
+ $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface'),
+ $request,
+ HttpKernelInterface::MASTER_REQUEST,
+ new Response()
+ );
+
+ $listener = new ContextListener($this->securityContext, array(), 'session');
+ $listener->onKernelResponse($event);
+
+ return $session;
+ }}