From 925b0ba2e56117e3bbe2947d7938ed35815efa1a Mon Sep 17 00:00:00 2001 From: Frédéric Guillot Date: Sat, 16 Aug 2014 13:59:37 -0700 Subject: Authentication backends refactoring --- app/Auth/Base.php | 60 +++++++ app/Auth/Database.php | 52 ++++++ app/Auth/GitHub.php | 178 +++++++++++++++++++++ app/Auth/Google.php | 153 ++++++++++++++++++ app/Auth/Ldap.php | 150 ++++++++++++++++++ app/Auth/RememberMe.php | 349 +++++++++++++++++++++++++++++++++++++++++ app/Auth/ReverseProxy.php | 73 +++++++++ app/Controller/Base.php | 31 +--- app/Controller/Config.php | 6 +- app/Controller/User.php | 28 ++-- app/Core/Tool.php | 7 +- app/Model/Authentication.php | 125 +++++++++++++++ app/Model/Base.php | 15 -- app/Model/GitHub.php | 171 -------------------- app/Model/Google.php | 146 ----------------- app/Model/LastLogin.php | 12 -- app/Model/Ldap.php | 104 ------------ app/Model/RememberMe.php | 334 --------------------------------------- app/Model/ReverseProxyAuth.php | 66 -------- app/Model/User.php | 78 --------- app/common.php | 18 ++- 21 files changed, 1182 insertions(+), 974 deletions(-) create mode 100644 app/Auth/Base.php create mode 100644 app/Auth/Database.php create mode 100644 app/Auth/GitHub.php create mode 100644 app/Auth/Google.php create mode 100644 app/Auth/Ldap.php create mode 100644 app/Auth/RememberMe.php create mode 100644 app/Auth/ReverseProxy.php create mode 100644 app/Model/Authentication.php delete mode 100644 app/Model/GitHub.php delete mode 100644 app/Model/Google.php delete mode 100644 app/Model/Ldap.php delete mode 100644 app/Model/RememberMe.php delete mode 100644 app/Model/ReverseProxyAuth.php (limited to 'app') 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 @@ +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 @@ +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/Auth/GitHub.php b/app/Auth/GitHub.php new file mode 100644 index 00000000..1e99df41 --- /dev/null +++ b/app/Auth/GitHub.php @@ -0,0 +1,178 @@ +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/Auth/Google.php b/app/Auth/Google.php new file mode 100644 index 00000000..3dca96be --- /dev/null +++ b/app/Auth/Google.php @@ -0,0 +1,153 @@ +user->getByGoogleId($google_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 Google 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, + 'google_id' => '', + )); + } + + /** + * Update the user table based on the Google profile information + * + * @access public + * @param integer $user_id User id + * @param array $profile Google profile + * @return boolean + */ + public function updateUser($user_id, array $profile) + { + return $this->user->update(array( + 'id' => $user_id, + 'google_id' => $profile['id'], + 'email' => $profile['email'], + 'name' => $profile['name'], + )); + } + + /** + * Get the Google service instance + * + * @access public + * @return \OAuth\OAuth2\Service\Google + */ + public function getService() + { + $uriFactory = new UriFactory(); + $currentUri = $uriFactory->createFromSuperGlobalArray($_SERVER); + $currentUri->setQuery('controller=user&action=google'); + + $storage = new Session(false); + + $credentials = new Credentials( + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + $currentUri->getAbsoluteUri() + ); + + $serviceFactory = new ServiceFactory(); + + return $serviceFactory->createService( + 'google', + $credentials, + $storage, + array('userinfo_email', 'userinfo_profile') + ); + } + + /** + * Get the authorization URL + * + * @access public + * @return \OAuth\Common\Http\Uri\Uri + */ + public function getAuthorizationUrl() + { + return $this->getService()->getAuthorizationUri(); + } + + /** + * Get Google profile information from the API + * + * @access public + * @param string $code Google authorization code + * @return bool|array + */ + public function getGoogleProfile($code) + { + try { + + $googleService = $this->getService(); + $googleService->requestAccessToken($code); + return json_decode($googleService->request('https://www.googleapis.com/oauth2/v1/userinfo'), true); + } + catch (TokenResponseException $e) { + return false; + } + + return false; + } +} 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 @@ +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/Auth/RememberMe.php b/app/Auth/RememberMe.php new file mode 100644 index 00000000..3cf6fc86 --- /dev/null +++ b/app/Auth/RememberMe.php @@ -0,0 +1,349 @@ +db + ->table(self::TABLE) + ->eq('token', $token) + ->eq('sequence', $sequence) + ->gt('expiration', time()) + ->findOne(); + } + + /** + * Get all sessions for a given user + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function getAll($user_id) + { + return $this->db + ->table(self::TABLE) + ->eq('user_id', $user_id) + ->desc('date_creation') + ->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration') + ->findAll(); + } + + /** + * Authenticate the user with the cookie + * + * @access public + * @return bool + */ + public function authenticate() + { + $credentials = $this->readCookie(); + + if ($credentials !== false) { + + $record = $this->find($credentials['token'], $credentials['sequence']); + + if ($record) { + + // Update the sequence + $this->writeCookie( + $record['token'], + $this->update($record['token'], $record['sequence']), + $record['expiration'] + ); + + // Create the session + $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; + } + } + + return false; + } + + /** + * Update the database and the cookie with a new sequence + * + * @access public + */ + public function refresh() + { + $credentials = $this->readCookie(); + + if ($credentials !== false) { + + $record = $this->find($credentials['token'], $credentials['sequence']); + + if ($record) { + + // Update the sequence + $this->writeCookie( + $record['token'], + $this->update($record['token'], $record['sequence']), + $record['expiration'] + ); + } + } + } + + /** + * Remove a session record + * + * @access public + * @param integer $session_id Session id + * @return mixed + */ + public function remove($session_id) + { + return $this->db + ->table(self::TABLE) + ->eq('id', $session_id) + ->remove(); + } + + /** + * Remove the current RememberMe session and the cookie + * + * @access public + * @param integer $user_id User id + */ + public function destroy($user_id) + { + $credentials = $this->readCookie(); + + if ($credentials !== false) { + + $this->deleteCookie(); + + $this->db + ->table(self::TABLE) + ->eq('user_id', $user_id) + ->eq('token', $credentials['token']) + ->remove(); + } + } + + /** + * Create a new RememberMe session + * + * @access public + * @param integer $user_id User id + * @param string $ip IP Address + * @param string $user_agent User Agent + * @return array + */ + public function create($user_id, $ip, $user_agent) + { + $token = hash('sha256', $user_id.$user_agent.$ip.Security::generateToken()); + $sequence = Security::generateToken(); + $expiration = time() + self::EXPIRATION; + + $this->cleanup($user_id); + + $this->db + ->table(self::TABLE) + ->insert(array( + 'user_id' => $user_id, + 'ip' => $ip, + 'user_agent' => $user_agent, + 'token' => $token, + 'sequence' => $sequence, + 'expiration' => $expiration, + 'date_creation' => time(), + )); + + return array( + 'token' => $token, + 'sequence' => $sequence, + 'expiration' => $expiration, + ); + } + + /** + * Remove old sessions for a given user + * + * @access public + * @param integer $user_id User id + * @return bool + */ + public function cleanup($user_id) + { + return $this->db + ->table(self::TABLE) + ->eq('user_id', $user_id) + ->lt('expiration', time()) + ->remove(); + } + + /** + * Return a new sequence token and update the database + * + * @access public + * @param string $token Session token + * @param string $sequence Sequence token + * @return string + */ + public function update($token, $sequence) + { + $new_sequence = Security::generateToken(); + + $this->db + ->table(self::TABLE) + ->eq('token', $token) + ->eq('sequence', $sequence) + ->update(array('sequence' => $new_sequence)); + + return $new_sequence; + } + + /** + * Encode the cookie + * + * @access public + * @param string $token Session token + * @param string $sequence Sequence token + * @return string + */ + public function encodeCookie($token, $sequence) + { + return implode('|', array($token, $sequence)); + } + + /** + * Decode the value of a cookie + * + * @access public + * @param string $value Raw cookie data + * @return array + */ + public function decodeCookie($value) + { + list($token, $sequence) = explode('|', $value); + + return array( + 'token' => $token, + 'sequence' => $sequence, + ); + } + + /** + * Return true if the current user has a RememberMe cookie + * + * @access public + * @return bool + */ + public function hasCookie() + { + return ! empty($_COOKIE[self::COOKIE_NAME]); + } + + /** + * Write and encode the cookie + * + * @access public + * @param string $token Session token + * @param string $sequence Sequence token + * @param string $expiration Cookie expiration + */ + public function writeCookie($token, $sequence, $expiration) + { + setcookie( + self::COOKIE_NAME, + $this->encodeCookie($token, $sequence), + $expiration, + BASE_URL_DIRECTORY, + null, + ! empty($_SERVER['HTTPS']), + true + ); + } + + /** + * Read and decode the cookie + * + * @access public + * @return mixed + */ + public function readCookie() + { + if (empty($_COOKIE[self::COOKIE_NAME])) { + return false; + } + + return $this->decodeCookie($_COOKIE[self::COOKIE_NAME]); + } + + /** + * Remove the cookie + * + * @access public + */ + public function deleteCookie() + { + setcookie( + self::COOKIE_NAME, + '', + time() - 3600, + BASE_URL_DIRECTORY, + null, + ! empty($_SERVER['HTTPS']), + true + ); + } +} diff --git a/app/Auth/ReverseProxy.php b/app/Auth/ReverseProxy.php new file mode 100644 index 00000000..e23ee24f --- /dev/null +++ b/app/Auth/ReverseProxy.php @@ -0,0 +1,73 @@ +user->getByUsername($login); + + if (! $user) { + $this->createUser($login); + $user = $this->user->getByUsername($login); + } + + // 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; + } + + /** + * Create automatically a new local user after the authentication + * + * @access private + * @param string $login Username + * @return bool + */ + private function createUser($login) + { + return $this->user->create(array( + 'email' => strpos($login, '@') !== false ? $login : '', + 'username' => $login, + 'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login, + 'is_ldap_user' => 1, + )); + } +} 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 @@ +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/GitHub.php b/app/Model/GitHub.php deleted file mode 100644 index bf4f4c51..00000000 --- a/app/Model/GitHub.php +++ /dev/null @@ -1,171 +0,0 @@ -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; - } -} diff --git a/app/Model/Google.php b/app/Model/Google.php deleted file mode 100644 index cca4f668..00000000 --- a/app/Model/Google.php +++ /dev/null @@ -1,146 +0,0 @@ -user->getByGoogleId($google_id); - - if ($user) { - - // Create the user session - $this->user->updateSession($user); - - // Update login history - $this->lastLogin->create( - LastLogin::AUTH_GOOGLE, - $user['id'], - $this->user->getIpAddress(), - $this->user->getUserAgent() - ); - - return true; - } - - return false; - } - - /** - * Unlink a Google 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, - 'google_id' => '', - )); - } - - /** - * Update the user table based on the Google profile information - * - * @access public - * @param integer $user_id User id - * @param array $profile Google profile - * @return boolean - */ - public function updateUser($user_id, array $profile) - { - return $this->user->update(array( - 'id' => $user_id, - 'google_id' => $profile['id'], - 'email' => $profile['email'], - 'name' => $profile['name'], - )); - } - - /** - * Get the Google service instance - * - * @access public - * @return \OAuth\OAuth2\Service\Google - */ - public function getService() - { - $uriFactory = new UriFactory(); - $currentUri = $uriFactory->createFromSuperGlobalArray($_SERVER); - $currentUri->setQuery('controller=user&action=google'); - - $storage = new Session(false); - - $credentials = new Credentials( - GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET, - $currentUri->getAbsoluteUri() - ); - - $serviceFactory = new ServiceFactory(); - - return $serviceFactory->createService( - 'google', - $credentials, - $storage, - array('userinfo_email', 'userinfo_profile') - ); - } - - /** - * Get the authorization URL - * - * @access public - * @return \OAuth\Common\Http\Uri\Uri - */ - public function getAuthorizationUrl() - { - return $this->getService()->getAuthorizationUri(); - } - - /** - * Get Google profile information from the API - * - * @access public - * @param string $code Google authorization code - * @return bool|array - */ - public function getGoogleProfile($code) - { - try { - - $googleService = $this->getService(); - $googleService->requestAccessToken($code); - return json_decode($googleService->request('https://www.googleapis.com/oauth2/v1/userinfo'), true); - } - catch (TokenResponseException $e) { - return false; - } - - return false; - } -} 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 @@ -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 * 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 @@ -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/RememberMe.php b/app/Model/RememberMe.php deleted file mode 100644 index e23ed887..00000000 --- a/app/Model/RememberMe.php +++ /dev/null @@ -1,334 +0,0 @@ -db - ->table(self::TABLE) - ->eq('token', $token) - ->eq('sequence', $sequence) - ->gt('expiration', time()) - ->findOne(); - } - - /** - * Get all sessions for a given user - * - * @access public - * @param integer $user_id User id - * @return array - */ - public function getAll($user_id) - { - return $this->db - ->table(self::TABLE) - ->eq('user_id', $user_id) - ->desc('date_creation') - ->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration') - ->findAll(); - } - - /** - * Authenticate the user with the cookie - * - * @access public - * @return bool - */ - public function authenticate() - { - $credentials = $this->readCookie(); - - if ($credentials !== false) { - - $record = $this->find($credentials['token'], $credentials['sequence']); - - if ($record) { - - // Update the sequence - $this->writeCookie( - $record['token'], - $this->update($record['token'], $record['sequence']), - $record['expiration'] - ); - - // Create the session - $this->user->updateSession($this->user->getById($record['user_id'])); - $this->acl->isRememberMe(true); - - return true; - } - } - - return false; - } - - /** - * Update the database and the cookie with a new sequence - * - * @access public - */ - public function refresh() - { - $credentials = $this->readCookie(); - - if ($credentials !== false) { - - $record = $this->find($credentials['token'], $credentials['sequence']); - - if ($record) { - - // Update the sequence - $this->writeCookie( - $record['token'], - $this->update($record['token'], $record['sequence']), - $record['expiration'] - ); - } - } - } - - /** - * Remove a session record - * - * @access public - * @param integer $session_id Session id - * @return mixed - */ - public function remove($session_id) - { - return $this->db - ->table(self::TABLE) - ->eq('id', $session_id) - ->remove(); - } - - /** - * Remove the current RememberMe session and the cookie - * - * @access public - * @param integer $user_id User id - */ - public function destroy($user_id) - { - $credentials = $this->readCookie(); - - if ($credentials !== false) { - - $this->deleteCookie(); - - $this->db - ->table(self::TABLE) - ->eq('user_id', $user_id) - ->eq('token', $credentials['token']) - ->remove(); - } - } - - /** - * Create a new RememberMe session - * - * @access public - * @param integer $user_id User id - * @param string $ip IP Address - * @param string $user_agent User Agent - * @return array - */ - public function create($user_id, $ip, $user_agent) - { - $token = hash('sha256', $user_id.$user_agent.$ip.Security::generateToken()); - $sequence = Security::generateToken(); - $expiration = time() + self::EXPIRATION; - - $this->cleanup($user_id); - - $this->db - ->table(self::TABLE) - ->insert(array( - 'user_id' => $user_id, - 'ip' => $ip, - 'user_agent' => $user_agent, - 'token' => $token, - 'sequence' => $sequence, - 'expiration' => $expiration, - 'date_creation' => time(), - )); - - return array( - 'token' => $token, - 'sequence' => $sequence, - 'expiration' => $expiration, - ); - } - - /** - * Remove old sessions for a given user - * - * @access public - * @param integer $user_id User id - * @return bool - */ - public function cleanup($user_id) - { - return $this->db - ->table(self::TABLE) - ->eq('user_id', $user_id) - ->lt('expiration', time()) - ->remove(); - } - - /** - * Return a new sequence token and update the database - * - * @access public - * @param string $token Session token - * @param string $sequence Sequence token - * @return string - */ - public function update($token, $sequence) - { - $new_sequence = Security::generateToken(); - - $this->db - ->table(self::TABLE) - ->eq('token', $token) - ->eq('sequence', $sequence) - ->update(array('sequence' => $new_sequence)); - - return $new_sequence; - } - - /** - * Encode the cookie - * - * @access public - * @param string $token Session token - * @param string $sequence Sequence token - * @return string - */ - public function encodeCookie($token, $sequence) - { - return implode('|', array($token, $sequence)); - } - - /** - * Decode the value of a cookie - * - * @access public - * @param string $value Raw cookie data - * @return array - */ - public function decodeCookie($value) - { - list($token, $sequence) = explode('|', $value); - - return array( - 'token' => $token, - 'sequence' => $sequence, - ); - } - - /** - * Return true if the current user has a RememberMe cookie - * - * @access public - * @return bool - */ - public function hasCookie() - { - return ! empty($_COOKIE[self::COOKIE_NAME]); - } - - /** - * Write and encode the cookie - * - * @access public - * @param string $token Session token - * @param string $sequence Sequence token - * @param string $expiration Cookie expiration - */ - public function writeCookie($token, $sequence, $expiration) - { - setcookie( - self::COOKIE_NAME, - $this->encodeCookie($token, $sequence), - $expiration, - BASE_URL_DIRECTORY, - null, - ! empty($_SERVER['HTTPS']), - true - ); - } - - /** - * Read and decode the cookie - * - * @access public - * @return mixed - */ - public function readCookie() - { - if (empty($_COOKIE[self::COOKIE_NAME])) { - return false; - } - - return $this->decodeCookie($_COOKIE[self::COOKIE_NAME]); - } - - /** - * Remove the cookie - * - * @access public - */ - public function deleteCookie() - { - setcookie( - self::COOKIE_NAME, - '', - time() - 3600, - BASE_URL_DIRECTORY, - null, - ! empty($_SERVER['HTTPS']), - true - ); - } -} diff --git a/app/Model/ReverseProxyAuth.php b/app/Model/ReverseProxyAuth.php deleted file mode 100644 index 14d18ba3..00000000 --- a/app/Model/ReverseProxyAuth.php +++ /dev/null @@ -1,66 +0,0 @@ -user->getByUsername($login); - - if (! $user) { - $this->createUser($login); - $user = $this->user->getByUsername($login); - } - - // Create the user session - $this->user->updateSession($user); - - // Update login history - $this->lastLogin->create( - LastLogin::AUTH_REVERSE_PROXY, - $user['id'], - $this->user->getIpAddress(), - $this->user->getUserAgent() - ); - - return true; - } - - return false; - } - - /** - * Create automatically a new local user after the authentication - * - * @access private - * @param string $login Username - * @return bool - */ - private function createUser($login) - { - return $this->user->create(array( - 'email' => strpos($login, '@') !== false ? $login : '', - 'username' => $login, - 'is_admin' => REVERSE_PROXY_DEFAULT_ADMIN === $login, - 'is_ldap_user' => 1, - )); - } -} 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 @@ -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 * 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'); -- cgit v1.2.3