Authentication backends refactoring

This commit is contained in:
Frédéric Guillot
2014-08-16 13:59:37 -07:00
parent 498408d507
commit 925b0ba2e5
20 changed files with 719 additions and 455 deletions

60
app/Auth/Base.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
namespace Auth;
use Core\Tool;
use Core\Registry;
use PicoDb\Database;
/**
* Base auth class
*
* @package auth
* @author Frederic Guillot
*
* @property \Model\Acl $acl
* @property \Model\LastLogin $lastLogin
* @property \Model\User $user
*/
abstract class Base
{
/**
* Database instance
*
* @access protected
* @var \PicoDb\Database
*/
protected $db;
/**
* Registry instance
*
* @access protected
* @var \Core\Registry
*/
protected $registry;
/**
* Constructor
*
* @access public
* @param \Core\Registry $registry Registry instance
*/
public function __construct(Registry $registry)
{
$this->registry = $registry;
$this->db = $this->registry->shared('db');
}
/**
* Load automatically models
*
* @access public
* @param string $name Model name
* @return mixed
*/
public function __get($name)
{
return Tool::loadModel($this->registry, $name);
}
}

52
app/Auth/Database.php Normal file
View File

@@ -0,0 +1,52 @@
<?php
namespace Auth;
use Model\User;
/**
* Database authentication
*
* @package auth
* @author Frederic Guillot
*/
class Database extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'Database';
/**
* Authenticate a user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return boolean
*/
public function authenticate($username, $password)
{
$user = $this->db->table(User::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne();
if ($user && password_verify($password, $user['password'])) {
// Update user session
$this->user->updateSession($user);
// Update login history
$this->lastLogin->create(
self::AUTH_NAME,
$user['id'],
$this->user->getIpAddress(),
$this->user->getUserAgent()
);
return true;
}
return false;
}
}

View File

@@ -1,171 +1,178 @@
<?php <?php
namespace Model; namespace Auth;
require __DIR__.'/../../vendor/OAuth/bootstrap.php'; require __DIR__.'/../../vendor/OAuth/bootstrap.php';
use OAuth\Common\Storage\Session; use OAuth\Common\Storage\Session;
use OAuth\Common\Consumer\Credentials; use OAuth\Common\Consumer\Credentials;
use OAuth\Common\Http\Uri\UriFactory; use OAuth\Common\Http\Uri\UriFactory;
use OAuth\ServiceFactory; use OAuth\ServiceFactory;
use OAuth\Common\Http\Exception\TokenResponseException; use OAuth\Common\Http\Exception\TokenResponseException;
/** /**
* GitHub model * GitHub backend
* *
* @package model * @package auth
*/ */
class GitHub extends Base class GitHub extends Base
{ {
/** /**
* Authenticate a GitHub user * Backend name
* *
* @access public * @var string
* @param string $github_id GitHub user id */
* @return boolean const AUTH_NAME = 'Github';
*/
public function authenticate($github_id) /**
{ * Authenticate a GitHub user
$user = $this->user->getByGitHubId($github_id); *
* @access public
if ($user) { * @param string $github_id GitHub user id
* @return boolean
// Create the user session */
$this->user->updateSession($user); public function authenticate($github_id)
{
// Update login history $user = $this->user->getByGitHubId($github_id);
$this->lastLogin->create(
LastLogin::AUTH_GITHUB, if ($user) {
$user['id'],
$this->user->getIpAddress(), // Create the user session
$this->user->getUserAgent() $this->user->updateSession($user);
);
// Update login history
return true; $this->lastLogin->create(
} self::AUTH_NAME,
$user['id'],
return false; $this->user->getIpAddress(),
} $this->user->getUserAgent()
);
/**
* Unlink a GitHub account for a given user return true;
* }
* @access public
* @param integer $user_id User id return false;
* @return boolean }
*/
public function unlink($user_id) /**
{ * Unlink a GitHub account for a given user
return $this->user->update(array( *
'id' => $user_id, * @access public
'github_id' => '', * @param integer $user_id User id
)); * @return boolean
} */
public function unlink($user_id)
/** {
* Update the user table based on the GitHub profile information return $this->user->update(array(
* 'id' => $user_id,
* @access public 'github_id' => '',
* @param integer $user_id User id ));
* @param array $profile GitHub profile }
* @return boolean
* @todo Don't overwrite existing email/name with empty GitHub data /**
*/ * Update the user table based on the GitHub profile information
public function updateUser($user_id, array $profile) *
{ * @access public
return $this->user->update(array( * @param integer $user_id User id
'id' => $user_id, * @param array $profile GitHub profile
'github_id' => $profile['id'], * @return boolean
'email' => $profile['email'], * @todo Don't overwrite existing email/name with empty GitHub data
'name' => $profile['name'], */
)); public function updateUser($user_id, array $profile)
} {
return $this->user->update(array(
/** 'id' => $user_id,
* Get the GitHub service instance 'github_id' => $profile['id'],
* 'email' => $profile['email'],
* @access public 'name' => $profile['name'],
* @return \OAuth\OAuth2\Service\GitHub ));
*/ }
public function getService()
{ /**
$uriFactory = new UriFactory(); * Get the GitHub service instance
$currentUri = $uriFactory->createFromSuperGlobalArray($_SERVER); *
$currentUri->setQuery('controller=user&action=gitHub'); * @access public
* @return \OAuth\OAuth2\Service\GitHub
$storage = new Session(false); */
public function getService()
$credentials = new Credentials( {
GITHUB_CLIENT_ID, $uriFactory = new UriFactory();
GITHUB_CLIENT_SECRET, $currentUri = $uriFactory->createFromSuperGlobalArray($_SERVER);
$currentUri->getAbsoluteUri() $currentUri->setQuery('controller=user&action=gitHub');
);
$storage = new Session(false);
$serviceFactory = new ServiceFactory();
$credentials = new Credentials(
return $serviceFactory->createService( GITHUB_CLIENT_ID,
'gitHub', GITHUB_CLIENT_SECRET,
$credentials, $currentUri->getAbsoluteUri()
$storage, );
array('')
); $serviceFactory = new ServiceFactory();
}
return $serviceFactory->createService(
/** 'gitHub',
* Get the authorization URL $credentials,
* $storage,
* @access public array('')
* @return \OAuth\Common\Http\Uri\Uri );
*/ }
public function getAuthorizationUrl()
{ /**
return $this->getService()->getAuthorizationUri(); * Get the authorization URL
} *
* @access public
/** * @return \OAuth\Common\Http\Uri\Uri
* Get GitHub profile information from the API */
* public function getAuthorizationUrl()
* @access public {
* @param string $code GitHub authorization code return $this->getService()->getAuthorizationUri();
* @return bool|array }
*/
public function getGitHubProfile($code) /**
{ * Get GitHub profile information from the API
try { *
$gitHubService = $this->getService(); * @access public
$gitHubService->requestAccessToken($code); * @param string $code GitHub authorization code
* @return bool|array
return json_decode($gitHubService->request('user'), true); */
} public function getGitHubProfile($code)
catch (TokenResponseException $e) { {
return false; try {
} $gitHubService = $this->getService();
$gitHubService->requestAccessToken($code);
return false;
} return json_decode($gitHubService->request('user'), true);
}
/** catch (TokenResponseException $e) {
* Revokes this user's GitHub tokens for Kanboard return false;
* }
* @access public
* @return bool|array return false;
* @todo Currently this simply removes all our tokens for this user, ideally it should }
* restrict itself to the one in question
*/ /**
public function revokeGitHubAccess() * Revokes this user's GitHub tokens for Kanboard
{ *
try { * @access public
$gitHubService = $this->getService(); * @return bool|array
* @todo Currently this simply removes all our tokens for this user, ideally it should
$basicAuthHeader = array('Authorization' => 'Basic ' . * restrict itself to the one in question
base64_encode(GITHUB_CLIENT_ID.':'.GITHUB_CLIENT_SECRET)); */
public function revokeGitHubAccess()
return json_decode($gitHubService->request('/applications/'.GITHUB_CLIENT_ID.'/tokens', 'DELETE', null, $basicAuthHeader), true); {
} try {
catch (TokenResponseException $e) { $gitHubService = $this->getService();
return false;
} $basicAuthHeader = array('Authorization' => 'Basic ' .
base64_encode(GITHUB_CLIENT_ID.':'.GITHUB_CLIENT_SECRET));
return false;
} return json_decode($gitHubService->request('/applications/'.GITHUB_CLIENT_ID.'/tokens', 'DELETE', null, $basicAuthHeader), true);
} }
catch (TokenResponseException $e) {
return false;
}
return false;
}
}

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace Model; namespace Auth;
require __DIR__.'/../../vendor/OAuth/bootstrap.php'; require __DIR__.'/../../vendor/OAuth/bootstrap.php';
@@ -11,13 +11,20 @@ use OAuth\ServiceFactory;
use OAuth\Common\Http\Exception\TokenResponseException; use OAuth\Common\Http\Exception\TokenResponseException;
/** /**
* Google model * Google backend
* *
* @package model * @package auth
* @author Frederic Guillot * @author Frederic Guillot
*/ */
class Google extends Base class Google extends Base
{ {
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'Google';
/** /**
* Authenticate a Google user * Authenticate a Google user
* *
@@ -36,7 +43,7 @@ class Google extends Base
// Update login history // Update login history
$this->lastLogin->create( $this->lastLogin->create(
LastLogin::AUTH_GOOGLE, self::AUTH_NAME,
$user['id'], $user['id'],
$this->user->getIpAddress(), $this->user->getIpAddress(),
$this->user->getUserAgent() $this->user->getUserAgent()

150
app/Auth/Ldap.php Normal file
View File

@@ -0,0 +1,150 @@
<?php
namespace Auth;
/**
* LDAP model
*
* @package auth
* @author Frederic Guillot
*/
class Ldap extends Base
{
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'LDAP';
/**
* Authenticate the user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return boolean
*/
public function authenticate($username, $password)
{
$result = $this->findUser($username, $password);
if (is_array($result)) {
$user = $this->user->getByUsername($username);
if ($user) {
// There is already a local user with that name
if ($user['is_ldap_user'] == 0) {
return false;
}
}
else {
// We create automatically a new user
if ($this->createUser($username, $result['name'], $result['email'])) {
$user = $this->user->getByUsername($username);
}
else {
return false;
}
}
// We open the session
$this->user->updateSession($user);
// Update login history
$this->lastLogin->create(
self::AUTH_NAME,
$user['id'],
$this->user->getIpAddress(),
$this->user->getUserAgent()
);
return true;
}
return false;
}
/**
* Find the user from the LDAP server
*
* @access public
* @param string $username Username
* @param string $password Password
* @return boolean|array
*/
public function findUser($username, $password)
{
if (! function_exists('ldap_connect')) {
die('The PHP LDAP extension is required');
}
// Skip SSL certificate verification
if (! LDAP_SSL_VERIFY) {
putenv('LDAPTLS_REQCERT=never');
}
$ldap = ldap_connect(LDAP_SERVER, LDAP_PORT);
if (! is_resource($ldap)) {
die('Unable to connect to the LDAP server: "'.LDAP_SERVER.'"');
}
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
if (! @ldap_bind($ldap, LDAP_USERNAME, LDAP_PASSWORD)) {
die('Unable to bind to the LDAP server: "'.LDAP_SERVER.'"');
}
$sr = @ldap_search($ldap, LDAP_ACCOUNT_BASE, sprintf(LDAP_USER_PATTERN, $username), array(LDAP_ACCOUNT_FULLNAME, LDAP_ACCOUNT_EMAIL));
if ($sr === false) {
return false;
}
$info = ldap_get_entries($ldap, $sr);
// User not found
if (count($info) == 0 || $info['count'] == 0) {
return false;
}
// We got our user
if (@ldap_bind($ldap, $info[0]['dn'], $password)) {
return array(
'username' => $username,
'name' => isset($info[0][LDAP_ACCOUNT_FULLNAME][0]) ? $info[0][LDAP_ACCOUNT_FULLNAME][0] : '',
'email' => isset($info[0][LDAP_ACCOUNT_EMAIL][0]) ? $info[0][LDAP_ACCOUNT_EMAIL][0] : '',
);
}
return false;
}
/**
* Create a new local user after the LDAP authentication
*
* @access public
* @param string $username Username
* @param string $name Name of the user
* @param string $email Email address
* @return bool
*/
public function createUser($username, $name, $email)
{
$values = array(
'username' => $username,
'name' => $name,
'email' => $email,
'is_admin' => 0,
'is_ldap_user' => 1,
);
return $this->user->create($values);
}
}

View File

@@ -1,17 +1,24 @@
<?php <?php
namespace Model; namespace Auth;
use Core\Security; use Core\Security;
/** /**
* RememberMe model * RememberMe model
* *
* @package model * @package auth
* @author Frederic Guillot * @author Frederic Guillot
*/ */
class RememberMe extends Base class RememberMe extends Base
{ {
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'RememberMe';
/** /**
* SQL table name * SQL table name
* *
@@ -95,6 +102,14 @@ class RememberMe extends Base
$this->user->updateSession($this->user->getById($record['user_id'])); $this->user->updateSession($this->user->getById($record['user_id']));
$this->acl->isRememberMe(true); $this->acl->isRememberMe(true);
// Update last login infos
$this->lastLogin->create(
self::AUTH_NAME,
$this->acl->getUserId(),
$this->user->getIpAddress(),
$this->user->getUserAgent()
);
return true; return true;
} }
} }

View File

@@ -1,17 +1,24 @@
<?php <?php
namespace Model; namespace Auth;
use Core\Security; use Core\Security;
/** /**
* ReverseProxyAuth model * ReverseProxy backend
* *
* @package model * @package auth
* @author Sylvain Veyrié * @author Sylvain Veyrié
*/ */
class ReverseProxyAuth extends Base class ReverseProxy extends Base
{ {
/**
* Backend name
*
* @var string
*/
const AUTH_NAME = 'ReverseProxy';
/** /**
* Authenticate the user with the HTTP header * Authenticate the user with the HTTP header
* *
@@ -35,7 +42,7 @@ class ReverseProxyAuth extends Base
// Update login history // Update login history
$this->lastLogin->create( $this->lastLogin->create(
LastLogin::AUTH_REVERSE_PROXY, self::AUTH_NAME,
$user['id'], $user['id'],
$this->user->getIpAddress(), $this->user->getIpAddress(),
$this->user->getUserAgent() $this->user->getUserAgent()

View File

@@ -15,20 +15,16 @@ use Model\LastLogin;
* @author Frederic Guillot * @author Frederic Guillot
* *
* @property \Model\Acl $acl * @property \Model\Acl $acl
* @property \Model\Authentication $authentication
* @property \Model\Action $action * @property \Model\Action $action
* @property \Model\Board $board * @property \Model\Board $board
* @property \Model\Category $category * @property \Model\Category $category
* @property \Model\Comment $comment * @property \Model\Comment $comment
* @property \Model\Config $config * @property \Model\Config $config
* @property \Model\File $file * @property \Model\File $file
* @property \Model\Google $google
* @property \Model\GitHub $gitHub
* @property \Model\LastLogin $lastLogin * @property \Model\LastLogin $lastLogin
* @property \Model\Ldap $ldap
* @property \Model\Notification $notification * @property \Model\Notification $notification
* @property \Model\Project $project * @property \Model\Project $project
* @property \Model\RememberMe $rememberMe
* @property \Model\ReverseProxyAuth $reverseProxyAuth
* @property \Model\SubTask $subTask * @property \Model\SubTask $subTask
* @property \Model\Task $task * @property \Model\Task $task
* @property \Model\User $user * @property \Model\User $user
@@ -123,29 +119,8 @@ abstract class Base
date_default_timezone_set($this->config->get('timezone', 'UTC')); date_default_timezone_set($this->config->get('timezone', 'UTC'));
// Authentication // Authentication
if (! $this->acl->isLogged() && ! $this->acl->isPublicAction($controller, $action)) { if (! $this->authentication->isAuthenticated($controller, $action)) {
$this->response->redirect('?controller=user&action=login');
// Try the "remember me" authentication first
if (! $this->rememberMe->authenticate()) {
// Automatic reverse proxy header authentication
if(! (REVERSE_PROXY_AUTH && $this->reverseProxyAuth->authenticate()) ) {
// Redirect to the login form if not authenticated
$this->response->redirect('?controller=user&action=login');
}
}
else {
$this->lastLogin->create(
LastLogin::AUTH_REMEMBER_ME,
$this->acl->getUserId(),
$this->user->getIpAddress(),
$this->user->getUserAgent()
);
}
}
else if ($this->rememberMe->hasCookie()) {
$this->rememberMe->refresh();
} }
// Check if the user is allowed to see this page // Check if the user is allowed to see this page

View File

@@ -28,7 +28,7 @@ class Config extends Base
'menu' => 'config', 'menu' => 'config',
'title' => t('Settings'), 'title' => t('Settings'),
'timezones' => $this->config->getTimezones(), 'timezones' => $this->config->getTimezones(),
'remember_me_sessions' => $this->rememberMe->getAll($this->acl->getUserId()), 'remember_me_sessions' => $this->authentication->backend('rememberMe')->getAll($this->acl->getUserId()),
'last_logins' => $this->lastLogin->getAll($this->acl->getUserId()), 'last_logins' => $this->lastLogin->getAll($this->acl->getUserId()),
))); )));
} }
@@ -73,7 +73,7 @@ class Config extends Base
'menu' => 'config', 'menu' => 'config',
'title' => t('Settings'), 'title' => t('Settings'),
'timezones' => $this->config->getTimezones(), 'timezones' => $this->config->getTimezones(),
'remember_me_sessions' => $this->rememberMe->getAll($this->acl->getUserId()), 'remember_me_sessions' => $this->authentication->backend('rememberMe')->getAll($this->acl->getUserId()),
'last_logins' => $this->lastLogin->getAll($this->acl->getUserId()), 'last_logins' => $this->lastLogin->getAll($this->acl->getUserId()),
))); )));
} }
@@ -124,7 +124,7 @@ class Config extends Base
public function removeRememberMeToken() public function removeRememberMeToken()
{ {
$this->checkCSRFParam(); $this->checkCSRFParam();
$this->rememberMe->remove($this->request->getIntegerParam('id')); $this->authentication->backend('rememberMe')->remove($this->request->getIntegerParam('id'));
$this->response->redirect('?controller=config&action=index#remember-me'); $this->response->redirect('?controller=config&action=index#remember-me');
} }
} }

View File

@@ -18,7 +18,7 @@ class User extends Base
public function logout() public function logout()
{ {
$this->checkCSRFParam(); $this->checkCSRFParam();
$this->rememberMe->destroy($this->acl->getUserId()); $this->authentication->backend('rememberMe')->destroy($this->acl->getUserId());
$this->session->close(); $this->session->close();
$this->response->redirect('?controller=user&action=login'); $this->response->redirect('?controller=user&action=login');
} }
@@ -30,7 +30,7 @@ class User extends Base
*/ */
public function login() public function login()
{ {
if (isset($_SESSION['user'])) { if ($this->acl->isLogged()) {
$this->response->redirect('?controller=app'); $this->response->redirect('?controller=app');
} }
@@ -50,7 +50,7 @@ class User extends Base
public function check() public function check()
{ {
$values = $this->request->getValues(); $values = $this->request->getValues();
list($valid, $errors) = $this->user->validateLogin($values); list($valid, $errors) = $this->authentication->validateForm($values);
if ($valid) { if ($valid) {
$this->response->redirect('?controller=app'); $this->response->redirect('?controller=app');
@@ -249,14 +249,14 @@ class User extends Base
if ($code) { if ($code) {
$profile = $this->google->getGoogleProfile($code); $profile = $this->authentication->backend('google')->getGoogleProfile($code);
if (is_array($profile)) { if (is_array($profile)) {
// If the user is already logged, link the account otherwise authenticate // If the user is already logged, link the account otherwise authenticate
if ($this->acl->isLogged()) { if ($this->acl->isLogged()) {
if ($this->google->updateUser($this->acl->getUserId(), $profile)) { if ($this->authentication->backend('google')->updateUser($this->acl->getUserId(), $profile)) {
$this->session->flash(t('Your Google Account is linked to your profile successfully.')); $this->session->flash(t('Your Google Account is linked to your profile successfully.'));
} }
else { else {
@@ -265,7 +265,7 @@ class User extends Base
$this->response->redirect('?controller=user'); $this->response->redirect('?controller=user');
} }
else if ($this->google->authenticate($profile['id'])) { else if ($this->authentication->backend('google')->authenticate($profile['id'])) {
$this->response->redirect('?controller=app'); $this->response->redirect('?controller=app');
} }
else { else {
@@ -279,7 +279,7 @@ class User extends Base
} }
} }
$this->response->redirect($this->google->getAuthorizationUrl()); $this->response->redirect($this->authentication->backend('google')->getAuthorizationUrl());
} }
/** /**
@@ -290,7 +290,7 @@ class User extends Base
public function unlinkGoogle() public function unlinkGoogle()
{ {
$this->checkCSRFParam(); $this->checkCSRFParam();
if ($this->google->unlink($this->acl->getUserId())) { if ($this->authentication->backend('google')->unlink($this->acl->getUserId())) {
$this->session->flash(t('Your Google Account is not linked anymore to your profile.')); $this->session->flash(t('Your Google Account is not linked anymore to your profile.'));
} }
else { else {
@@ -310,14 +310,14 @@ class User extends Base
$code = $this->request->getStringParam('code'); $code = $this->request->getStringParam('code');
if ($code) { if ($code) {
$profile = $this->gitHub->getGitHubProfile($code); $profile = $this->authentication->backend('gitHub')->getGitHubProfile($code);
if (is_array($profile)) { if (is_array($profile)) {
// If the user is already logged, link the account otherwise authenticate // If the user is already logged, link the account otherwise authenticate
if ($this->acl->isLogged()) { if ($this->acl->isLogged()) {
if ($this->gitHub->updateUser($this->acl->getUserId(), $profile)) { if ($this->authentication->backend('gitHub')->updateUser($this->acl->getUserId(), $profile)) {
$this->session->flash(t('Your GitHub account was successfully linked to your profile.')); $this->session->flash(t('Your GitHub account was successfully linked to your profile.'));
} }
else { else {
@@ -326,7 +326,7 @@ class User extends Base
$this->response->redirect('?controller=user'); $this->response->redirect('?controller=user');
} }
else if ($this->gitHub->authenticate($profile['id'])) { else if ($this->authentication->backend('gitHub')->authenticate($profile['id'])) {
$this->response->redirect('?controller=app'); $this->response->redirect('?controller=app');
} }
else { else {
@@ -340,7 +340,7 @@ class User extends Base
} }
} }
$this->response->redirect($this->gitHub->getAuthorizationUrl()); $this->response->redirect($this->authentication->backend('gitHub')->getAuthorizationUrl());
} }
/** /**
@@ -352,9 +352,9 @@ class User extends Base
{ {
$this->checkCSRFParam(); $this->checkCSRFParam();
$this->gitHub->revokeGitHubAccess(); $this->authentication->backend('gitHub')->revokeGitHubAccess();
if ($this->gitHub->unlink($this->acl->getUserId())) { if ($this->authentication->backend('gitHub')->unlink($this->acl->getUserId())) {
$this->session->flash(t('Your GitHub account is no longer linked to your profile.')); $this->session->flash(t('Your GitHub account is no longer linked to your profile.'));
} }
else { else {

View File

@@ -34,8 +34,11 @@ class Tool
public static function loadModel(Registry $registry, $name) public static function loadModel(Registry $registry, $name)
{ {
$class = '\Model\\'.ucfirst($name); if (! isset($registry->$name)) {
$registry->$name = new $class($registry); $class = '\Model\\'.ucfirst($name);
$registry->$name = new $class($registry);
}
return $registry->shared($name); return $registry->shared($name);
} }
} }

View File

@@ -0,0 +1,125 @@
<?php
namespace Model;
use Auth\Database;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Authentication model
*
* @package model
* @author Frederic Guillot
*/
class Authentication extends Base
{
/**
* Load automatically an authentication backend
*
* @access public
* @param string $name Backend class name
* @return mixed
*/
public function backend($name)
{
if (! isset($this->registry->$name)) {
$class = '\Auth\\'.ucfirst($name);
$this->registry->$name = new $class($this->registry);
}
return $this->registry->shared($name);
}
/**
* Check if the current user is authenticated
*
* @access public
* @param string $controller Controller
* @param string $action Action name
* @return bool
*/
public function isAuthenticated($controller, $action)
{
// If the action is public we don't need to do any checks
if ($this->acl->isPublicAction($controller, $action)) {
return true;
}
// If the user is already logged it's ok
if ($this->acl->isLogged()) {
// We update each time the RememberMe cookie tokens
if ($this->backend('rememberMe')->hasCookie()) {
$this->backend('rememberMe')->refresh();
}
return true;
}
// We try first with the RememberMe cookie
if ($this->backend('rememberMe')->authenticate()) {
return true;
}
// Then with the ReverseProxy authentication
if (REVERSE_PROXY_AUTH && $this->backend('reverseProxy')->authenticate()) {
return true;
}
return false;
}
/**
* Validate user login form
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateForm(array $values)
{
$v = new Validator($values, array(
new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Required('password', t('The password is required')),
));
$result = $v->execute();
$errors = $v->getErrors();
if ($result) {
$authenticated = false;
// Try first the database auth and then LDAP if activated
if ($this->backend('database')->authenticate($values['username'], $values['password'])) {
$authenticated = true;
}
else if (LDAP_AUTH && $this->backend('ldap')->authenticate($values['username'], $values['password'])) {
$authenticated = true;
}
if ($authenticated) {
// Setup the remember me feature
if (! empty($values['remember_me'])) {
$credentials = $this->backend('rememberMe')
->create($this->acl->getUserId(), $this->user->getIpAddress(), $this->user->getUserAgent());
$this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
}
}
else {
$result = false;
$errors['login'] = t('Bad username or password');
}
}
return array(
$result,
$errors
);
}
}

View File

@@ -2,20 +2,6 @@
namespace Model; namespace Model;
require __DIR__.'/../../vendor/SimpleValidator/Validator.php';
require __DIR__.'/../../vendor/SimpleValidator/Base.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Required.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Unique.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/MaxLength.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/MinLength.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Integer.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Equals.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/AlphaNumeric.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/GreaterThan.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Date.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Email.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Numeric.php';
use Core\Event; use Core\Event;
use Core\Tool; use Core\Tool;
use Core\Registry; use Core\Registry;
@@ -35,7 +21,6 @@ use PicoDb\Database;
* @property \Model\Config $config * @property \Model\Config $config
* @property \Model\File $file * @property \Model\File $file
* @property \Model\LastLogin $lastLogin * @property \Model\LastLogin $lastLogin
* @property \Model\Ldap $ldap
* @property \Model\Notification $notification * @property \Model\Notification $notification
* @property \Model\Project $project * @property \Model\Project $project
* @property \Model\SubTask $subTask * @property \Model\SubTask $subTask

View File

@@ -24,18 +24,6 @@ class LastLogin extends Base
*/ */
const NB_LOGINS = 10; const NB_LOGINS = 10;
/**
* Authentication methods
*
* @var string
*/
const AUTH_DATABASE = 'database';
const AUTH_REMEMBER_ME = 'remember_me';
const AUTH_LDAP = 'ldap';
const AUTH_GOOGLE = 'google';
const AUTH_GITHUB = 'github';
const AUTH_REVERSE_PROXY = 'reverse_proxy';
/** /**
* Create a new record * Create a new record
* *

View File

@@ -1,104 +0,0 @@
<?php
namespace Model;
/**
* LDAP model
*
* @package model
* @author Frederic Guillot
*/
class Ldap extends Base
{
/**
* Authenticate a user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return null|boolean
*/
public function authenticate($username, $password)
{
if (! function_exists('ldap_connect')) {
die('The PHP LDAP extension is required');
}
// Skip SSL certificate verification
if (! LDAP_SSL_VERIFY) {
putenv('LDAPTLS_REQCERT=never');
}
$ldap = ldap_connect(LDAP_SERVER, LDAP_PORT);
if (! is_resource($ldap)) {
die('Unable to connect to the LDAP server: "'.LDAP_SERVER.'"');
}
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
if (! @ldap_bind($ldap, LDAP_USERNAME, LDAP_PASSWORD)) {
die('Unable to bind to the LDAP server: "'.LDAP_SERVER.'"');
}
$sr = @ldap_search($ldap, LDAP_ACCOUNT_BASE, sprintf(LDAP_USER_PATTERN, $username), array(LDAP_ACCOUNT_FULLNAME, LDAP_ACCOUNT_EMAIL));
if ($sr === false) {
return false;
}
$info = ldap_get_entries($ldap, $sr);
// User not found
if (count($info) == 0 || $info['count'] == 0) {
return false;
}
if (@ldap_bind($ldap, $info[0]['dn'], $password)) {
return $this->create($username, $info[0][LDAP_ACCOUNT_FULLNAME][0], $info[0][LDAP_ACCOUNT_EMAIL][0]);
}
return false;
}
/**
* Create automatically a new local user after the LDAP authentication
*
* @access public
* @param string $username Username
* @param string $name Name of the user
* @param string $email Email address
* @return bool
*/
public function create($username, $name, $email)
{
$user = $this->user->getByUsername($username);
// There is an existing user account
if ($user) {
if ($user['is_ldap_user'] == 1) {
// LDAP user already created
return true;
}
else {
// There is already a local user with that username
return false;
}
}
// Create a LDAP user
$values = array(
'username' => $username,
'name' => $name,
'email' => $email,
'is_admin' => 0,
'is_ldap_user' => 1,
);
return $userModel->create($values);
}
}

View File

@@ -311,84 +311,6 @@ class User extends Base
return array(false, $v->getErrors()); return array(false, $v->getErrors());
} }
/**
* Validate user login
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateLogin(array $values)
{
$v = new Validator($values, array(
new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Required('password', t('The password is required')),
));
$result = $v->execute();
$errors = $v->getErrors();
if ($result) {
list($authenticated, $method) = $this->authenticate($values['username'], $values['password']);
if ($authenticated === true) {
// Create the user session
$user = $this->getByUsername($values['username']);
$this->updateSession($user);
// Update login history
$this->lastLogin->create(
$method,
$user['id'],
$this->getIpAddress(),
$this->getUserAgent()
);
// Setup the remember me feature
if (! empty($values['remember_me'])) {
$credentials = $this->rememberMe->create($user['id'], $this->getIpAddress(), $this->getUserAgent());
$this->rememberMe->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
}
}
else {
$result = false;
$errors['login'] = t('Bad username or password');
}
}
return array(
$result,
$errors
);
}
/**
* Authenticate a user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return array
*/
public function authenticate($username, $password)
{
// Database authentication
$user = $this->db->table(self::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne();
$authenticated = $user && \password_verify($password, $user['password']);
$method = LastLogin::AUTH_DATABASE;
// LDAP authentication
if (! $authenticated && LDAP_AUTH) {
$authenticated = $this->ldap->authenticate($username, $password);
$method = LastLogin::AUTH_LDAP;
}
return array($authenticated, $method);
}
/** /**
* Get the user agent of the connected user * Get the user agent of the connected user
* *

View File

@@ -4,7 +4,19 @@ require __DIR__.'/Core/Loader.php';
require __DIR__.'/helpers.php'; require __DIR__.'/helpers.php';
require __DIR__.'/translator.php'; require __DIR__.'/translator.php';
require 'vendor/swiftmailer/swift_required.php'; require __DIR__.'/../vendor/SimpleValidator/Validator.php';
require __DIR__.'/../vendor/SimpleValidator/Base.php';
require __DIR__.'/../vendor/SimpleValidator/Validators/Required.php';
require __DIR__.'/../vendor/SimpleValidator/Validators/Unique.php';
require __DIR__.'/../vendor/SimpleValidator/Validators/MaxLength.php';
require __DIR__.'/../vendor/SimpleValidator/Validators/MinLength.php';
require __DIR__.'/../vendor/SimpleValidator/Validators/Integer.php';
require __DIR__.'/../vendor/SimpleValidator/Validators/Equals.php';
require __DIR__.'/../vendor/SimpleValidator/Validators/AlphaNumeric.php';
require __DIR__.'/../vendor/SimpleValidator/Validators/GreaterThan.php';
require __DIR__.'/../vendor/SimpleValidator/Validators/Date.php';
require __DIR__.'/../vendor/SimpleValidator/Validators/Email.php';
require __DIR__.'/../vendor/SimpleValidator/Validators/Numeric.php';
use Core\Event; use Core\Event;
use Core\Loader; use Core\Loader;
@@ -47,6 +59,10 @@ defined('LDAP_AUTH') or define('LDAP_AUTH', false);
defined('LDAP_SERVER') or define('LDAP_SERVER', ''); defined('LDAP_SERVER') or define('LDAP_SERVER', '');
defined('LDAP_PORT') or define('LDAP_PORT', 389); defined('LDAP_PORT') or define('LDAP_PORT', 389);
defined('LDAP_SSL_VERIFY') or define('LDAP_SSL_VERIFY', true); defined('LDAP_SSL_VERIFY') or define('LDAP_SSL_VERIFY', true);
defined('LDAP_USERNAME') or define('LDAP_USERNAME', null);
defined('LDAP_PASSWORD') or define('LDAP_PASSWORD', null);
defined('LDAP_ACCOUNT_BASE') or define('LDAP_ACCOUNT_BASE', '');
defined('LDAP_USER_PATTERN') or define('LDAP_USER_PATTERN', '');
defined('LDAP_ACCOUNT_FULLNAME') or define('LDAP_ACCOUNT_FULLNAME', 'displayname'); defined('LDAP_ACCOUNT_FULLNAME') or define('LDAP_ACCOUNT_FULLNAME', 'displayname');
defined('LDAP_ACCOUNT_EMAIL') or define('LDAP_ACCOUNT_EMAIL', 'mail'); defined('LDAP_ACCOUNT_EMAIL') or define('LDAP_ACCOUNT_EMAIL', 'mail');

View File

@@ -55,7 +55,7 @@ define('LDAP_USERNAME', null);
define('LDAP_PASSWORD', null); define('LDAP_PASSWORD', null);
// LDAP account base, i.e. root of all user account // LDAP account base, i.e. root of all user account
// Example: ou=people,dc=example,dc=com // Example: ou=People,dc=example,dc=com
define('LDAP_ACCOUNT_BASE', ''); define('LDAP_ACCOUNT_BASE', '');
// LDAP query pattern to use when searching for a user account // LDAP query pattern to use when searching for a user account

View File

@@ -23,17 +23,54 @@ Differences between a local user and a LDAP user are the following:
- By default, all LDAP users have no admin privileges - By default, all LDAP users have no admin privileges
- To become administrator, a LDAP user must be promoted by another administrator - To become administrator, a LDAP user must be promoted by another administrator
The full name and the email address are automatically fetched from the LDAP server.
Configuration Configuration
------------- -------------
The first step is to create a custom config file named `config.php`. You have to create a custom config file named `config.php` (you can also use the template `config.default.php`).
This file must be stored in the root directory. This file must be stored in the root directory of Kanboard.
To do that, you can create an empty PHP file or copy/rename the sample file `config.default.php`. ### Available configuration parameters
```php
// Enable LDAP authentication (false by default)
define('LDAP_AUTH', false);
// LDAP server hostname
define('LDAP_SERVER', '');
// LDAP server port (389 by default)
define('LDAP_PORT', 389);
// By default, require certificate to be verified for ldaps:// style URL. Set to false to skip the verification.
define('LDAP_SSL_VERIFY', true);
// LDAP username to connect with. NULL for anonymous bind (by default).
define('LDAP_USERNAME', null);
// LDAP password to connect with. NULL for anonymous bind (by default).
define('LDAP_PASSWORD', null);
// LDAP account base, i.e. root of all user account
// Example: ou=People,dc=example,dc=com
define('LDAP_ACCOUNT_BASE', '');
// LDAP query pattern to use when searching for a user account
// Example for ActiveDirectory: '(&(objectClass=user)(sAMAccountName=%s))'
// Example for OpenLDAP: 'uid=%s'
define('LDAP_USER_PATTERN', '');
// Name of an attribute of the user account object which should be used as the full name of the user.
define('LDAP_ACCOUNT_FULLNAME', 'displayname');
// Name of an attribute of the user account object which should be used as the email of the user.
define('LDAP_ACCOUNT_EMAIL', 'mail');
```
### Example for Microsoft Active Directory ### Example for Microsoft Active Directory
Let's say we have a domain `MYDOMAIN` (mydomain.local) and the primary controller is `myserver.mydomain.local`. Let's say we have a domain `KANBOARD` (kanboard.local) and the primary controller is `myserver.kanboard.local`.
```php ```php
<?php <?php
@@ -41,15 +78,18 @@ Let's say we have a domain `MYDOMAIN` (mydomain.local) and the primary controlle
// Enable LDAP authentication (false by default) // Enable LDAP authentication (false by default)
define('LDAP_AUTH', true); define('LDAP_AUTH', true);
// Set credentials for be allow to browse the LDAP directory
define('LDAP_USERNAME', 'administrator@kanboard.local');
define('LDAP_PASSWORD', 'my super secret password');
// LDAP server hostname // LDAP server hostname
define('LDAP_SERVER', 'myserver.mydomain.local'); define('LDAP_SERVER', 'myserver.kanboard.local');
// User LDAP DN
define('LDAP_USER_DN', 'MYDOMAIN\\%s');
// Another way to do the same thing
define('LDAP_USER_DN', '%s@mydomain.local');
// LDAP properties
define('LDAP_ACCOUNT_BASE', 'CN=Users,DC=kanboard,DC=local');
define('LDAP_USER_PATTERN', '(&(objectClass=user)(sAMAccountName=%s))');
define('LDAP_ACCOUNT_FULLNAME', 'displayname');
define('LDAP_ACCOUNT_EMAIL', 'mail');
``` ```
### Example for OpenLDAP ### Example for OpenLDAP
@@ -65,9 +105,11 @@ define('LDAP_AUTH', true);
// LDAP server hostname // LDAP server hostname
define('LDAP_SERVER', 'myserver.example.com'); define('LDAP_SERVER', 'myserver.example.com');
// User LDAP DN // LDAP properties
define('LDAP_USER_DN', 'uid=%s,ou=People,dc=example,dc=com'); define('LDAP_ACCOUNT_BASE', 'ou=People,dc=example,dc=com');
define('LDAP_USER_PATTERN', 'uid=%s');
define('LDAP_ACCOUNT_FULLNAME', 'displayname');
define('LDAP_ACCOUNT_EMAIL', 'mail');
``` ```
The `%s` is replaced by the username for the parameter `LDAP_USER_DN`, so you can define a custom Distinguished Name. The `%s` is replaced by the username for the parameter `LDAP_USER_PATTERN`, so you can define a custom Distinguished Name.

View File

@@ -4,6 +4,20 @@ if (version_compare(PHP_VERSION, '5.5.0', '<')) {
require __DIR__.'/../../vendor/password.php'; require __DIR__.'/../../vendor/password.php';
} }
require __DIR__.'/../../vendor/SimpleValidator/Validator.php';
require __DIR__.'/../../vendor/SimpleValidator/Base.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Required.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Unique.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/MaxLength.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/MinLength.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Integer.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Equals.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/AlphaNumeric.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/GreaterThan.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Date.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Email.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Numeric.php';
require_once __DIR__.'/../../app/Core/Security.php'; require_once __DIR__.'/../../app/Core/Security.php';
require_once __DIR__.'/../../vendor/PicoDb/Database.php'; require_once __DIR__.'/../../vendor/PicoDb/Database.php';