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

60
app/Auth/Base.php Normal file
View File

@@ -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);
}
}

52
app/Auth/Database.php Normal file
View File

@@ -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;
}
}

178
app/Auth/GitHub.php Normal file
View File

@@ -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;
}
}

153
app/Auth/Google.php Normal file
View File

@@ -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;
}
}

150
app/Auth/Ldap.php Normal file
View File

@@ -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);
}
}

349
app/Auth/RememberMe.php Normal file
View File

@@ -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
);
}
}

73
app/Auth/ReverseProxy.php Normal file
View File

@@ -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,
));
}
}