diff options
Diffstat (limited to 'app/Auth')
-rw-r--r-- | app/Auth/Base.php | 60 | ||||
-rw-r--r-- | app/Auth/Database.php | 52 | ||||
-rw-r--r-- | app/Auth/GitHub.php | 178 | ||||
-rw-r--r-- | app/Auth/Google.php | 153 | ||||
-rw-r--r-- | app/Auth/Ldap.php | 150 | ||||
-rw-r--r-- | app/Auth/RememberMe.php | 349 | ||||
-rw-r--r-- | app/Auth/ReverseProxy.php | 73 |
7 files changed, 1015 insertions, 0 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/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 @@ +<?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/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 @@ +<?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; + +/** + * Google backend + * + * @package auth + * @author Frederic Guillot + */ +class Google extends Base +{ + /** + * Backend name + * + * @var string + */ + const AUTH_NAME = 'Google'; + + /** + * Authenticate a Google user + * + * @access public + * @param string $google_id Google unique id + * @return boolean + */ + public function authenticate($google_id) + { + $user = $this->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 @@ +<?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/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 @@ +<?php + +namespace Auth; + +use Core\Security; + +/** + * RememberMe model + * + * @package auth + * @author Frederic Guillot + */ +class RememberMe extends Base +{ + /** + * Backend name + * + * @var string + */ + const AUTH_NAME = 'RememberMe'; + + /** + * SQL table name + * + * @var string + */ + const TABLE = 'remember_me'; + + /** + * Cookie name + * + * @var string + */ + const COOKIE_NAME = '__R'; + + /** + * Expiration (60 days) + * + * @var integer + */ + const EXPIRATION = 5184000; + + /** + * Get a remember me record + * + * @access public + * @param $token + * @param $sequence + * @return mixed + */ + public function find($token, $sequence) + { + return $this->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 @@ +<?php + +namespace Auth; + +use Core\Security; + +/** + * ReverseProxy backend + * + * @package auth + * @author Sylvain VeyriƩ + */ +class ReverseProxy extends Base +{ + /** + * Backend name + * + * @var string + */ + const AUTH_NAME = 'ReverseProxy'; + + /** + * Authenticate the user with the HTTP header + * + * @access public + * @return bool + */ + public function authenticate() + { + if (isset($_SERVER[REVERSE_PROXY_USER_HEADER])) { + + $login = $_SERVER[REVERSE_PROXY_USER_HEADER]; + $user = $this->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, + )); + } +} |