Add the possibility to link a Google Account or a Mozilla Account to Miniflux

This commit is contained in:
Frédéric Guillot 2013-09-06 22:57:09 -04:00
parent 95f5dd1257
commit 517ac8dcf0
10 changed files with 335 additions and 13 deletions

View File

@ -464,6 +464,29 @@
} }
function mozilla_auth(action)
{
navigator.id.watch({
onlogin: function(assertion) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "?action=" + action, true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.setRequestHeader("Connection", "close");
xhr.onload = function () {
window.location.href = this.responseText;
};
xhr.send("token=" + assertion);
},
onlogout: function() {}
});
navigator.id.request();
}
document.onclick = function(e) { document.onclick = function(e) {
var action = e.target.getAttribute("data-action"); var action = e.target.getAttribute("data-action");
@ -502,6 +525,14 @@
e.preventDefault(); e.preventDefault();
download_item(); download_item();
break; break;
case 'mozilla-login':
e.preventDefault();
mozilla_auth("mozilla-auth");
break;
case 'mozilla-link':
e.preventDefault();
mozilla_auth("mozilla-link");
break;
} }
} }
}; };

8
assets/js/persona.js Normal file

File diff suppressed because one or more lines are too long

127
index.php
View File

@ -23,8 +23,9 @@ Session\open(dirname($_SERVER['PHP_SELF']));
// Called before each action // Called before each action
Router\before(function($action) { Router\before(function($action) {
if ($action !== 'login' && ! isset($_SESSION['user'])) { $ignore_actions = array('login', 'google-auth', 'google-redirect-auth', 'mozilla-auth');
if (! isset($_SESSION['user']) && ! in_array($action, $ignore_actions)) {
Response\redirect('?action=login'); Response\redirect('?action=login');
} }
@ -33,10 +34,13 @@ Router\before(function($action) {
if ($language !== 'en_US') PicoTools\Translator\load($language); if ($language !== 'en_US') PicoTools\Translator\load($language);
// HTTP secure headers // HTTP secure headers
$frame_src = \PicoFeed\Filter::$iframe_whitelist;
$frame_src[] = 'https://login.persona.org';
Response\csp(array( Response\csp(array(
'media-src' => '*', 'media-src' => '*',
'img-src' => '*', 'img-src' => '*',
'frame-src' => \PicoFeed\Filter::$iframe_whitelist 'frame-src' => $frame_src
)); ));
Response\xframe(); Response\xframe();
@ -56,9 +60,11 @@ 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('index.php'); if (isset($_SESSION['user'])) Response\redirect('?action=unread');
Response\html(Template\load('login', array( Response\html(Template\load('login', array(
'google_auth_enable' => Model\get_config_value('auth_google_token') !== '',
'mozilla_auth_enable' => Model\get_config_value('auth_mozilla_token') !== '',
'errors' => array(), 'errors' => array(),
'values' => array() 'values' => array()
))); )));
@ -74,6 +80,8 @@ Router\post_action('login', function() {
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\get_config_value('auth_google_token') !== '',
'mozilla_auth_enable' => Model\get_config_value('auth_mozilla_token') !== '',
'errors' => $errors, 'errors' => $errors,
'values' => $values 'values' => $values
))); )));
@ -658,6 +666,117 @@ Router\post_action('config', function() {
}); });
// Link to a Google Account (redirect)
Router\get_action('google-redirect-link', function() {
require 'vendor/PicoTools/AuthProvider.php';
Response\Redirect(AuthProvider\google_get_url(Helper\get_current_base_url(), '?action=google-link'));
});
// Link to a Google Account (association)
Router\get_action('google-link', function() {
require 'vendor/PicoTools/AuthProvider.php';
list($valid, $token) = AuthProvider\google_validate();
if ($valid) {
Model\save_auth_token('google', $token);
Session\flash(t('Your Google Account is linked to Miniflux.'));
}
else {
Session\flash_error(t('Unable to link Miniflux to your Google Account.'));
}
Response\redirect('?action=config');
});
// Authenticate with a Google Account (redirect)
Router\get_action('google-redirect-auth', function() {
require 'vendor/PicoTools/AuthProvider.php';
Response\Redirect(AuthProvider\google_get_url(Helper\get_current_base_url(), '?action=google-auth'));
});
// Authenticate with a Google Account (callback url)
Router\get_action('google-auth', function() {
require 'vendor/PicoTools/AuthProvider.php';
list($valid, $token) = AuthProvider\google_validate();
if ($valid && $token === Model\get_config_value('auth_google_token')) {
$_SESSION['user'] = array(
'username' => Model\get_config_value('username'),
'language' => Model\get_config_value('language'),
);
Response\redirect('?action=unread');
}
else {
Response\html(Template\load('login', array(
'google_auth_enable' => Model\get_config_value('auth_google_token') !== '',
'mozilla_auth_enable' => Model\get_config_value('auth_mozilla_token') !== '',
'errors' => array('login' => t('Unable to authenticate with Google')),
'values' => array()
)));
}
});
// Authenticate with a Mozilla Persona (ajax check)
Router\post_action('mozilla-auth', function() {
require 'vendor/PicoTools/AuthProvider.php';
list($valid, $token) = AuthProvider\mozilla_validate(Request\value('token'));
if ($valid && $token === Model\get_config_value('auth_mozilla_token')) {
$_SESSION['user'] = array(
'username' => Model\get_config_value('username'),
'language' => Model\get_config_value('language'),
);
Response\text('?action=unread');
}
else {
Response\text("?action=login");
}
});
// Link Miniflux to a Mozilla Account (ajax check)
Router\post_action('mozilla-link', function() {
require 'vendor/PicoTools/AuthProvider.php';
list($valid, $token) = AuthProvider\mozilla_validate(Request\value('token'));
if ($valid) {
Model\save_auth_token('mozilla', $token);
Session\flash(t('Your Mozilla Persona Account is linked to Miniflux.'));
}
else {
Session\flash_error(t('Unable to link Miniflux to your Mozilla Persona Account.'));
}
Response\text("?action=config");
});
// Remove account link
Router\get_action('unlink-account-provider', function() {
Model\remove_auth_token(Request\param('type'));
Response\redirect('?action=config');
});
// Display unread items // Display unread items
Router\notfound(function() { Router\notfound(function() {
@ -665,7 +784,7 @@ Router\notfound(function() {
$offset = Request\int_param('offset', 0); $offset = Request\int_param('offset', 0);
$items = Model\get_items('unread', $offset, Model\get_config_value('items_per_page')); $items = Model\get_items('unread', $offset, Model\get_config_value('items_per_page'));
$nb_items = Model\count_items('unread');; $nb_items = Model\count_items('unread');
if ($nb_items === 0) Response\redirect('?action=feeds&nothing_to_read=1'); if ($nb_items === 0) Response\redirect('?action=feeds&nothing_to_read=1');

View File

@ -1,6 +1,17 @@
<?php <?php
return array( return array(
'Your Google Account is linked to Miniflux' => 'Votre compte Google est relié à Miniflux',
'Link Miniflux to my Google account' => 'Lier Miniflux à mon compte Google',
'Your Mozilla Persona Account is linked to Miniflux' => 'Votre compte Mozilla Persona est relié à Miniflux',
'Link Miniflux to my Mozilla Persona account' => 'Lier Miniflux à mon compte Mozilla Persona',
'Your Google Account is linked to Miniflux.' => 'Votre compte Google est relié à Miniflux.',
'Unable to link Miniflux to your Google Account.' => 'Impossible de lier Miniflux à votre compte Google',
'Unable to authenticate with Google' => 'Impossible de s\'authentifier avec Google',
'Your Mozilla Persona Account is linked to Miniflux.' => 'Votre compte Mozilla Persona est lié avec Miniflux.',
'Unable to link Miniflux to your Mozilla Persona Account.' => 'Impossible de lier Miniflux avec votre compte Mozilla Persona.',
'Login with my Google Account' => 'Se connecter avec mon compte Google',
'Login with my Mozilla Persona Account' => 'Se connecter avec mon compte Mozilla Persona',
'Bookmarklet:' => 'Bookmarklet :', 'Bookmarklet:' => 'Bookmarklet :',
'Subscribe with Miniflux' => 'S\'abonner avec Miniflux', 'Subscribe with Miniflux' => 'S\'abonner avec Miniflux',
'Drag and drop this link to your bookmarks' => 'Glisser-déposer ce lien dans vos favoris', 'Drag and drop this link to your bookmarks' => 'Glisser-déposer ce lien dans vos favoris',

View File

@ -24,7 +24,7 @@ use PicoFeed\Reader;
use PicoFeed\Export; use PicoFeed\Export;
const DB_VERSION = 15; const DB_VERSION = 16;
const HTTP_USERAGENT = 'Miniflux - http://miniflux.net'; const HTTP_USERAGENT = 'Miniflux - http://miniflux.net';
const HTTP_FAKE_USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36'; const HTTP_FAKE_USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36';
const LIMIT_ALL = -1; const LIMIT_ALL = -1;
@ -126,6 +126,28 @@ function new_tokens()
} }
function save_auth_token($type, $value)
{
return \PicoTools\singleton('db')
->table('config')
->update(array(
'auth_'.$type.'_token' => $value
));
}
function remove_auth_token($type)
{
\PicoTools\singleton('db')
->table('config')
->update(array(
'auth_'.$type.'_token' => ''
));
$_SESSION['config'] = get_config();
}
function export_feeds() function export_feeds()
{ {
$opml = new Export(get_feeds()); $opml = new Export(get_feeds());
@ -885,7 +907,18 @@ function get_config()
{ {
return \PicoTools\singleton('db') return \PicoTools\singleton('db')
->table('config') ->table('config')
->columns('username', 'language', 'autoflush', 'nocontent', 'items_per_page', 'theme', 'api_token', 'feed_token') ->columns(
'username',
'language',
'autoflush',
'nocontent',
'items_per_page',
'theme',
'api_token',
'feed_token',
'auth_google_token',
'auth_mozilla_token'
)
->findOne(); ->findOne();
} }
@ -976,11 +1009,8 @@ function save_config(array $values)
{ {
// Update the password if needed // Update the password if needed
if (! empty($values['password'])) { if (! empty($values['password'])) {
$values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT); $values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT);
} else { } else {
unset($values['password']); unset($values['password']);
} }

View File

@ -3,6 +3,13 @@
namespace Schema; namespace Schema;
function version_16($pdo)
{
$pdo->exec('ALTER TABLE config ADD COLUMN auth_google_token TEXT DEFAULT ""');
$pdo->exec('ALTER TABLE config ADD COLUMN auth_mozilla_token TEXT DEFAULT ""');
}
function version_15($pdo) function version_15($pdo)
{ {
$pdo->exec('ALTER TABLE feeds ADD COLUMN download_content INTEGER DEFAULT 0'); $pdo->exec('ALTER TABLE feeds ADD COLUMN download_content INTEGER DEFAULT 0');

View File

@ -11,7 +11,7 @@
<link rel="apple-touch-icon" sizes="144x144" href="./assets/img/touch-icon-ipad-retina.png"> <link rel="apple-touch-icon" sizes="144x144" href="./assets/img/touch-icon-ipad-retina.png">
<title><?= isset($title) ? Helper\escape($title) : 'miniflux' ?></title> <title><?= isset($title) ? Helper\escape($title) : 'miniflux' ?></title>
<link href="<?= Helper\css() ?>" rel="stylesheet" media="screen"> <link href="<?= Helper\css() ?>" rel="stylesheet" media="screen">
<script type="text/javascript" src="./assets/js/app.js?version=<?= filemtime('assets/js/app.js') ?>" defer></script> <script type="text/javascript" src="assets/js/app.js?version=<?= filemtime('assets/js/app.js') ?>" defer></script>
</head> </head>
<body> <body>
<header> <header>

View File

@ -27,6 +27,23 @@
<?= Helper\form_checkbox('nocontent', t('Do not fetch the content of articles'), 1, isset($values['nocontent']) ? $values['nocontent'] : false) ?><br /> <?= Helper\form_checkbox('nocontent', t('Do not fetch the content of articles'), 1, isset($values['nocontent']) ? $values['nocontent'] : false) ?><br />
<ul>
<li>
<?php if ($values['auth_google_token']): ?>
<?= t('Your Google Account is linked to Miniflux') ?>, <a href="?action=unlink-account-provider&amp;type=google"><?= t('remove') ?></a>
<?php else: ?>
<a href="?action=google-redirect-link"><?= t('Link Miniflux to my Google account') ?></a>
<?php endif ?>
</li>
<li>
<?php if ($values['auth_mozilla_token']): ?>
<?= t('Your Mozilla Persona Account is linked to Miniflux') ?>, <a href="?action=unlink-account-provider&amp;type=mozilla"><?= t('remove') ?></a>
<?php else: ?>
<a href="#" data-action="mozilla-link"><?= t('Link Miniflux to my Mozilla Persona account') ?></a>
<?php endif ?>
</li>
</ul>
<div class="form-actions"> <div class="form-actions">
<input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/> <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
</div> </div>
@ -70,4 +87,6 @@
<li><a href="?action=console"><?= t('Console') ?></a></li> <li><a href="?action=console"><?= t('Console') ?></a></li>
</ul> </ul>
</div> </div>
</section> </section>
<script type="text/javascript" src="assets/js/persona.js" async></script>

View File

@ -11,6 +11,10 @@
<link rel="apple-touch-icon" sizes="144x144" href="./assets/img/touch-icon-ipad-retina.png"> <link rel="apple-touch-icon" sizes="144x144" href="./assets/img/touch-icon-ipad-retina.png">
<title>miniflux</title> <title>miniflux</title>
<link href="<?= Helper\css() ?>" rel="stylesheet" media="screen"> <link href="<?= Helper\css() ?>" rel="stylesheet" media="screen">
<script type="text/javascript" src="assets/js/app.js?version=<?= filemtime('assets/js/app.js') ?>" defer></script>
<?php if ($mozilla_auth_enable): ?>
<script type="text/javascript" src="assets/js/persona.js" defer></script>
<?php endif ?>
</head> </head>
<body id="login-page"> <body id="login-page">
<section class="page"> <section class="page">
@ -20,9 +24,7 @@
<section> <section>
<?php if (isset($errors['login'])): ?> <?php if (isset($errors['login'])): ?>
<p class="alert alert-error"><?= Helper\escape($errors['login']) ?></p> <p class="alert alert-error"><?= Helper\escape($errors['login']) ?></p>
<?php endif ?> <?php endif ?>
<form method="post" action="?action=login"> <form method="post" action="?action=login">
@ -33,6 +35,14 @@
<?= 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')) ?>
<?php if ($google_auth_enable): ?>
<p><br/><a href="?action=google-redirect-auth"><?= t('Login with my Google Account') ?></a></p>
<?php endif ?>
<?php if ($mozilla_auth_enable): ?>
<p><br/><a href="#" data-action="mozilla-login"><?= t('Login with my Mozilla Persona Account') ?></a></p>
<?php endif ?>
<div class="form-actions"> <div class="form-actions">
<input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/> <input type="submit" value="<?= t('Sign in') ?>" class="btn btn-blue"/>
</div> </div>

87
vendor/PicoTools/AuthProvider.php vendored Normal file
View File

@ -0,0 +1,87 @@
<?php
namespace AuthProvider;
function google_get_url($realm, $return_path)
{
$return_to = $realm.$return_path;
$url = 'https://accounts.google.com/o/openid2/auth?';
$params = array();
$params['openid.ns'] = 'http://specs.openid.net/auth/2.0';
$params['openid.mode'] = 'checkid_setup';
$params['openid.return_to'] = $return_to;
$params['openid.realm'] = $realm;
$params['openid.identity'] = 'http://specs.openid.net/auth/2.0/identifier_select';
$params['openid.claimed_id'] = 'http://specs.openid.net/auth/2.0/identifier_select';
return $url.http_build_query($params, '', '&');
}
function google_validate()
{
$identity = '';
if (! ini_get('allow_url_fopen')) {
die('You must have "allow_url_fopen=On" to use this feature!');
}
if (! isset($_GET['openid_mode']) || $_GET['openid_mode'] !== 'id_res') {
return array(false, $identity);
}
$params = array();
$params['openid.ns'] = 'http://specs.openid.net/auth/2.0';
$params['openid.mode'] = 'check_authentication';
$params['openid.assoc_handle'] = $_GET['openid_assoc_handle'];
$params['openid.signed'] = $_GET['openid_signed'];
$params['openid.sig'] = $_GET['openid_sig'];
foreach (explode(',', $_GET['openid_signed']) as $item) {
$params['openid.'.$item] = $_GET['openid_' . str_replace('.', '_', $item)];
}
$context = stream_context_create(array(
'http'=>array(
'method'=> 'POST',
'header'=> implode("\r\n", array(
'Content-type: application/x-www-form-urlencoded',
'Accept: application/xrds+xml, */*'
)),
'content' => http_build_query($params, '', '&')
)));
$response = file_get_contents('https://www.google.com/accounts/o8/ud', false, $context);
$identity = $_GET['openid_identity'];
return array(strpos($response, 'is_valid:true') !== false, $identity);
}
function mozilla_validate($token)
{
if (! ini_get('allow_url_fopen')) {
die('You must have "allow_url_fopen=On" to use this feature!');
}
$params = array(
'assertion' => $token,
'audience' => (isset($_SERVER['HTTPS']) ? 'https://' : 'http://').$_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT']
);
$context = stream_context_create(array(
'http'=> array(
'method'=> 'POST',
'header'=> implode("\r\n", array(
'Content-type: application/x-www-form-urlencoded',
)),
'content' => http_build_query($params, '', '&')
)));
$body = file_get_contents('https://verifier.login.persona.org/verify', false, $context);
$response = json_decode($body, true);
return array(
$response['status'] === 'okay',
$response['email']
);
}