summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrédéric Guillot <fred@kanboard.net>2014-08-16 13:59:37 -0700
committerFrédéric Guillot <fred@kanboard.net>2014-08-16 13:59:37 -0700
commit925b0ba2e56117e3bbe2947d7938ed35815efa1a (patch)
tree7c6f47dcaffa9bb03cbd0c807849fcbb82be3e1c
parent498408d5075cf0060e0f53e58261e6537e0f6080 (diff)
Authentication backends refactoring
-rw-r--r--app/Auth/Base.php60
-rw-r--r--app/Auth/Database.php52
-rw-r--r--app/Auth/GitHub.php (renamed from app/Model/GitHub.php)349
-rw-r--r--app/Auth/Google.php (renamed from app/Model/Google.php)15
-rw-r--r--app/Auth/Ldap.php150
-rw-r--r--app/Auth/RememberMe.php (renamed from app/Model/RememberMe.php)19
-rw-r--r--app/Auth/ReverseProxy.php (renamed from app/Model/ReverseProxyAuth.php)17
-rw-r--r--app/Controller/Base.php31
-rw-r--r--app/Controller/Config.php6
-rw-r--r--app/Controller/User.php28
-rw-r--r--app/Core/Tool.php7
-rw-r--r--app/Model/Authentication.php125
-rw-r--r--app/Model/Base.php15
-rw-r--r--app/Model/LastLogin.php12
-rw-r--r--app/Model/Ldap.php104
-rw-r--r--app/Model/User.php78
-rw-r--r--app/common.php18
-rw-r--r--config.default.php2
-rw-r--r--docs/ldap-authentication.markdown72
-rw-r--r--tests/units/Base.php14
20 files changed, 719 insertions, 455 deletions
diff --git a/app/Auth/Base.php b/app/Auth/Base.php
new file mode 100644
index 00000000..f9c1c329
--- /dev/null
+++ b/app/Auth/Base.php
@@ -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);
+ }
+}
diff --git a/app/Auth/Database.php b/app/Auth/Database.php
new file mode 100644
index 00000000..67881593
--- /dev/null
+++ b/app/Auth/Database.php
@@ -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;
+ }
+}
diff --git a/app/Model/GitHub.php b/app/Auth/GitHub.php
index bf4f4c51..1e99df41 100644
--- a/app/Model/GitHub.php
+++ b/app/Auth/GitHub.php
@@ -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;
+ }
+}
diff --git a/app/Model/Google.php b/app/Auth/Google.php
index cca4f668..3dca96be 100644
--- a/app/Model/Google.php
+++ b/app/Auth/Google.php
@@ -1,6 +1,6 @@
<?php
-namespace Model;
+namespace Auth;
require __DIR__.'/../../vendor/OAuth/bootstrap.php';
@@ -11,14 +11,21 @@ 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
*
* @access public
@@ -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()
diff --git a/app/Auth/Ldap.php b/app/Auth/Ldap.php
new file mode 100644
index 00000000..bb17653d
--- /dev/null
+++ b/app/Auth/Ldap.php
@@ -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);
+ }
+}
diff --git a/app/Model/RememberMe.php b/app/Auth/RememberMe.php
index e23ed887..3cf6fc86 100644
--- a/app/Model/RememberMe.php
+++ b/app/Auth/RememberMe.php
@@ -1,18 +1,25 @@
<?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
*
* @var string
@@ -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;
}
}
diff --git a/app/Model/ReverseProxyAuth.php b/app/Auth/ReverseProxy.php
index 14d18ba3..e23ee24f 100644
--- a/app/Model/ReverseProxyAuth.php
+++ b/app/Auth/ReverseProxy.php
@@ -1,18 +1,25 @@
<?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
*
* @access public
@@ -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()
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index 11841e09..ed8a6b3b 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -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
diff --git a/app/Controller/Config.php b/app/Controller/Config.php
index 498f3214..48bfb9cf 100644
--- a/app/Controller/Config.php
+++ b/app/Controller/Config.php
@@ -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');
}
}
diff --git a/app/Controller/User.php b/app/Controller/User.php
index d30c6fd2..0bb7aec1 100644
--- a/app/Controller/User.php
+++ b/app/Controller/User.php
@@ -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 {
diff --git a/app/Core/Tool.php b/app/Core/Tool.php
index 1a2e9904..85b684e2 100644
--- a/app/Core/Tool.php
+++ b/app/Core/Tool.php
@@ -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);
}
}
diff --git a/app/Model/Authentication.php b/app/Model/Authentication.php
new file mode 100644
index 00000000..4c8aad82
--- /dev/null
+++ b/app/Model/Authentication.php
@@ -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
+ );
+ }
+}
diff --git a/app/Model/Base.php b/app/Model/Base.php
index 92578ffc..1439a36e 100644
--- a/app/Model/Base.php
+++ b/app/Model/Base.php
@@ -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
diff --git a/app/Model/LastLogin.php b/app/Model/LastLogin.php
index e2ea63e1..3391db50 100644
--- a/app/Model/LastLogin.php
+++ b/app/Model/LastLogin.php
@@ -25,18 +25,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
*
* @access public
diff --git a/app/Model/Ldap.php b/app/Model/Ldap.php
deleted file mode 100644
index 007f7171..00000000
--- a/app/Model/Ldap.php
+++ /dev/null
@@ -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);
- }
-}
diff --git a/app/Model/User.php b/app/Model/User.php
index d0e33fd0..5f6b8a3a 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -312,84 +312,6 @@ class User extends Base
}
/**
- * 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
*
* @access public
diff --git a/app/common.php b/app/common.php
index 9ce0016a..55ecd894 100644
--- a/app/common.php
+++ b/app/common.php
@@ -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');
diff --git a/config.default.php b/config.default.php
index 5f8313a9..b79bad94 100644
--- a/config.default.php
+++ b/config.default.php
@@ -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
diff --git a/docs/ldap-authentication.markdown b/docs/ldap-authentication.markdown
index 65abbbb3..989ee24d 100644
--- a/docs/ldap-authentication.markdown
+++ b/docs/ldap-authentication.markdown
@@ -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.
+
+### 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);
-To do that, you can create an empty PHP file or copy/rename the sample file `config.default.php`.
+// 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);
-// LDAP server hostname
-define('LDAP_SERVER', 'myserver.mydomain.local');
-
-// User LDAP DN
-define('LDAP_USER_DN', 'MYDOMAIN\\%s');
+// Set credentials for be allow to browse the LDAP directory
+define('LDAP_USERNAME', 'administrator@kanboard.local');
+define('LDAP_PASSWORD', 'my super secret password');
-// Another way to do the same thing
-define('LDAP_USER_DN', '%s@mydomain.local');
+// LDAP server hostname
+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.
diff --git a/tests/units/Base.php b/tests/units/Base.php
index 313e6f43..0fc0f99e 100644
--- a/tests/units/Base.php
+++ b/tests/units/Base.php
@@ -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';