diff options
-rw-r--r-- | app/Auth/Base.php | 60 | ||||
-rw-r--r-- | app/Auth/Database.php | 52 | ||||
-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.php | 150 | ||||
-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.php | 31 | ||||
-rw-r--r-- | app/Controller/Config.php | 6 | ||||
-rw-r--r-- | app/Controller/User.php | 28 | ||||
-rw-r--r-- | app/Core/Tool.php | 7 | ||||
-rw-r--r-- | app/Model/Authentication.php | 125 | ||||
-rw-r--r-- | app/Model/Base.php | 15 | ||||
-rw-r--r-- | app/Model/LastLogin.php | 12 | ||||
-rw-r--r-- | app/Model/Ldap.php | 104 | ||||
-rw-r--r-- | app/Model/User.php | 78 | ||||
-rw-r--r-- | app/common.php | 18 | ||||
-rw-r--r-- | config.default.php | 2 | ||||
-rw-r--r-- | docs/ldap-authentication.markdown | 72 | ||||
-rw-r--r-- | tests/units/Base.php | 14 |
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'; |