Merge pull request #7 from skipperbent/feature-csrf

CSRF support
This commit is contained in:
Simon Sessingø
2015-10-21 18:30:10 +02:00
9 changed files with 158 additions and 58 deletions
+33 -5
View File
@@ -17,12 +17,22 @@ Add the latest version pf Simple PHP Router to your ```composer.json```
## Notes ## Notes
### Features
- Basic routing (get, post, put, delete) with support for custom multiple verbs.
- Regular Expression Constraints for parameters.
- Named routes.
- Generating url to routes.
- Route groups.
- Middleware (classes that intercepts before the route is rendered).
- Namespaces.
- Route prefixes.
- CSRF protection.
### Features currently "in-the-works" ### Features currently "in-the-works"
- Global Constraints - Global Constraints
- Named Routes
- Sub-Domain Routing - Sub-Domain Routing
- CSRF Protection
- Optional/required parameters - Optional/required parameters
## Initialising the router ## Initialising the router
@@ -138,8 +148,11 @@ class Router extends SimpleRouter {
// Init locale settings // Init locale settings
Locale::getInstance(); Locale::getInstance();
// Set default namespace // Set default namespace for routes
$defaultNamespace = '\\'.Registry::getInstance()->get('AppName') . '\\Controller'; $defaultNamespace = '\\'.Registry::getInstance()->get('AppName') . '\\Controller';
// Add custom csrf verifier (must extend BaseCsrfVerifier)
parent::csrfVerifier('MyProject\Middleware\CustomCsrfVerifier');
// Handle exceptions // Handle exceptions
try { try {
@@ -171,13 +184,28 @@ function url($controller, $parameters = null, $getParams = null) {
} }
``` ```
This is a basic example for getting the current csrf token
```php
/**
* Get current csrf-token
* @return null|string
*/
function csrf_token() {
$token = new \Pecee\CsrfToken();
return $token->getToken();
}
```
### Example for getting the url
In ```routes.php``` we have added this route: In ```routes.php``` we have added this route:
```SimpleRouter::get('/item/{id}', 'myController@show');``` ```SimpleRouter::get('/item/{id}', 'myController@show', ['as' => 'item']);```
In the template we then call: In the template we then call:
```url('myController@show', ['id' => 22], ['category' => 'shoes']);``` ```url('item', ['id' => 22], ['category' => 'shoes']);```
Result url is: Result url is:
+24 -38
View File
@@ -3,37 +3,21 @@ namespace Pecee;
class CsrfToken { class CsrfToken {
const CSRF_KEY = 'csrf_token'; const CSRF_KEY = 'XSRF-TOKEN';
protected static $instance; protected $token;
protected $lastToken;
protected $currentToken;
public static function getInstance() {
if(self::$instance === null) {
self::$instance = new static();
}
return self::$instance;
}
public function __construct() { public function __construct() {
$this->lastToken = isset($_SESSION[self::CSRF_KEY]) ? $_SESSION[self::CSRF_KEY] : null; if($this->getToken() === null) {
$this->currentToken = $this->generate(); $this->setToken($this->generateToken());
// Initialise session, if it hasn't been initialised.
if(!isset($_SESSION)) {
session_start();
} }
$_SESSION['csrf_token'] = $this->currentToken;
} }
/** /**
* Generate random identifier for CSRF token * Generate random identifier for CSRF token
* @return string * @return string
*/ */
public static function generate() { public static function generateToken() {
if (function_exists('mcrypt_create_iv')) { if (function_exists('mcrypt_create_iv')) {
return bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM)); return bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
} }
@@ -47,28 +31,30 @@ class CsrfToken {
* @return bool * @return bool
*/ */
public function validate($token) { public function validate($token) {
return hash_equals($token, $_SESSION[self::CSRF_KEY]); if($token !== null && $this->getToken() !== null) {
return hash_equals($token, $this->getToken());
}
return false;
} }
/** /**
* Set csrf token cookie
*
* @param $token
*/
public function setToken($token) {
setcookie(self::CSRF_KEY, $token, time() + 60 * 120, '/');
}
/**
* Get csrf token
* @return string|null * @return string|null
*/ */
public function getLastToken(){ public function getToken(){
return $this->lastToken; if(isset($_COOKIE[self::CSRF_KEY])) {
} return $_COOKIE[self::CSRF_KEY];
}
/** return null;
* @param string|null $lastToken
*/
public function setLastToken($lastToken){
$this->lastToken = $lastToken;
}
/**
* @return string|null
*/
public function getCurrentToken(){
return $this->currentToken;
} }
} }
@@ -0,0 +1,34 @@
<?php
namespace Pecee\Http\Middleware;
use Pecee\CsrfToken;
use Pecee\Http\Request;
use Pecee\SimpleRouter\RouterException;
class BaseCsrfVerifier extends Middleware {
const POST_KEY = 'csrf-token';
const HEADER_KEY = 'X-CSRF-TOKEN';
public function handle(Request $request) {
if($request->getMethod() != 'get') {
$token = (isset($_POST[self::POST_KEY])) ? $_POST[self::POST_KEY] : null;
// If the token is not posted, check headers for valid x-csrf-token
if($token === null) {
$token = $request->getHeader(self::HEADER_KEY);
}
$tokenValidator = new CsrfToken();
if( !$tokenValidator->validate( $token ) ) {
throw new RouterException('Invalid csrf-token.');
}
}
}
}
+1 -3
View File
@@ -7,7 +7,5 @@ use Pecee\SimpleRouter\RouterEntry;
abstract class Middleware abstract class Middleware
{ {
public function handle(Request $request) { abstract function handle(Request $request);
return true;
}
} }
@@ -1,9 +0,0 @@
<?php
namespace Pecee\Http\Middleware;
class VerifyCsrfToken extends Middleware {
}
+19
View File
@@ -6,11 +6,13 @@ class Request {
protected $uri; protected $uri;
protected $host; protected $host;
protected $method; protected $method;
protected $headers;
public function __construct() { public function __construct() {
$this->host = $_SERVER['HTTP_HOST']; $this->host = $_SERVER['HTTP_HOST'];
$this->uri = rtrim($_SERVER['REQUEST_URI'], '/') . '/'; $this->uri = rtrim($_SERVER['REQUEST_URI'], '/') . '/';
$this->method = (isset($_POST['_method'])) ? strtolower($_POST['_method']) : strtolower($_SERVER['REQUEST_METHOD']); $this->method = (isset($_POST['_method'])) ? strtolower($_POST['_method']) : strtolower($_SERVER['REQUEST_METHOD']);
$this->headers = getallheaders();
} }
/** /**
@@ -50,4 +52,21 @@ class Request {
return (isset($_SERVER['PHP_AUTH_PW'])) ? $_SERVER['PHP_AUTH_PW']: null; return (isset($_SERVER['PHP_AUTH_PW'])) ? $_SERVER['PHP_AUTH_PW']: null;
} }
/**
* Get headers
* @return array
*/
public function getHeaders() {
return $this->headers;
}
/**
* Get header value by name
* @param string $name
* @return string|null
*/
public function getHeader($name) {
return (isset($this->headers[$name])) ? $this->headers[$name] : null;
}
} }
+31 -2
View File
@@ -2,6 +2,7 @@
namespace Pecee\SimpleRouter; namespace Pecee\SimpleRouter;
use Pecee\ArrayUtil; use Pecee\ArrayUtil;
use Pecee\Http\Middleware\BaseCsrfVerifier;
use Pecee\Http\Request; use Pecee\Http\Request;
use Pecee\Url; use Pecee\Url;
@@ -17,6 +18,7 @@ class RouterBase {
protected $backstack; protected $backstack;
protected $loadedRoute; protected $loadedRoute;
protected $defaultNamespace; protected $defaultNamespace;
protected $baseCsrfVerifier;
// TODO: make interface for controller routers, so they can be easily detected // TODO: make interface for controller routers, so they can be easily detected
// TODO: clean up - cut some of the methods down to smaller pieces // TODO: clean up - cut some of the methods down to smaller pieces
@@ -26,6 +28,7 @@ class RouterBase {
$this->backstack = array(); $this->backstack = array();
$this->controllerUrlMap = array(); $this->controllerUrlMap = array();
$this->request = new Request(); $this->request = new Request();
$this->baseCsrfVerifier = new BaseCsrfVerifier();
} }
public function addRoute(RouterEntry $route) { public function addRoute(RouterEntry $route) {
@@ -85,8 +88,16 @@ class RouterBase {
} }
public function routeRequest() { public function routeRequest() {
// Loop through each route-request
// Verify csrf token for request
if($this->baseCsrfVerifier !== null) {
/* @var $csrfVerifier BaseCsrfVerifier */
$csrfVerifier = $this->baseCsrfVerifier;
$csrfVerifier = new $csrfVerifier();
$csrfVerifier->handle($this->request);
}
// Loop through each route-request
$this->processRoutes($this->routes); $this->processRoutes($this->routes);
// Make sure the urls is in the right order when comparing // Make sure the urls is in the right order when comparing
@@ -100,7 +111,6 @@ class RouterBase {
foreach($this->controllerUrlMap as $route) { foreach($this->controllerUrlMap as $route) {
$routeMatch = $route->matchRoute($this->request); $routeMatch = $route->matchRoute($this->request);
if($routeMatch && !($routeMatch instanceof RouterGroup)) { if($routeMatch && !($routeMatch instanceof RouterGroup)) {
if(count($route->getRequestMethods()) && !in_array($this->request->getMethod(), $route->getRequestMethods())) { if(count($route->getRequestMethods()) && !in_array($this->request->getMethod(), $route->getRequestMethods())) {
@@ -179,6 +189,25 @@ class RouterBase {
return $this->request; return $this->request;
} }
/**
* Get base csrf verifier class
* @return BaseCsrfVerifier
*/
public function getBaseCsrfVerifier() {
return $this->baseCsrfVerifier;
}
/**
* Set base csrf verifier class
*
* @param BaseCsrfVerifier $baseCsrfVerifier
* @return self
*/
public function setBaseCsrfVerifier(BaseCsrfVerifier $baseCsrfVerifier) {
$this->baseCsrfVerifier = $baseCsrfVerifier;
return $this;
}
protected function processUrl($route, $method = null, $parameters = null, $getParams = null) { protected function processUrl($route, $method = null, $parameters = null, $getParams = null) {
$url = $route->getUrl(); $url = $route->getUrl();
+1 -1
View File
@@ -269,7 +269,7 @@ abstract class RouterEntry {
$className = $this->getNamespace() . '\\' . $controller[0]; $className = $this->getNamespace() . '\\' . $controller[0];
$class = $this->loadClass($className); $class = $this->loadClass($className);
$method = $request->getMethod() . ucfirst($controller[1]); $method = $controller[1];
if (!method_exists($class, $method)) { if (!method_exists($class, $method)) {
throw new RouterException(sprintf('Method %s does not exist in class %s', $method, $className), 404); throw new RouterException(sprintf('Method %s does not exist in class %s', $method, $className), 404);
+15
View File
@@ -9,14 +9,29 @@
namespace Pecee\SimpleRouter; namespace Pecee\SimpleRouter;
use Pecee\Http\Middleware\BaseCsrfVerifier;
class SimpleRouter { class SimpleRouter {
/**
* Start/route request
* @param null $defaultNamespace
* @throws RouterException
*/
public static function start($defaultNamespace = null) { public static function start($defaultNamespace = null) {
$router = RouterBase::GetInstance(); $router = RouterBase::GetInstance();
$router->setDefaultNamespace($defaultNamespace); $router->setDefaultNamespace($defaultNamespace);
$router->routeRequest(); $router->routeRequest();
} }
/**
* Set base csrf verifier
* @param BaseCsrfVerifier $baseCsrfVerifier
*/
public static function csrfVerifier(BaseCsrfVerifier $baseCsrfVerifier) {
RouterBase::getInstance()->setBaseCsrfVerifier($baseCsrfVerifier);
}
public static function get($url, $callback, array $settings = null) { public static function get($url, $callback, array $settings = null) {
$route = new RouterRoute($url, $callback); $route = new RouterRoute($url, $callback);
$route->addSettings($settings); $route->addSettings($settings);