Authentication backends refactoring

This commit is contained in:
Frédéric Guillot
2014-08-16 13:59:37 -07:00
parent 498408d507
commit 925b0ba2e5
20 changed files with 719 additions and 455 deletions

View File

@@ -0,0 +1,125 @@
<?php
namespace Model;
use Auth\Database;
use SimpleValidator\Validator;
use SimpleValidator\Validators;
/**
* Authentication model
*
* @package model
* @author Frederic Guillot
*/
class Authentication extends Base
{
/**
* Load automatically an authentication backend
*
* @access public
* @param string $name Backend class name
* @return mixed
*/
public function backend($name)
{
if (! isset($this->registry->$name)) {
$class = '\Auth\\'.ucfirst($name);
$this->registry->$name = new $class($this->registry);
}
return $this->registry->shared($name);
}
/**
* Check if the current user is authenticated
*
* @access public
* @param string $controller Controller
* @param string $action Action name
* @return bool
*/
public function isAuthenticated($controller, $action)
{
// If the action is public we don't need to do any checks
if ($this->acl->isPublicAction($controller, $action)) {
return true;
}
// If the user is already logged it's ok
if ($this->acl->isLogged()) {
// We update each time the RememberMe cookie tokens
if ($this->backend('rememberMe')->hasCookie()) {
$this->backend('rememberMe')->refresh();
}
return true;
}
// We try first with the RememberMe cookie
if ($this->backend('rememberMe')->authenticate()) {
return true;
}
// Then with the ReverseProxy authentication
if (REVERSE_PROXY_AUTH && $this->backend('reverseProxy')->authenticate()) {
return true;
}
return false;
}
/**
* Validate user login form
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateForm(array $values)
{
$v = new Validator($values, array(
new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Required('password', t('The password is required')),
));
$result = $v->execute();
$errors = $v->getErrors();
if ($result) {
$authenticated = false;
// Try first the database auth and then LDAP if activated
if ($this->backend('database')->authenticate($values['username'], $values['password'])) {
$authenticated = true;
}
else if (LDAP_AUTH && $this->backend('ldap')->authenticate($values['username'], $values['password'])) {
$authenticated = true;
}
if ($authenticated) {
// Setup the remember me feature
if (! empty($values['remember_me'])) {
$credentials = $this->backend('rememberMe')
->create($this->acl->getUserId(), $this->user->getIpAddress(), $this->user->getUserAgent());
$this->backend('rememberMe')->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
}
}
else {
$result = false;
$errors['login'] = t('Bad username or password');
}
}
return array(
$result,
$errors
);
}
}

View File

@@ -2,20 +2,6 @@
namespace Model;
require __DIR__.'/../../vendor/SimpleValidator/Validator.php';
require __DIR__.'/../../vendor/SimpleValidator/Base.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Required.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Unique.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/MaxLength.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/MinLength.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Integer.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Equals.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/AlphaNumeric.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/GreaterThan.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Date.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Email.php';
require __DIR__.'/../../vendor/SimpleValidator/Validators/Numeric.php';
use Core\Event;
use Core\Tool;
use Core\Registry;
@@ -35,7 +21,6 @@ use PicoDb\Database;
* @property \Model\Config $config
* @property \Model\File $file
* @property \Model\LastLogin $lastLogin
* @property \Model\Ldap $ldap
* @property \Model\Notification $notification
* @property \Model\Project $project
* @property \Model\SubTask $subTask

View File

@@ -1,171 +0,0 @@
<?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;
}
}

View File

@@ -1,146 +0,0 @@
<?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;
/**
* Google model
*
* @package model
* @author Frederic Guillot
*/
class Google extends Base
{
/**
* 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(
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;
}
}

View File

@@ -24,18 +24,6 @@ class LastLogin extends Base
*/
const NB_LOGINS = 10;
/**
* Authentication methods
*
* @var string
*/
const AUTH_DATABASE = 'database';
const AUTH_REMEMBER_ME = 'remember_me';
const AUTH_LDAP = 'ldap';
const AUTH_GOOGLE = 'google';
const AUTH_GITHUB = 'github';
const AUTH_REVERSE_PROXY = 'reverse_proxy';
/**
* Create a new record
*

View File

@@ -1,104 +0,0 @@
<?php
namespace Model;
/**
* LDAP model
*
* @package model
* @author Frederic Guillot
*/
class Ldap extends Base
{
/**
* Authenticate a user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return null|boolean
*/
public function authenticate($username, $password)
{
if (! function_exists('ldap_connect')) {
die('The PHP LDAP extension is required');
}
// Skip SSL certificate verification
if (! LDAP_SSL_VERIFY) {
putenv('LDAPTLS_REQCERT=never');
}
$ldap = ldap_connect(LDAP_SERVER, LDAP_PORT);
if (! is_resource($ldap)) {
die('Unable to connect to the LDAP server: "'.LDAP_SERVER.'"');
}
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
if (! @ldap_bind($ldap, LDAP_USERNAME, LDAP_PASSWORD)) {
die('Unable to bind to the LDAP server: "'.LDAP_SERVER.'"');
}
$sr = @ldap_search($ldap, LDAP_ACCOUNT_BASE, sprintf(LDAP_USER_PATTERN, $username), array(LDAP_ACCOUNT_FULLNAME, LDAP_ACCOUNT_EMAIL));
if ($sr === false) {
return false;
}
$info = ldap_get_entries($ldap, $sr);
// User not found
if (count($info) == 0 || $info['count'] == 0) {
return false;
}
if (@ldap_bind($ldap, $info[0]['dn'], $password)) {
return $this->create($username, $info[0][LDAP_ACCOUNT_FULLNAME][0], $info[0][LDAP_ACCOUNT_EMAIL][0]);
}
return false;
}
/**
* Create automatically a new local user after the LDAP authentication
*
* @access public
* @param string $username Username
* @param string $name Name of the user
* @param string $email Email address
* @return bool
*/
public function create($username, $name, $email)
{
$user = $this->user->getByUsername($username);
// There is an existing user account
if ($user) {
if ($user['is_ldap_user'] == 1) {
// LDAP user already created
return true;
}
else {
// There is already a local user with that username
return false;
}
}
// Create a LDAP user
$values = array(
'username' => $username,
'name' => $name,
'email' => $email,
'is_admin' => 0,
'is_ldap_user' => 1,
);
return $userModel->create($values);
}
}

View File

@@ -1,334 +0,0 @@
<?php
namespace Model;
use Core\Security;
/**
* RememberMe model
*
* @package model
* @author Frederic Guillot
*/
class RememberMe extends Base
{
/**
* 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);
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
);
}
}

View File

@@ -1,66 +0,0 @@
<?php
namespace Model;
use Core\Security;
/**
* ReverseProxyAuth model
*
* @package model
* @author Sylvain Veyrié
*/
class ReverseProxyAuth extends Base
{
/**
* 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(
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,
));
}
}

View File

@@ -311,84 +311,6 @@ class User extends Base
return array(false, $v->getErrors());
}
/**
* Validate user login
*
* @access public
* @param array $values Form values
* @return array $valid, $errors [0] = Success or not, [1] = List of errors
*/
public function validateLogin(array $values)
{
$v = new Validator($values, array(
new Validators\Required('username', t('The username is required')),
new Validators\MaxLength('username', t('The maximum length is %d characters', 50), 50),
new Validators\Required('password', t('The password is required')),
));
$result = $v->execute();
$errors = $v->getErrors();
if ($result) {
list($authenticated, $method) = $this->authenticate($values['username'], $values['password']);
if ($authenticated === true) {
// Create the user session
$user = $this->getByUsername($values['username']);
$this->updateSession($user);
// Update login history
$this->lastLogin->create(
$method,
$user['id'],
$this->getIpAddress(),
$this->getUserAgent()
);
// Setup the remember me feature
if (! empty($values['remember_me'])) {
$credentials = $this->rememberMe->create($user['id'], $this->getIpAddress(), $this->getUserAgent());
$this->rememberMe->writeCookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
}
}
else {
$result = false;
$errors['login'] = t('Bad username or password');
}
}
return array(
$result,
$errors
);
}
/**
* Authenticate a user
*
* @access public
* @param string $username Username
* @param string $password Password
* @return array
*/
public function authenticate($username, $password)
{
// Database authentication
$user = $this->db->table(self::TABLE)->eq('username', $username)->eq('is_ldap_user', 0)->findOne();
$authenticated = $user && \password_verify($password, $user['password']);
$method = LastLogin::AUTH_DATABASE;
// LDAP authentication
if (! $authenticated && LDAP_AUTH) {
$authenticated = $this->ldap->authenticate($username, $password);
$method = LastLogin::AUTH_LDAP;
}
return array($authenticated, $method);
}
/**
* Get the user agent of the connected user
*