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

View File

@ -1,17 +1,24 @@
<?php
namespace Model;
namespace Auth;
use Core\Security;
/**
* ReverseProxyAuth model
* ReverseProxy backend
*
* @package model
* @package auth
* @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
*
@ -35,7 +42,7 @@ class ReverseProxyAuth extends Base
// Update login history
$this->lastLogin->create(
LastLogin::AUTH_REVERSE_PROXY,
self::AUTH_NAME,
$user['id'],
$this->user->getIpAddress(),
$this->user->getUserAgent()

View File

@ -15,20 +15,16 @@ use Model\LastLogin;
* @author Frederic Guillot
*
* @property \Model\Acl $acl
* @property \Model\Authentication $authentication
* @property \Model\Action $action
* @property \Model\Board $board
* @property \Model\Category $category
* @property \Model\Comment $comment
* @property \Model\Config $config
* @property \Model\File $file
* @property \Model\Google $google
* @property \Model\GitHub $gitHub
* @property \Model\LastLogin $lastLogin
* @property \Model\Ldap $ldap
* @property \Model\Notification $notification
* @property \Model\Project $project
* @property \Model\RememberMe $rememberMe
* @property \Model\ReverseProxyAuth $reverseProxyAuth
* @property \Model\SubTask $subTask
* @property \Model\Task $task
* @property \Model\User $user
@ -123,29 +119,8 @@ abstract class Base
date_default_timezone_set($this->config->get('timezone', 'UTC'));
// Authentication
if (! $this->acl->isLogged() && ! $this->acl->isPublicAction($controller, $action)) {
// 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();
if (! $this->authentication->isAuthenticated($controller, $action)) {
$this->response->redirect('?controller=user&action=login');
}
// Check if the user is allowed to see this page

View File

@ -28,7 +28,7 @@ class Config extends Base
'menu' => 'config',
'title' => t('Settings'),
'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()),
)));
}
@ -73,7 +73,7 @@ class Config extends Base
'menu' => 'config',
'title' => t('Settings'),
'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()),
)));
}
@ -124,7 +124,7 @@ class Config extends Base
public function removeRememberMeToken()
{
$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');
}
}

View File

@ -18,7 +18,7 @@ class User extends Base
public function logout()
{
$this->checkCSRFParam();
$this->rememberMe->destroy($this->acl->getUserId());
$this->authentication->backend('rememberMe')->destroy($this->acl->getUserId());
$this->session->close();
$this->response->redirect('?controller=user&action=login');
}
@ -30,7 +30,7 @@ class User extends Base
*/
public function login()
{
if (isset($_SESSION['user'])) {
if ($this->acl->isLogged()) {
$this->response->redirect('?controller=app');
}
@ -50,7 +50,7 @@ class User extends Base
public function check()
{
$values = $this->request->getValues();
list($valid, $errors) = $this->user->validateLogin($values);
list($valid, $errors) = $this->authentication->validateForm($values);
if ($valid) {
$this->response->redirect('?controller=app');
@ -249,14 +249,14 @@ class User extends Base
if ($code) {
$profile = $this->google->getGoogleProfile($code);
$profile = $this->authentication->backend('google')->getGoogleProfile($code);
if (is_array($profile)) {
// If the user is already logged, link the account otherwise authenticate
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.'));
}
else {
@ -265,7 +265,7 @@ class User extends Base
$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');
}
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()
{
$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.'));
}
else {
@ -310,14 +310,14 @@ class User extends Base
$code = $this->request->getStringParam('code');
if ($code) {
$profile = $this->gitHub->getGitHubProfile($code);
$profile = $this->authentication->backend('gitHub')->getGitHubProfile($code);
if (is_array($profile)) {
// If the user is already logged, link the account otherwise authenticate
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.'));
}
else {
@ -326,7 +326,7 @@ class User extends Base
$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');
}
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->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.'));
}
else {

View File

@ -34,8 +34,11 @@ class Tool
public static function loadModel(Registry $registry, $name)
{
$class = '\Model\\'.ucfirst($name);
$registry->$name = new $class($registry);
if (! isset($registry->$name)) {
$class = '\Model\\'.ucfirst($name);
$registry->$name = new $class($registry);
}
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;
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\Tool;
use Core\Registry;
@ -35,7 +21,6 @@ use PicoDb\Database;
* @property \Model\Config $config
* @property \Model\File $file
* @property \Model\LastLogin $lastLogin
* @property \Model\Ldap $ldap
* @property \Model\Notification $notification
* @property \Model\Project $project
* @property \Model\SubTask $subTask

View File

@ -24,18 +24,6 @@ class LastLogin extends Base
*/
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
*

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());
}
/**
* 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
*

View File

@ -4,7 +4,19 @@ require __DIR__.'/Core/Loader.php';
require __DIR__.'/helpers.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\Loader;
@ -47,6 +59,10 @@ defined('LDAP_AUTH') or define('LDAP_AUTH', false);
defined('LDAP_SERVER') or define('LDAP_SERVER', '');
defined('LDAP_PORT') or define('LDAP_PORT', 389);
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_EMAIL') or define('LDAP_ACCOUNT_EMAIL', 'mail');

View File

@ -55,7 +55,7 @@ define('LDAP_USERNAME', null);
define('LDAP_PASSWORD', null);
// 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', '');
// 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
- 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
-------------
The first step is to create a custom config file named `config.php`.
This file must be stored in the root directory.
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 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
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
@ -41,15 +78,18 @@ Let's say we have a domain `MYDOMAIN` (mydomain.local) and the primary controlle
// Enable LDAP authentication (false by default)
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
define('LDAP_SERVER', 'myserver.mydomain.local');
// User LDAP DN
define('LDAP_USER_DN', 'MYDOMAIN\\%s');
// Another way to do the same thing
define('LDAP_USER_DN', '%s@mydomain.local');
define('LDAP_SERVER', 'myserver.kanboard.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
@ -65,9 +105,11 @@ define('LDAP_AUTH', true);
// LDAP server hostname
define('LDAP_SERVER', 'myserver.example.com');
// User LDAP DN
define('LDAP_USER_DN', 'uid=%s,ou=People,dc=example,dc=com');
// LDAP properties
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/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__.'/../../vendor/PicoDb/Database.php';