First version of the pluggable authentication.
This commit is contained in:
parent
d901ca74ab
commit
9ac28b12b4
30
PHPCI/Security/Authentication/LoginPasswordProvider.php
Normal file
30
PHPCI/Security/Authentication/LoginPasswordProvider.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* PHPCI - Continuous Integration for PHP
|
||||
*
|
||||
* @copyright Copyright 2015, Block 8 Limited.
|
||||
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
|
||||
* @link https://www.phptesting.org/
|
||||
*/
|
||||
|
||||
namespace PHPCI\Security\Authentication;
|
||||
|
||||
use PHPCI\Model\User;
|
||||
|
||||
/**
|
||||
* User provider which authenticiation using a password.
|
||||
*
|
||||
* @author Adirelle <adirelle@gmail.com>
|
||||
*/
|
||||
interface LoginPasswordProvider extends UserProvider
|
||||
{
|
||||
/** Verify if the supplied password matches the user's one.
|
||||
*
|
||||
* @param User $user
|
||||
* @param string $password
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function verifyPassword(User $user, $password);
|
||||
}
|
111
PHPCI/Security/Authentication/Service.php
Normal file
111
PHPCI/Security/Authentication/Service.php
Normal file
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* PHPCI - Continuous Integration for PHP
|
||||
*
|
||||
* @copyright Copyright 2015, Block 8 Limited.
|
||||
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
|
||||
* @link https://www.phptesting.org/
|
||||
*/
|
||||
|
||||
namespace PHPCI\Security\Authentication;
|
||||
|
||||
use b8\Config;
|
||||
|
||||
/**
|
||||
* Authentication facade.
|
||||
*
|
||||
* @author Adirelle <adirelle@gmail.com>
|
||||
*/
|
||||
class Service
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var Service
|
||||
*/
|
||||
static private $instance;
|
||||
|
||||
/** Return the service singletion.
|
||||
*
|
||||
* @return Service
|
||||
*/
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
$config = Config::getInstance()->get(
|
||||
'phpci.security.authentication',
|
||||
array('internal' => 'internal')
|
||||
);
|
||||
|
||||
$providers = [];
|
||||
foreach ($config as $key => $providerConfig) {
|
||||
$providers[$key] = self::buildProvider($key, $providerConfig);
|
||||
}
|
||||
self::$instance = new self($providers);
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/** Create a provider from a given configuration.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string|array $config
|
||||
* @return UserProvider
|
||||
*/
|
||||
public static function buildProvider($key, $config)
|
||||
{
|
||||
if (is_string($config)) {
|
||||
$config = array('type' => $config);
|
||||
}
|
||||
|
||||
$type = $config['type'];
|
||||
if (class_exists($type)) {
|
||||
$class = $type;
|
||||
} elseif (class_exists('PHPCI\\Security\\Authentication\\UserProvider\\' . $type)) {
|
||||
$class = 'PHPCI\\Security\\Authentication\\UserProvider\\' . $type;
|
||||
} else {
|
||||
// TODO: error
|
||||
}
|
||||
|
||||
return new $class($key, $config);
|
||||
}
|
||||
|
||||
/** The table of providers.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $providers;
|
||||
|
||||
/** Initialize the service.
|
||||
*
|
||||
* @param array $providers
|
||||
*/
|
||||
public function __construct(array $providers)
|
||||
{
|
||||
$this->providers = $providers;
|
||||
}
|
||||
|
||||
/** Return all providers.
|
||||
*
|
||||
* @return UserProvider[]
|
||||
*/
|
||||
public function getProviders()
|
||||
{
|
||||
return $this->providers;
|
||||
}
|
||||
|
||||
/** Return the user providers that allows password authentication.
|
||||
*
|
||||
* @return LoginPasswordProvider[]
|
||||
*/
|
||||
public function getLoginPasswordProviders()
|
||||
{
|
||||
$providers = [];
|
||||
foreach ($this->providers as $key => $provider) {
|
||||
if ($provider instanceof LoginPasswordProvider) {
|
||||
$providers[$key] = $provider;
|
||||
}
|
||||
}
|
||||
return $providers;
|
||||
}
|
||||
}
|
36
PHPCI/Security/Authentication/UserProvider.php
Normal file
36
PHPCI/Security/Authentication/UserProvider.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* PHPCI - Continuous Integration for PHP
|
||||
*
|
||||
* @copyright Copyright 2015, Block 8 Limited.
|
||||
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
|
||||
* @link https://www.phptesting.org/
|
||||
*/
|
||||
|
||||
namespace PHPCI\Security\Authentication;
|
||||
|
||||
use PHPCI\Model\User;
|
||||
|
||||
/**
|
||||
* User provider interface.
|
||||
*
|
||||
* @author Adirelle <adirelle@gmail.com>
|
||||
*/
|
||||
interface UserProvider
|
||||
{
|
||||
|
||||
/** Check if all software requirements are met (libraries, extensions, ...)
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function checkRequirements();
|
||||
|
||||
/** Provision an new user for the given identifier.
|
||||
*
|
||||
* @param string $identifier The user identifier.
|
||||
*
|
||||
* @return User|null The new user or null if the provider does not know the user.
|
||||
*/
|
||||
public function provisionUser($identifier);
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* PHPCI - Continuous Integration for PHP
|
||||
*
|
||||
* @copyright Copyright 2014, Block 8 Limited.
|
||||
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
|
||||
* @link https://www.phptesting.org/
|
||||
*/
|
||||
|
||||
namespace PHPCI\Security\Authentication\UserProvider;
|
||||
|
||||
use PHPCI\Security\Authentication\UserProvider;
|
||||
|
||||
/**
|
||||
* Abstract user provider.
|
||||
*
|
||||
* @author Adirelle <adirelle@gmail.com>
|
||||
*/
|
||||
abstract class AbstractProvider implements UserProvider
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $key;
|
||||
|
||||
public function __construct($key)
|
||||
{
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getKey()
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
}
|
37
PHPCI/Security/Authentication/UserProvider/Internal.php
Normal file
37
PHPCI/Security/Authentication/UserProvider/Internal.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* PHPCI - Continuous Integration for PHP
|
||||
*
|
||||
* @copyright Copyright 2014, Block 8 Limited.
|
||||
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
|
||||
* @link https://www.phptesting.org/
|
||||
*/
|
||||
|
||||
namespace PHPCI\Security\Authentication\UserProvider;
|
||||
|
||||
use PHPCI\Model\User;
|
||||
use PHPCI\Security\Authentication\LoginPasswordProvider;
|
||||
|
||||
/**
|
||||
* Internal user provider.
|
||||
* @author Adirelle <adirelle@gmail.com>
|
||||
*/
|
||||
class Internal extends AbstractProvider implements LoginPasswordProvider
|
||||
{
|
||||
|
||||
public function verifyPassword(User $user, $password)
|
||||
{
|
||||
return password_verify($password, $user->getHash());
|
||||
}
|
||||
|
||||
public function checkRequirements()
|
||||
{
|
||||
// Always fine
|
||||
}
|
||||
|
||||
public function provisionUser($identifier)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
89
Tests/PHPCI/Security/Authentication/ServiceTest.php
Normal file
89
Tests/PHPCI/Security/Authentication/ServiceTest.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* PHPCI - Continuous Integration for PHP
|
||||
*
|
||||
* @copyright Copyright 2014, Block 8 Limited.
|
||||
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
|
||||
* @link https://www.phptesting.org/
|
||||
*/
|
||||
|
||||
namespace PHPCI\Security\Authentication\Tests;
|
||||
|
||||
use PHPCI\Security\Authentication\Service;
|
||||
use PHPUnit_Framework_TestCase;
|
||||
|
||||
class ServiceTest extends \Prophecy\PhpUnit\ProphecyTestCase
|
||||
{
|
||||
/**
|
||||
* @covers PHPCI\Security\Authentication\Service::getInstance
|
||||
* @todo Implement testGetInstance().
|
||||
*/
|
||||
public function testGetInstance()
|
||||
{
|
||||
$this->assertInstanceOf('PHPCI\Security\Authentication\Service', Service::getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers PHPCI\Security\Authentication\Service::buildProvider
|
||||
*/
|
||||
public function testBuildBuiltinProvider()
|
||||
{
|
||||
$provider = Service::buildProvider("test", array('type' => 'internal'));
|
||||
|
||||
$this->assertInstanceOf('PHPCI\Security\Authentication\UserProvider\Internal', $provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers PHPCI\Security\Authentication\Service::buildProvider
|
||||
*/
|
||||
public function testBuildAnyProvider()
|
||||
{
|
||||
$config = array('type' => 'PHPCI\Security\Authentication\Tests\DummyProvider');
|
||||
$provider = Service::buildProvider("test", $config);
|
||||
|
||||
$this->assertInstanceOf('PHPCI\Security\Authentication\Tests\DummyProvider', $provider);
|
||||
$this->assertEquals('test', $provider->key);
|
||||
$this->assertEquals($config, $provider->config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers PHPCI\Security\Authentication\Service::getProviders
|
||||
*/
|
||||
public function testGetProviders()
|
||||
{
|
||||
$a = $this->prophesize('PHPCI\Security\Authentication\UserProvider')->reveal();
|
||||
$b = $this->prophesize('PHPCI\Security\Authentication\UserProvider')->reveal();
|
||||
$providers = array('a' => $a, 'b' => $b);
|
||||
|
||||
$service = new Service($providers);
|
||||
|
||||
$this->assertEquals($providers, $service->getProviders());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers PHPCI\Security\Authentication\Service::getLoginPasswordProviders
|
||||
* @todo Implement testGetLoginPasswordProviders().
|
||||
*/
|
||||
public function testGetLoginPasswordProviders()
|
||||
{
|
||||
$a = $this->prophesize('PHPCI\Security\Authentication\UserProvider')->reveal();
|
||||
$b = $this->prophesize('PHPCI\Security\Authentication\LoginPasswordProvider')->reveal();
|
||||
$providers = array('a' => $a, 'b' => $b);
|
||||
|
||||
$service = new Service($providers);
|
||||
|
||||
$this->assertEquals(array('b' => $b), $service->getLoginPasswordProviders());
|
||||
}
|
||||
}
|
||||
|
||||
class DummyProvider
|
||||
{
|
||||
public $key;
|
||||
public $config;
|
||||
public function __construct($key, array $config)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->config = $config;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* PHPCI - Continuous Integration for PHP
|
||||
*
|
||||
* @copyright Copyright 2014, Block 8 Limited.
|
||||
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
|
||||
* @link https://www.phptesting.org/
|
||||
*/
|
||||
|
||||
namespace PHPCI\Security\Authentication\UserProvider\Tests;
|
||||
|
||||
use PHPCI\Security\Authentication\UserProvider\Internal;
|
||||
use PHPUnit_Framework_TestCase;
|
||||
|
||||
/**
|
||||
* Generated by PHPUnit_SkeletonGenerator on 2015-03-08 at 18:26:51.
|
||||
*/
|
||||
class InternalTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
/**
|
||||
* @var Internal
|
||||
*/
|
||||
protected $provider;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
$this->provider = new Internal("internal");
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers PHPCI\Security\Authentication\UserProvider\Internal::verifyPassword
|
||||
*/
|
||||
public function testVerifyPassword()
|
||||
{
|
||||
$user = new \PHPCI\Model\User;
|
||||
$password = 'bla';
|
||||
$user->setHash(password_hash($password, PASSWORD_DEFAULT));
|
||||
|
||||
$this->assertTrue($this->provider->verifyPassword($user, $password));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers PHPCI\Security\Authentication\UserProvider\Internal::verifyPassword
|
||||
*/
|
||||
public function testVerifyInvaldPassword()
|
||||
{
|
||||
$user = new \PHPCI\Model\User;
|
||||
$password = 'foo';
|
||||
$user->setHash(password_hash($password, PASSWORD_DEFAULT));
|
||||
|
||||
$this->assertFalse($this->provider->verifyPassword($user, 'bar'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers PHPCI\Security\Authentication\UserProvider\Internal::checkRequirements
|
||||
*/
|
||||
public function testCheckRequirements()
|
||||
{
|
||||
$this->provider->checkRequirements();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers PHPCI\Security\Authentication\UserProvider\Internal::provisionUser
|
||||
*/
|
||||
public function testProvisionUser()
|
||||
{
|
||||
$this->assertNull($this->provider->provisionUser('john@doe.com'));
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* PHPCI - Continuous Integration for PHP
|
||||
*
|
||||
* @copyright Copyright 2014, Block 8 Limited.
|
||||
* @copyright Copyright 2015, Block 8 Limited.
|
||||
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
|
||||
* @link https://www.phptesting.org/
|
||||
*/
|
||||
|
@ -17,6 +17,7 @@ use PHPCensor\Controller;
|
|||
|
||||
/**
|
||||
* Session Controller - Handles user login / logout.
|
||||
*
|
||||
* @author Dan Cryer <dan@block8.co.uk>
|
||||
* @package PHPCI
|
||||
* @subpackage Web
|
||||
|
@ -28,6 +29,11 @@ class SessionController extends Controller
|
|||
*/
|
||||
protected $userStore;
|
||||
|
||||
/**
|
||||
* @var \PHPCI\Security\Authentication\Service
|
||||
*/
|
||||
protected $authentication;
|
||||
|
||||
/**
|
||||
* Initialise the controller, set up stores and services.
|
||||
*/
|
||||
|
@ -35,11 +41,12 @@ class SessionController extends Controller
|
|||
{
|
||||
$this->response->disableLayout();
|
||||
$this->userStore = b8\Store\Factory::getStore('User');
|
||||
$this->authentication = \PHPCI\Security\Authentication\Service::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user login (form and processing)
|
||||
*/
|
||||
* Handles user login (form and processing)
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
$isLoginFailure = false;
|
||||
|
@ -51,23 +58,42 @@ class SessionController extends Controller
|
|||
} else {
|
||||
unset($_SESSION['login_token']);
|
||||
|
||||
$user = $this->userStore->getByEmailOrName($this->getParam('email'));
|
||||
$email = $this->getParam('email');
|
||||
$password = $this->getParam('password', '');
|
||||
$isLoginFailure = true;
|
||||
|
||||
if ($user && password_verify($this->getParam('password', ''), $user->getHash())) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['php-censor-user-id'] = $user->getId();
|
||||
$user = $this->userStore->getByEmailOrName($email);
|
||||
|
||||
$providers = $this->authentication->getLoginPasswordProviders();
|
||||
|
||||
if (null !== $user) {
|
||||
// Delegate password verification to the user provider, if found
|
||||
$key = $user->getProviderKey();
|
||||
$isLoginFailure = !isset($providers[$key]) || !$providers[$key]->verifyPassword($user, $password);
|
||||
} else {
|
||||
// Ask each providers to provision the user
|
||||
foreach ($providers as $provider) {
|
||||
$user = $provider->provisionUser($email);
|
||||
if ($user !== null && $provider->verifyPassword($user, $password)) {
|
||||
$this->userStore->save($user);
|
||||
$isLoginFailure = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isLoginFailure) {
|
||||
$_SESSION['php-censor-user-id'] = $user->getId();
|
||||
$response = new b8\Http\Response\RedirectResponse();
|
||||
$response->setHeader('Location', $this->getLoginRedirect());
|
||||
return $response;
|
||||
} else {
|
||||
$isLoginFailure = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$form = new b8\Form();
|
||||
$form->setMethod('POST');
|
||||
$form->setAction(APP_URL.'session/login');
|
||||
$form->setAction(APP_URL . 'session/login');
|
||||
|
||||
$email = new b8\Form\Element\Text('email');
|
||||
$email->setLabel(Lang::get('login'));
|
||||
|
|
Loading…
Reference in a new issue