Csrf

* add csrf middleware and protect login route
* test login action
parent 89567924
......@@ -51,6 +51,8 @@ $container['logger'] = function ($c) {
return $logger;
};
// Event manager
$container['events'] = function ($c) {
return new \Zend\EventManager\EventManager(
new \Zend\EventManager\SharedEventManager(),
......@@ -58,6 +60,12 @@ $container['events'] = function ($c) {
);
};
// Csrf guard
$container['csrf'] = function ($c) {
return new \Slim\Csrf\Guard;
};
// Database
$container['db'] = function ($c) {
......@@ -132,7 +140,14 @@ $container['GrEduLabs\\Action\\User\\Login'] = function ($c) {
$adapter = $c->get('authentication_db_adapter');
$service->setAdapter($adapter);
return new GrEduLabs\Action\User\Login($c->get('view'), $service, $adapter, $c->get('flash'));
return new GrEduLabs\Action\User\Login(
$c->get('view'),
$service,
$adapter,
$c->get('flash'),
$c->get('csrf'),
$c->get('router')->pathFor('index')
);
};
$container['GrEduLabs\\Action\\User\\LoginSso'] = function ($c) {
......
......@@ -24,8 +24,13 @@ $app->get('/', function ($request, $response, $args) {
// authentication
$app->group('/user', function () {
$this->map(['GET', 'POST'], '/login', 'GrEduLabs\\Action\\User\\Login')->setName('user.login');
$this->get('/login-sso', 'GrEduLabs\\Action\\User\\LoginSso')->setName('user.loginSso');
$this->get('/logout', 'GrEduLabs\\Action\\User\\Logout')->setName('user.logout');
$this->get('/profile', 'GrEduLabs\\Action\\User\\Profile')->setName('user.profile');
$this->map(['GET', 'POST'], '/login', 'GrEduLabs\\Action\\User\\Login')
->setName('user.login')
->add('csrf');
$this->get('/login-sso', 'GrEduLabs\\Action\\User\\LoginSso')
->setName('user.loginSso');
$this->get('/logout', 'GrEduLabs\\Action\\User\\Logout')
->setName('user.logout');
$this->get('/profile', 'GrEduLabs\\Action\\User\\Profile')
->setName('user.profile');
});
......@@ -10,8 +10,9 @@
namespace GrEduLabs\Action\User;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Csrf\Guard;
use Slim\Flash\Messages;
use Slim\Http\Request;
use Slim\Views\Twig;
use Zend\Authentication\Adapter\AdapterInterface;
use Zend\Authentication\Adapter\ValidatableAdapterInterface;
......@@ -39,6 +40,16 @@ class Login
*/
protected $flash;
/**
* @Var Guard
*/
protected $csrf;
/**
* @var string
*/
protected $successUrl;
/**
* Constructor
* @param Twig $view
......@@ -50,32 +61,55 @@ class Login
Twig $view,
AuthenticationServiceInterface $authService,
AdapterInterface $authAdapter,
Messages $flash
Messages $flash,
Guard $csrf,
$successUrl
) {
$this->view = $view;
$this->authService = $authService;
$this->authAdapter = $authAdapter;
$this->flash = $flash;
$this->authService->setAdapter($this->authAdapter);
$this->csrf = $csrf;
$this->successUrl = $successUrl;
if (method_exists($this->authService, 'setAdapter')) {
$this->authService->setAdapter($this->authAdapter);
}
}
public function __invoke(ServerRequestInterface $req, ResponseInterface $res, array $args = [])
public function __invoke(Request $req, ResponseInterface $res)
{
if ($req->isPost()) {
if ($this->authAdapter instanceof ValidatableAdapterInterface) {
$this->authAdapter->setIdentity($req->getParam('identity'))
->setCredential($req->getParam('credential'));
$this->authAdapter->setIdentity($req->getParam('identity'));
$this->authAdapter->setCredential($req->getParam('credential'));
}
$result = $this->authService->authenticate();
$result = $this->authService->authenticate($this->authAdapter);
if (!$result->isValid()) {
$this->flash->addMessage('danger', reset($result->getMessages()));
return $res->withRedirect($req->getUri());
}
return $res->withRedirect('/');
return $res->withRedirect($this->successUrl);
}
return $this->view->render($res, 'user/login.twig');
return $this->view->render($res, 'user/login.twig', $this->getCsrfData($req));
}
private function getCsrfData(Request $req)
{
$nameKey = $this->csrf->getTokenNameKey();
$valueKey = $this->csrf->getTokenValueKey();
$name = $req->getAttribute($nameKey);
$value = $req->getAttribute($valueKey);
return [
'csrf_name_key' => $nameKey,
'csrf_value_key' => $valueKey,
'csrf_name' => $name,
'csrf_value' => $value,
];
}
}
......@@ -27,5 +27,7 @@
<button type="submit" name="login_method" value="credentials" class="btn btn-primary">Sing in</button>
<a href="{{ path_for('user.loginSso') }}" class="btn bnt-default">SSO</a>
</div>
<input type="hidden" name="{{ csrf_name_key }}" value="{{ csrf_name }}">
<input type="hidden" name="{{ csrf_value_key }}" value="{{ csrf_value }}">
</form>
{% endblock %}
\ No newline at end of file
......@@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "40b5bd0e81652838fc8051f261b47421",
"content-hash": "0e0d863dd17813ff2a9fa920f5714e31",
"hash": "6db91d83a5d7f09ca4aa148dd467aa37",
"content-hash": "9dbc2f0e135613f71e872c3845d833ba",
"packages": [
{
"name": "container-interop/container-interop",
......@@ -256,6 +256,54 @@
],
"time": "2015-06-18 19:15:47"
},
{
"name": "paragonie/random_compat",
"version": "1.1.5",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "dd8998b7c846f6909f4e7a5f67fabebfc412a4f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/dd8998b7c846f6909f4e7a5f67fabebfc412a4f7",
"reference": "dd8998b7c846f6909f4e7a5f67fabebfc412a4f7",
"shasum": ""
},
"require": {
"php": ">=5.2.0"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"autoload": {
"files": [
"lib/random.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"pseudorandom",
"random"
],
"time": "2016-01-06 13:31:20"
},
{
"name": "pimple/pimple",
"version": "v3.0.2",
......@@ -389,6 +437,56 @@
],
"time": "2012-12-21 11:40:51"
},
{
"name": "slim/csrf",
"version": "0.6.0",
"source": {
"type": "git",
"url": "https://github.com/slimphp/Slim-Csrf.git",
"reference": "f56bccc1c3a9c76e5fcf7c6d56698a32f9e7e9d8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/slimphp/Slim-Csrf/zipball/f56bccc1c3a9c76e5fcf7c6d56698a32f9e7e9d8",
"reference": "f56bccc1c3a9c76e5fcf7c6d56698a32f9e7e9d8",
"shasum": ""
},
"require": {
"paragonie/random_compat": "^1.1",
"php": ">=5.5.0",
"psr/http-message": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0",
"slim/slim": "~3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Slim\\Csrf\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Josh Lockhart",
"email": "hello@joshlockhart.com",
"homepage": "http://joshlockhart.com"
}
],
"description": "Slim Framework 3 CSRF protection middleware",
"homepage": "http://slimframework.com",
"keywords": [
"csrf",
"framework",
"middleware",
"slim"
],
"time": "2015-12-22 10:13:02"
},
{
"name": "slim/flash",
"version": "0.1.0",
......
<?php
/**
* gredu_labs
*
* @link https://github.com/eellak/gredu_labs for the canonical source repository
* @copyright Copyright (c) 2008-2015 Greek Free/Open Source Software Society (https://gfoss.ellak.gr/)
* @license GNU GPLv3 http://www.gnu.org/licenses/gpl-3.0-standalone.html
*/
namespace GrEduLabsTest\Action\User;
use GrEduLabs\Action\User\Login;
use Slim\Csrf\Guard;
use Slim\Http\Response;
use Zend\Authentication\Result;
class LoginTest extends \PHPUnit_Framework_TestCase
{
private $request;
private $response;
private $action;
private $view;
private $authService;
private $authAdapter;
private $flash;
private $guard;
private $successUrl = '/some/success/url';
private $guardStorage = [];
protected function setUp()
{
$this->request = $this->getMockBuilder('\\Slim\\Http\\Request')
->disableOriginalConstructor()
->getMock();
$this->response = new Response();
$this->view = $this->getMockBuilder('\\Slim\\Views\\Twig')
->disableOriginalConstructor()
->getMock();
$this->authService = $this->getMock('\\Zend\\Authentication\\AuthenticationServiceInterface');
$this->flash = $this->getMock('\\Slim\\Flash\\Messages');
$this->authAdapter = $this->getMock('\\Zend\\Authentication\\Adapter\\AdapterInterface');
$this->guard = new Guard('csrf', $this->guardStorage);
$this->action = new Login(
$this->view,
$this->authService,
$this->authAdapter,
$this->flash,
$this->guard,
$this->successUrl
);
}
public function testConstructorSetsDependecies()
{
$this->assertAttributeSame($this->view, 'view', $this->action);
$this->assertAttributeSame($this->authService, 'authService', $this->action);
$this->assertAttributeSame($this->authAdapter, 'authAdapter', $this->action);
$this->assertAttributeSame($this->flash, 'flash', $this->action);
$this->assertAttributeSame($this->guard, 'csrf', $this->action);
$this->assertAttributeSame($this->successUrl, 'successUrl', $this->action);
}
public function testInvokeSetsAdapterToService()
{
$adapter = null;
$authService = $this->getMock('\\Zend\\Authentication\\AuthenticationService');
$authService->expects($this->any())
->method('setAdapter')
->will($this->returnCallback(function () use (&$adapter) {
$args = func_get_args();
$adapter = $args[0];
}));
$action = new Login(
$this->view,
$authService,
$this->authAdapter,
$this->flash,
$this->guard,
$this->successUrl
);
$this->assertSame($this->authAdapter, $adapter);
}
public function testInvokeReturnsViewOnGetRequest()
{
$template = null;
$data = [];
$this->request->expects($this->any())
->method('isPost')
->will($this->returnValue(false));
$this->view->expects($this->any())
->method('render')
->will($this->returnCallback(function () use (&$template, &$data) {
$args = func_get_args();
$template = $args[1];
$data = $args[2];
return $args[0];
}));
$action = $this->action;
$actionResponse = $action($this->request, $this->response);
$this->assertSame($template, 'user/login.twig');
$this->assertContains($this->guardStorage, $data);
}
public function testInvokePassCredentialsToAuthAdapter()
{
$identity = null;
$credential = null;
$adapter = $this->getMock('\\Zend\\Authentication\\Adapter\\ValidatableAdapterInterface');
$adapter->expects($this->any())
->method('setIdentity')
->will($this->returnCallback(function () use (&$identity) {
$args = func_get_args();
$identity = $args[0];
}));
$adapter->expects($this->any())
->method('setCredential')
->will($this->returnCallback(function () use (&$credential) {
$args = func_get_args();
$credential = $args[0];
}));
$this->authService->expects($this->any())
->method('authenticate')
->with($this->isInstanceOf('\\Zend\\Authentication\\Adapter\\AdapterInterface'))
->will($this->returnValue(
new Result(Result::FAILURE, null, ['Failed to login'])
));
$this->request->expects($this->any())
->method('isPost')
->will($this->returnValue(true));
$this->request->expects($this->any())
->method('getParam')
->will($this->returnCallback(function () {
$args = func_get_args();
if ($args[0] === 'identity') {
return 'theIdentity';
}
if ($args[0] === 'credential') {
return 'theCredential';
}
}));
$action = new Login(
$this->view,
$this->authService,
$adapter,
$this->flash,
$this->guard,
$this->successUrl
);
$actionResponse = $action($this->request, $this->response);
$this->assertInstanceOf('\\Psr\\Http\\Message\\ResponseInterface', $actionResponse);
$this->assertNotNull($identity);
$this->assertNotNull($credential);
$this->assertSame('theIdentity', $identity);
$this->assertSame('theCredential', $credential);
}
public function testInvokeSetsFlasMessageOnInvalidLoginAndRedirects()
{
$flashKey = null;
$flashMessage = null;
$this->request->expects($this->any())
->method('isPost')
->will($this->returnValue(true));
$this->request->expects($this->any())
->method('getUri')
->will($this->returnValue('/request/uri'));
$this->authService->expects($this->any())
->method('authenticate')
->with($this->isInstanceOf('\\Zend\\Authentication\\Adapter\\AdapterInterface'))
->will($this->returnValue(
new Result(Result::FAILURE, null, ['Failed to login'])
));
$this->flash->expects($this->any())
->method('addMessage')
->will($this->returnCallback(function () use (&$flashKey, &$flashMessage) {
$args = func_get_args();
$flashKey = $args[0];
$flashMessage = $args[1];
}));
$action = $this->action;
$actionResponse = $action($this->request, $this->response);
$this->assertInstanceOf('\\Psr\\Http\\Message\\ResponseInterface', $actionResponse);
$location = $actionResponse->getHeader('Location');
$this->assertContains('/request/uri', $location);
$this->assertSame($flashKey, 'danger');
$this->assertSame($flashMessage, 'Failed to login');
}
public function testInvokeRedirectsOnSuccessLogin()
{
$this->request->expects($this->any())
->method('isPost')
->will($this->returnValue(true));
$this->authService->expects($this->any())
->method('authenticate')
->with($this->isInstanceOf('\\Zend\\Authentication\\Adapter\\AdapterInterface'))
->will($this->returnValue(
new Result(Result::SUCCESS, 'identity', ['Success'])
));
$action = $this->action;
$actionResponse = $action($this->request, $this->response);
$this->assertInstanceOf('\\Psr\\Http\\Message\\ResponseInterface', $actionResponse);
$location = $actionResponse->getHeader('Location');
$this->assertContains($this->successUrl, $location);
}
}
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