RequestToken.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\security\validation;
  9. use lithium\security\Password;
  10. use lithium\util\String;
  11. use lithium\util\Set;
  12. /**
  13. * The `RequestToken` class creates cryptographically-secure tokens and keys that can be used to
  14. * validate the authenticity of client requests.
  15. *
  16. * `RequestToken` will persist the token for the life of
  17. * the client session, and generate per-request keys that will match against that token.
  18. *
  19. * Using these token/key pairs in forms and other non-idempotent requests will help you secure your
  20. * application against cross-site request forgeries, or CSRF attacks.
  21. *
  22. * ### Example
  23. *
  24. * {{{
  25. * // views/comments/add.html.php:
  26. * // ...
  27. * <?=$this->form->create($object); ?>
  28. * <?=$this->security->requestToken(); ?>
  29. * // Other fields...
  30. * <?=$this->form->end(); ?>
  31. * }}}
  32. *
  33. * {{{
  34. * // controllers/CommentsController.php:
  35. * public function add() {
  36. * if ($this->request->data && !RequestToken::check($this->request)) {
  37. * // Key didn't match the CSRF token. Regenerate the session token and
  38. * // prompt the user to retry the form submission.
  39. * RequestToken::get(array('regenerate' => true));
  40. * return;
  41. * }
  42. * // Handle a normal request...
  43. * }
  44. * }}}
  45. * @link http://shiflett.org/articles/cross-site-request-forgeries Cross-Site Request Forgeries
  46. * @see lithium\template\helper\Security::requestToken()
  47. */
  48. class RequestToken {
  49. /**
  50. * Class dependencies.
  51. *
  52. * @var array
  53. */
  54. protected static $_classes = array(
  55. 'session' => 'lithium\storage\Session'
  56. );
  57. /**
  58. * Used to get or reconfigure dependencies with custom classes.
  59. *
  60. * @param array $config When assigning new configuration, should be an array containing a
  61. * `'classes'` key.
  62. * @return array If `$config` is empty, returns an array with a `'classes'` key containing class
  63. * dependencies. Otherwise returns `null`.
  64. */
  65. public static function config(array $config = array()) {
  66. if (!$config) {
  67. return array('classes' => static::$_classes);
  68. }
  69. foreach ($config as $key => $val) {
  70. $key = "_{$key}";
  71. if (isset(static::${$key})) {
  72. static::${$key} = $val + static::${$key};
  73. }
  74. }
  75. }
  76. /**
  77. * Generates (or regenerates) a cryptographically-secure token to be used for the life of the
  78. * client session, and stores the token using the `Session` class.
  79. *
  80. * @see lithium\util\String::hash()
  81. * @param array $options An array of options to be used when generating or storing the token:
  82. * - `'regenerate'` _boolean_: If `true`, will force the regeneration of a the
  83. * token, even if one is already available in the session. Defaults to `false`.
  84. * - `'sessionKey'` _string_: The key used for session storage and retrieval.
  85. * Defaults to `'security.token'`.
  86. * - `'salt'` _string_: If the token is being generated (or regenerated), sets a
  87. * custom salt value to be used by `String::hash()`.
  88. * - `'type'` _string_: The hashing algorithm used by `String::hash()` when
  89. * generating the token. Defaults to `'sha512'`.
  90. * @return string Returns a cryptographically-secure client session token.
  91. */
  92. public static function get(array $options = array()) {
  93. $defaults = array(
  94. 'regenerate' => false,
  95. 'sessionKey' => 'security.token',
  96. 'salt' => null,
  97. 'type' => 'sha512'
  98. );
  99. $options += $defaults;
  100. $session = static::$_classes['session'];
  101. if ($options['regenerate'] || !($token = $session::read($options['sessionKey']))) {
  102. $token = String::hash(uniqid(microtime(true)), $options);
  103. $session::write($options['sessionKey'], $token);
  104. }
  105. return $token;
  106. }
  107. /**
  108. * Generates a single-use key to be embedded in a form or used with another non-idempotent
  109. * request (a request that changes the state of the server or application), that will match
  110. * against a client session token using the `check()` method.
  111. *
  112. * @see lithium\security\validation\RequestToken::check()
  113. * @param array $options An array of options to be passed to `RequestToken::get()`.
  114. * @return string Returns a hashed key string for use with `RequestToken::check()`.
  115. */
  116. public static function key(array $options = array()) {
  117. return Password::hash(static::get($options));
  118. }
  119. /**
  120. * Checks a single-use hash key against the session token that generated it, using
  121. * a cryptographically-secure verification method. Accepts either the request key as a string,
  122. * or a `Request` object with a `$data` property containing a `['security']['token']` key.
  123. *
  124. * For example, the following two controller code samples are equivalent:
  125. *
  126. * {{{
  127. * $key = $this->request->data['security']['token'];
  128. *
  129. * if (!RequestToken::check($key)) {
  130. * // Handle invalid request...
  131. * }
  132. * }}}
  133. *
  134. * {{{
  135. * if (!RequestToken::check($this->request)) {
  136. * // Handle invalid request...
  137. * }
  138. * }}}
  139. *
  140. * @param mixed $key Either the actual key as a string, or a `Request` object containing the
  141. * key.
  142. * @param array $options The options to use when matching the key to the token:
  143. * - `'sessionKey'` _string_: The key used when reading the token from the session.
  144. * @return boolean Returns `true` if the hash key is a cryptographic match to the stored
  145. * session token. Returns `false` on failure, which indicates a forged request attempt.
  146. */
  147. public static function check($key, array $options = array()) {
  148. $defaults = array('sessionKey' => 'security.token');
  149. $options += $defaults;
  150. $session = static::$_classes['session'];
  151. if (is_object($key) && isset($key->data)) {
  152. $result = Set::extract($key->data, '/security/token');
  153. $key = $result ? $result[0] : null;
  154. }
  155. return Password::check($session::read($options['sessionKey']), (string) $key);
  156. }
  157. }
  158. ?>