Add RememberMe authentication

This commit is contained in:
Frédéric Guillot 2014-05-26 20:47:40 -04:00
parent 0146e96fcf
commit 7e553f72fd
9 changed files with 402 additions and 6 deletions

View File

@ -23,12 +23,14 @@ require __DIR__.'/models/item.php';
require __DIR__.'/models/schema.php'; require __DIR__.'/models/schema.php';
require __DIR__.'/models/auto_update.php'; require __DIR__.'/models/auto_update.php';
require __DIR__.'/models/database.php'; require __DIR__.'/models/database.php';
require __DIR__.'/models/remember_me.php';
if (file_exists('config.php')) require 'config.php'; if (file_exists('config.php')) require 'config.php';
defined('APP_VERSION') or define('APP_VERSION', 'master'); defined('APP_VERSION') or define('APP_VERSION', 'master');
defined('HTTP_TIMEOUT') or define('HTTP_TIMEOUT', 20); defined('HTTP_TIMEOUT') or define('HTTP_TIMEOUT', 20);
defined('BASE_URL_DIRECTORY') or define('BASE_URL_DIRECTORY', dirname($_SERVER['PHP_SELF']));
defined('ROOT_DIRECTORY') or define('ROOT_DIRECTORY', __DIR__); defined('ROOT_DIRECTORY') or define('ROOT_DIRECTORY', __DIR__);
defined('DATA_DIRECTORY') or define('DATA_DIRECTORY', 'data'); defined('DATA_DIRECTORY') or define('DATA_DIRECTORY', 'data');

View File

@ -9,7 +9,7 @@ use PicoFarad\Template;
// Called before each action // Called before each action
Router\before(function($action) { Router\before(function($action) {
Session\open(dirname($_SERVER['PHP_SELF']), SESSION_SAVE_PATH); Session\open(BASE_URL_DIRECTORY, SESSION_SAVE_PATH);
// Select another database // Select another database
if (! empty($_SESSION['database'])) { if (! empty($_SESSION['database'])) {
@ -20,8 +20,14 @@ Router\before(function($action) {
$ignore_actions = array('login', 'google-auth', 'google-redirect-auth', 'mozilla-auth', 'bookmark-feed', 'select-db'); $ignore_actions = array('login', 'google-auth', 'google-redirect-auth', 'mozilla-auth', 'bookmark-feed', 'select-db');
if (! isset($_SESSION['user']) && ! in_array($action, $ignore_actions)) { if (! isset($_SESSION['user']) && ! in_array($action, $ignore_actions)) {
if (! Model\RememberMe\authenticate()) {
Response\redirect('?action=login'); Response\redirect('?action=login');
} }
}
else if (Model\RememberMe\has_cookie()) {
Model\RememberMe\refresh();
}
// Load translations // Load translations
$language = Model\Config\get('language') ?: 'en_US'; $language = Model\Config\get('language') ?: 'en_US';

View File

@ -11,6 +11,7 @@ use PicoFarad\Template;
// Logout and destroy session // Logout and destroy session
Router\get_action('logout', function() { Router\get_action('logout', function() {
Model\RememberMe\destroy();
Session\close(); Session\close();
Response\redirect('?action=login'); Response\redirect('?action=login');
}); });
@ -18,7 +19,9 @@ Router\get_action('logout', function() {
// Display form login // Display form login
Router\get_action('login', function() { Router\get_action('login', function() {
if (isset($_SESSION['user'])) Response\redirect('?action=unread'); if (isset($_SESSION['user'])) {
Response\redirect('?action=unread');
}
Response\html(Template\load('login', array( Response\html(Template\load('login', array(
'google_auth_enable' => Model\Config\get('auth_google_token') !== '', 'google_auth_enable' => Model\Config\get('auth_google_token') !== '',
@ -36,7 +39,9 @@ Router\post_action('login', function() {
$values = Request\values(); $values = Request\values();
list($valid, $errors) = Model\User\validate_login($values); list($valid, $errors) = Model\User\validate_login($values);
if ($valid) Response\redirect('?action=unread'); if ($valid) {
Response\redirect('?action=unread');
}
Response\html(Template\load('login', array( Response\html(Template\load('login', array(
'google_auth_enable' => Model\Config\get('auth_google_token') !== '', 'google_auth_enable' => Model\Config\get('auth_google_token') !== '',

View File

@ -223,4 +223,6 @@ return array(
'Unable to create the new database.' => 'Impossible créer la nouvelle base de données.', 'Unable to create the new database.' => 'Impossible créer la nouvelle base de données.',
'Add a new database (new user)' => 'Ajouter une nouvelle base de données (nouvel utilisateur)', 'Add a new database (new user)' => 'Ajouter une nouvelle base de données (nouvel utilisateur)',
'Create' => 'Créer', 'Create' => 'Créer',
'Unknown' => 'Inconnu',
'Remember Me' => 'Connexion automatique',
); );

View File

@ -8,7 +8,7 @@ use PicoDb\Database;
use PicoFeed\Config as ReaderConfig; use PicoFeed\Config as ReaderConfig;
use PicoFeed\Logging; use PicoFeed\Logging;
const DB_VERSION = 24; const DB_VERSION = 25;
const HTTP_USER_AGENT = 'Miniflux (http://miniflux.net)'; const HTTP_USER_AGENT = 'Miniflux (http://miniflux.net)';
// Get PicoFeed config // Get PicoFeed config
@ -297,3 +297,48 @@ function save(array $values)
return Database::get('db')->table('config')->update($values); return Database::get('db')->table('config')->update($values);
} }
// Get the user agent of the connected user
function get_user_agent()
{
return empty($_SERVER['HTTP_USER_AGENT']) ? t('Unknown') : $_SERVER['HTTP_USER_AGENT'];
}
// Get the real IP address of the connected user
function get_ip_address($only_public = false)
{
$keys = array(
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR'
);
foreach ($keys as $key) {
if (isset($_SERVER[$key])) {
foreach (explode(',', $_SERVER[$key]) as $ip_address) {
$ip_address = trim($ip_address);
if ($only_public) {
// Return only public IP address
if (filter_var($ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
return $ip_address;
}
}
else {
return $ip_address;
}
}
}
}
return t('Unknown');
}

307
models/remember_me.php Normal file
View File

@ -0,0 +1,307 @@
<?php
namespace Model\RememberMe;
use PicoDb\Database;
use Model\Config;
use Model\User;
use Model\Database as DatabaseModel;
const TABLE = 'remember_me';
const COOKIE_NAME = '_R_';
const EXPIRATION = 5184000;
/**
* Get a remember me record
*
* @access public
* @return mixed
*/
function find($token, $sequence)
{
return Database::get('db')
->table(TABLE)
->eq('token', $token)
->eq('sequence', $sequence)
->gt('expiration', time())
->findOne();
}
/**
* Get all sessions
*
* @access public
* @return array
*/
function get_all()
{
return Database::get('db')
->table(TABLE)
->desc('date_creation')
->columns('id', 'ip', 'user_agent', 'date_creation', 'expiration')
->findAll();
}
/**
* Authenticate the user with the cookie
*
* @access public
* @return bool
*/
function authenticate()
{
$credentials = read_cookie();
if ($credentials !== false) {
$record = find($credentials['token'], $credentials['sequence']);
if ($record) {
// Update the sequence
write_cookie(
$record['token'],
update($record['token'], $record['sequence']),
$record['expiration']
);
// Create the session
$_SESSION['user'] = User\get($record['username']);
$_SESSION['config'] = Config\get_all();
return true;
}
}
return false;
}
/**
* Update the database and the cookie with a new sequence
*
* @access public
*/
function refresh()
{
$credentials = read_cookie();
if ($credentials !== false) {
$record = find($credentials['token'], $credentials['sequence']);
if ($record) {
// Update the sequence
write_cookie(
$record['token'],
update($record['token'], $record['sequence']),
$record['expiration']
);
}
}
}
/**
* Remove a session record
*
* @access public
* @param integer $session_id Session id
* @return mixed
*/
function remove($session_id)
{
return Database::get('db')
->table(TABLE)
->eq('id', $session_id)
->remove();
}
/**
* Remove the current RememberMe session and the cookie
*
* @access public
* @param integer $user_id User id
*/
function destroy()
{
$credentials = read_cookie();
if ($credentials !== false) {
delete_cookie();
Database::get('db')
->table(TABLE)
->eq('token', $credentials['token'])
->remove();
}
}
/**
* Create a new RememberMe session
*
* @access public
* @param integer $dbname Database name
* @param integer $username Username
* @param string $ip IP Address
* @param string $user_agent User Agent
* @return array
*/
function create($dbname, $username, $ip, $user_agent)
{
$token = hash('sha256', $dbname.$username.$user_agent.$ip.Config\generate_token());
$sequence = Config\generate_token();
$expiration = time() + EXPIRATION;
cleanup();
Database::get('db')
->table(TABLE)
->insert(array(
'username' => $username,
'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
*
* @access public
* @return bool
*/
function cleanup()
{
return Database::get('db')
->table(TABLE)
->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
*/
function update($token, $sequence)
{
$new_sequence = Config\generate_token();
Database::get('db')
->table(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
*/
function encode_cookie($token, $sequence)
{
return implode('|', array(base64_encode(DatabaseModel\select()), $token, $sequence));
}
/**
* Decode the value of a cookie
*
* @access public
* @param string $value Raw cookie data
* @return array
*/
function decode_cookie($value)
{
@list($database, $token, $sequence) = explode('|', $value);
DatabaseModel\select(base64_decode($database));
return array(
'token' => $token,
'sequence' => $sequence,
);
}
/**
* Return true if the current user has a RememberMe cookie
*
* @access public
* @return bool
*/
function has_cookie()
{
return ! empty($_COOKIE[COOKIE_NAME]);
}
/**
* Write and encode the cookie
*
* @access public
* @param string $token Session token
* @param string $sequence Sequence token
* @param string $expiration Cookie expiration
*/
function write_cookie($token, $sequence, $expiration)
{
setcookie(
COOKIE_NAME,
encode_cookie($token, $sequence),
$expiration,
BASE_URL_DIRECTORY,
null,
! empty($_SERVER['HTTPS']),
true
);
}
/**
* Read and decode the cookie
*
* @access public
* @return mixed
*/
function read_cookie()
{
if (empty($_COOKIE[COOKIE_NAME])) {
return false;
}
return decode_cookie($_COOKIE[COOKIE_NAME]);
}
/**
* Remove the cookie
*
* @access public
*/
function delete_cookie()
{
setcookie(
COOKIE_NAME,
'',
time() - 3600,
BASE_URL_DIRECTORY,
null,
! empty($_SERVER['HTTPS']),
true
);
}

View File

@ -2,6 +2,24 @@
namespace Schema; namespace Schema;
function version_25($pdo)
{
$pdo->exec(
'CREATE TABLE remember_me (
id INTEGER PRIMARY KEY,
username TEXT,
ip TEXT,
user_agent TEXT,
token TEXT,
sequence TEXT,
expiration INTEGER,
date_creation INTEGER
)'
);
}
function version_24($pdo) function version_24($pdo)
{ {
$pdo->exec("ALTER TABLE config ADD COLUMN auto_update_url TEXT DEFAULT 'https://github.com/fguillot/miniflux/archive/master.zip'"); $pdo->exec("ALTER TABLE config ADD COLUMN auto_update_url TEXT DEFAULT 'https://github.com/fguillot/miniflux/archive/master.zip'");

View File

@ -5,6 +5,9 @@ namespace Model\User;
use SimpleValidator\Validator; use SimpleValidator\Validator;
use SimpleValidator\Validators; use SimpleValidator\Validators;
use PicoDb\Database; use PicoDb\Database;
use Model\Config;
use Model\RememberMe;
use Model\Database as DatabaseModel;
// Get a user by username // Get a user by username
function get($username) function get($username)
@ -37,7 +40,13 @@ function validate_login(array $values)
unset($user['password']); unset($user['password']);
$_SESSION['user'] = $user; $_SESSION['user'] = $user;
$_SESSION['config'] = \Model\Config\get_all(); $_SESSION['config'] = Config\get_all();
// Setup the remember me feature
if (! empty($values['remember_me'])) {
$credentials = RememberMe\create(DatabaseModel\select(), $values['username'], Config\get_ip_address(), Config\get_user_agent());
RememberMe\write_cookie($credentials['token'], $credentials['sequence'], $credentials['expiration']);
}
} }
else { else {

View File

@ -36,6 +36,8 @@
<?= Helper\form_label(t('Password'), 'password') ?> <?= Helper\form_label(t('Password'), 'password') ?>
<?= Helper\form_password('password', $values, $errors, array('required')) ?> <?= Helper\form_password('password', $values, $errors, array('required')) ?>
<?= Helper\form_checkbox('remember_me', t('Remember Me'), 1) ?><br/>
<?php if ($google_auth_enable): ?> <?php if ($google_auth_enable): ?>
<p><br/><a href="?action=google-redirect-auth"><?= t('Login with my Google Account') ?></a></p> <p><br/><a href="?action=google-redirect-auth"><?= t('Login with my Google Account') ?></a></p>
<?php endif ?> <?php endif ?>