Commit 882ef344 authored by catch's avatar catch

Issue #2036351 by damiankloip, Xano, dawehner: Convert CSRF tokens to a service.

parent fcb24f94
......@@ -369,6 +369,14 @@ services:
class: Drupal\Core\EventSubscriber\LegacyAccessSubscriber
tags:
- { name: event_subscriber }
private_key:
class: Drupal\Core\PrivateKey
arguments: ['@state']
csrf_token:
class: Drupal\Core\Access\CsrfTokenGenerator
arguments: ['@private_key']
calls:
- [setRequest, ['@?request']]
access_manager:
class: Drupal\Core\Access\AccessManager
calls:
......
......@@ -3032,21 +3032,21 @@ function drupal_json_decode($var) {
/**
* Ensures the private key variable used to generate tokens is set.
*
* @return
* @return string
* The private key.
*
* @see \Drupal\Core\Access\CsrfTokenManager
*
* @deprecated as of Drupal 8.0. Use the 'private_key' service instead.
*/
function drupal_get_private_key() {
if (!($key = Drupal::state()->get('system.private_key'))) {
$key = Crypt::randomStringHashed(55);
Drupal::state()->set('system.private_key', $key);
}
return $key;
return \Drupal::service('private_key')->get();
}
/**
* Generates a token based on $value, the user session, and the private key.
*
* @param $value
* @param string $value
* An additional value to base the token on.
*
* @return string
......@@ -3055,28 +3055,34 @@ function drupal_get_private_key() {
* 'drupal_private_key' configuration variable.
*
* @see drupal_get_hash_salt()
* @see \Drupal\Core\Access\CsrfTokenManager
*
* @deprecated as of Drupal 8.0. Use the csrf_token service instead.
*/
function drupal_get_token($value = '') {
return Crypt::hmacBase64($value, session_id() . drupal_get_private_key() . drupal_get_hash_salt());
return \Drupal::csrfToken()->get($value);
}
/**
* Validates a token based on $value, the user session, and the private key.
*
* @param $token
* @param string $token
* The token to be validated.
* @param $value
* @param string $value
* An additional value to base the token on.
* @param $skip_anonymous
* @param bool $skip_anonymous
* Set to true to skip token validation for anonymous users.
*
* @return
* @return bool
* True for a valid token, false for an invalid token. When $skip_anonymous
* is true, the return value will always be true for anonymous users.
*
* @see \Drupal\Core\Access\CsrfTokenManager
*
* @deprecated as of Drupal 8.0. Use the csrf_token service instead.
*/
function drupal_valid_token($token, $value = '', $skip_anonymous = FALSE) {
global $user;
return (($skip_anonymous && $user->id() == 0) || ($token == drupal_get_token($value)));
return \Drupal::csrfToken()->validate($token, $value, $skip_anonymous);
}
/**
......
......@@ -391,4 +391,14 @@ public static function languageManager() {
return static::$container->get('language_manager');
}
/**
* Returns the CSRF token manager service.
*
* @return \Drupal\Core\Access\CsrfTokenGenerator
* The CSRF token manager.
*/
public static function csrfToken() {
return static::$container->get('csrf_token');
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Access\CsrfTokenGenerator.
*/
namespace Drupal\Core\Access;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\PrivateKey;
use Symfony\Component\HttpFoundation\Request;
/**
* Generates and validates CSRF tokens.
*
* @see \Drupal\Tests\Core\Access\CsrfTokenGeneratorTest
*/
class CsrfTokenGenerator {
/**
* The private key service.
*
* @var \Drupal\Core\PrivateKey
*/
protected $privateKey;
/**
* The current request object.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* Constructs the token generator.
*
* @param \Drupal\Core\PrivateKey $private_key
* The private key service.
*/
public function __construct(PrivateKey $private_key) {
$this->privateKey = $private_key;
}
/**
* Sets the $request property.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The HttpRequest object representing the current request.
*/
public function setRequest(Request $request) {
$this->request = $request;
}
/**
* Generates a token based on $value, the user session, and the private key.
*
* @param string $value
* (optional) An additional value to base the token on.
*
* @return string
* A 43-character URL-safe token for validation, based on the user session
* ID, the hash salt provided by drupal_get_hash_salt(), and the
* 'drupal_private_key' configuration variable.
*
* @see drupal_get_hash_salt()
*/
public function get($value = '') {
return Crypt::hmacBase64($value, session_id() . $this->privateKey->get() . drupal_get_hash_salt());
}
/**
* Validates a token based on $value, the user session, and the private key.
*
* @param string $token
* The token to be validated.
* @param string $value
* (optional) An additional value to base the token on.
* @param bool $skip_anonymous
* (optional) Set to TRUE to skip token validation for anonymous users.
*
* @return bool
* TRUE for a valid token, FALSE for an invalid token. When $skip_anonymous
* is TRUE, the return value will always be TRUE for anonymous users.
*/
public function validate($token, $value = '', $skip_anonymous = FALSE) {
$user = $this->request->attributes->get('account');
return ($skip_anonymous && $user->id() == 0) || ($token == $this->get($value));
}
}
<?php
/**
* @file
* Contains \Drupal\Core\PrivateKey.
*/
namespace Drupal\Core;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Component\Utility\Crypt;
/**
* Manages the Drupal private key.
*/
class PrivateKey {
/**
* The state service.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $state;
/**
* Constructs the token generator.
*
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state
* The state service.
*/
function __construct(KeyValueStoreInterface $state) {
$this->state = $state;
}
/**
* Gets the private key.
*
* @return string
* The private key.
*/
public function get() {
if (!$key = $this->state->get('system.private_key')) {
$key = $this->create();
$this->set($key);
}
return $key;
}
/**
* Sets the private key.
*
* @param string $key
* The private key to set.
*/
public function set($key) {
return $this->state->set('system.private_key', $key);
}
/**
* Creates a new private key.
*
* @return string
* The private key.
*/
protected function create() {
return Crypt::randomStringHashed(55);
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Access\CsrfTokenGeneratorTest.
*/
namespace Drupal\Tests\Core\Access {
use Drupal\Tests\UnitTestCase;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Component\Utility\Crypt;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests the CSRF token generator.
*/
class CsrfTokenGeneratorTest extends UnitTestCase {
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $generator;
public static function getInfo() {
return array(
'name' => 'CsrfTokenGenerator test',
'description' => 'Tests the CsrfTokenGenerator class.',
'group' => 'Access'
);
}
/**
* {@inheritdoc}
*/
function setUp() {
parent::setUp();
$this->key = Crypt::randomStringHashed(55);
$private_key = $this->getMockBuilder('Drupal\Core\PrivateKey')
->disableOriginalConstructor()
->setMethods(array('get'))
->getMock();
$private_key->expects($this->any())
->method('get')
->will($this->returnValue($this->key));
$this->generator = new CsrfTokenGenerator($private_key);
$this->generator->setRequest(new Request());
}
/**
* Tests CsrfTokenGenerator::get().
*/
public function testGet() {
$this->assertInternalType('string', $this->generator->get());
$this->assertNotSame($this->generator->get(), $this->generator->get($this->randomName()));
$this->assertNotSame($this->generator->get($this->randomName()), $this->generator->get($this->randomName()));
}
/**
* Tests CsrfTokenGenerator::validate().
*/
public function testValidate() {
$token = $this->generator->get();
$this->assertTrue($this->generator->validate($token));
$this->assertFalse($this->generator->validate($token, 'foo'));
$token = $this->generator->get('bar');
$this->assertTrue($this->generator->validate($token, 'bar'));
}
}
}
/**
* @todo Remove this when https://drupal.org/node/2036259 is resolved.
*/
namespace {
if (!function_exists('drupal_get_hash_salt')) {
function drupal_get_hash_salt() {
return hash('sha256', 'test_hash_salt');
}
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\Core\PrivateKeyTest.
*/
namespace Drupal\Tests\Core;
use Drupal\Core\PrivateKey;
use Drupal\Tests\UnitTestCase;
use Drupal\Component\Utility\Crypt;
/**
* Tests the private key service.
*/
class PrivateKeyTest extends UnitTestCase {
/**
* The state mock class.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $state;
/**
* The private key service mock.
*
* @var \Drupal\Core\PrivateKey
*/
protected $privateKey;
/**
* The random key to use in tests.
*
* @var string
*/
protected $key;
public static function getInfo() {
return array(
'name' => 'PrivateKey test',
'description' => 'Tests the PrivateKey class.',
'group' => 'System'
);
}
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->key = Crypt::randomStringHashed(55);
$this->state = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreInterface');
$this->privateKey = new PrivateKey($this->state);
}
/**
* Tests PrivateKey::get().
*/
public function testGet() {
$this->state->expects($this->once())
->method('get')
->with('system.private_key')
->will($this->returnValue($this->key));
$this->assertEquals($this->key, $this->privateKey->get());
}
/**
* Tests PrivateKey::get() with no private key from state.
*/
public function testGetNoState() {
$this->assertInternalType('string', $this->privateKey->get());
}
/**
* Tests PrivateKey::setPrivateKey().
*/
public function testSet() {
$random_name = $this->randomName();
$this->state->expects($this->once())
->method('set')
->with('system.private_key', $random_name)
->will($this->returnValue(TRUE));
$this->privateKey->set($random_name);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment