Merge pull request #1735 from RaspAP/feat/session-manager

Feature: Session manager & dedicated login
This commit is contained in:
Bill Zimmerman 2025-01-22 15:11:30 +01:00 committed by GitHub
commit 7a9e5169f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 305 additions and 46 deletions

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require_once '../../includes/config.php';
require_once '../../includes/session.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';
require_once '../../includes/session.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../src/RaspAP/Parsers/IwParser.php';

View File

@ -1,7 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/functions.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -0,0 +1,32 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';
$lastActivity = $_SESSION['lastActivity'] ?? time();
$sessionLifetime = time() - $lastActivity;
$status = $sessionLifetime >= RASPI_SESSION_TIMEOUT ? 'session_expired' : 'active';
if ($status = 'session_expired') {
session_unset(); // unset all session variables
session_destroy(); // destroy the session
}
// send response
header('Content-Type: application/json');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
header('Pragma: no-cache');
$response = [
'status' => $status,
'last_activity' => $lastActivity,
'session_lifetime' => $sessionLifetime
];
echo json_encode($response);
exit();

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../includes/defaults.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require '../../includes/csrf.php';
require_once '../../includes/session.php';
require_once '../../includes/config.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -1,6 +1,7 @@
<?php
require_once '../../includes/config.php';
require_once '../../includes/session.php';
require_once '../../src/RaspAP/Auth/HTTPAuth.php';
require_once '../../includes/authenticate.php';

View File

@ -10,6 +10,7 @@ License: GNU General Public License v3.0
--raspap-content-main: #495057;
--raspap-text-muted: #858796;
--raspap-brand-color: #2b8080;
--raspap-offwhite: #faf9f6;
}
a {
@ -332,3 +333,47 @@ button > i.fas {
animation: heart 1000ms infinite;
}
#modal-admin-login .modal-content {
background: radial-gradient(circle at 120% -20%, #032626, #052c2c, #073232, #0a3838, #0d3f3f, #114545, #144c4c);
align-items: center;
}
#modal-admin-login .modal-body {
min-width: 330px;
}
.login-brand {
color: var(--raspap-theme-color);
filter: brightness(150%);
}
.admin-login {
color: var(--raspap-offwhite);
font-size: 1.2em
}
.btn-admin-login {
color: var(--raspap-offwhite);
background-color: var(--raspap-theme-color);
}
.btn-admin-login:hover {
color: var(--raspap-offwhite);
background-color: #236969;
}
.no-right-radius {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
.btn-passwd-append {
border: 1px solid #ced4da;
}
#passwd-toggle:active,
#passwd-toggle:hover,
#passwd-toggle:focus {
border: 1px solid #ced4da;
}

0
app/img/raspAP-logo.php Normal file → Executable file
View File

0
app/img/wg-qr-code.php Normal file → Executable file
View File

0
app/img/wifi-qr-code.php Normal file → Executable file
View File

View File

@ -671,6 +671,44 @@ window.addEventListener('load', function() {
});
}, false);
let sessionCheckInterval = setInterval(checkSession, 5000);
function checkSession() {
// skip session check if on login page
if (window.location.pathname === '/login') {
return;
}
var csrfToken = $('meta[name=csrf_token]').attr('content');
$.post('ajax/session/do_check_session.php',{'csrf_token': csrfToken},function (data) {
if (data.status === 'session_expired') {
clearInterval(sessionCheckInterval);
showSessionExpiredModal();
}
}).fail(function (jqXHR, status, err) {
console.error("Error checking session status:", status, err);
});
}
function showSessionExpiredModal() {
$('#sessionTimeoutModal').modal('show');
}
$(document).on("click", "#js-session-expired-login", function(e) {
const loginModal = $('#modal-admin-login');
const redirectUrl = window.location.pathname;
window.location.href = `/login?action=${encodeURIComponent(redirectUrl)}`;
});
// show modal login on page load
$(document).ready(function () {
const params = new URLSearchParams(window.location.search);
const redirectUrl = $('#redirect-url').val() || params.get('action') || '/';
$('#modal-admin-login').modal('show');
$('#redirect-url').val(redirectUrl);
$('#username').focus();
$('#username').addClass("focusedInput");
});
// DHCP or Static IP option group
$('#chkstatic').on('change', function() {
if (this.checked) {

View File

@ -1,6 +1,7 @@
<?php
define('RASPI_BRAND_TEXT', 'RaspAP');
define('RASPI_BRAND_TITLE', RASPI_BRAND_TEXT.' Admin Panel');
define('RASPI_CONFIG', '/etc/raspap');
define('RASPI_CONFIG_NETWORK', RASPI_CONFIG.'/networking/defaults.json');
define('RASPI_CONFIG_PROVIDERS', 'config/vpn-providers.json');
@ -11,6 +12,7 @@ define('RASPI_CACHE_PATH', sys_get_temp_dir() . '/raspap');
define('RASPI_ERROR_LOG', sys_get_temp_dir() . '/raspap_error.log');
define('RASPI_DEBUG_LOG', 'raspap_debug.log');
define('RASPI_LOG_SIZE_LIMIT', 64);
define('RASPI_SESSION_TIMEOUT', 1440);
// Constants for configuration file paths.
// These are typical for default RPi installs. Modify if needed.

View File

@ -1,16 +1,10 @@
<?php
if (RASPI_AUTH_ENABLED) {
$user = $_SERVER['PHP_AUTH_USER'] ?? '';
$pass = $_SERVER['PHP_AUTH_PW'] ?? '';
$auth = new \RaspAP\Auth\HTTPAuth;
if (!$auth->isLogged()) {
if ($auth->login($user, $pass)) {
$config = $auth->getAuthConfig();
} else {
$auth->authenticate();
}
}
}

View File

@ -1,7 +1,6 @@
<?php
require_once 'functions.php';
require_once 'session.php';
if (csrfValidateRequest() && !CSRFValidate()) {
handleInvalidCSRFToken();

View File

@ -6,6 +6,7 @@ if (!defined('RASPI_CONFIG')) {
$defaults = [
'RASPI_BRAND_TEXT' => 'RaspAP',
'RASPI_BRAND_TITLE' => RASPI_BRAND_TEXT.' Admin Panel',
'RASPI_VERSION' => '3.2.4',
'RASPI_CONFIG_NETWORK' => RASPI_CONFIG.'/networking/defaults.json',
'RASPI_CONFIG_PROVIDERS' => 'config/vpn-providers.json',
@ -16,6 +17,7 @@ $defaults = [
'RASPI_ERROR_LOG' => sys_get_temp_dir() . '/raspap_error.log',
'RASPI_DEBUG_LOG' => 'raspap_debug.log',
'RASPI_LOG_SIZE_LIMIT' => 64,
'RASPI_SESSION_TIMEOUT' => 1440,
// Constants for configuration file paths.
// These are typical for default RPi installs. Modify if needed.

18
includes/footer.php Normal file → Executable file
View File

@ -1,3 +1,5 @@
<?php $_SESSION['lastActivity'] = time(); ?>
<div class="d-flex align-items-center justify-content-between small">
<div class="text-muted">
<span class="pe-2"><a href="/about">v<?php echo RASPI_VERSION; ?></a></span> |
@ -8,3 +10,19 @@
</div>
</div>
<div class="modal fade" data-bs-backdrop="static" data-bs-keyboard="false" id="sessionTimeoutModal" tabindex="-1" aria-labelledby="sessionTimeoutLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title" id="sessionTimeoutLabel"><i class="fa fa-clock me-2"></i><?php echo _("Session Expired"); ?></div>
</div>
<div class="modal-body">
<?php echo _("Your session has expired. Please login to continue.") ?>
</div>
<div class="modal-footer">
<button type="button" id="js-session-expired-login" class="btn btn-outline btn-primary"><?php echo _("Login"); ?></button>
</div>
</div>
</div>
</div>

View File

@ -1,3 +1,4 @@
<?php require_once 'session.php'; ?>
<?php
/* Functions for Networking */
@ -336,25 +337,28 @@ function CSRFMetaTag()
*/
function CSRFValidate()
{
if(isset($_POST['csrf_token'])) {
$post_token = $_POST['csrf_token'];
if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
error_log('Session expired or CSRF token is missing.');
header('Location: /login');
exit;
}
$post_token = $_POST['csrf_token'] ?? null;
$header_token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;
if (empty($post_token) && is_null($header_token)) {
error_log('CSRF token missing in the request');
return false;
}
$request_token = $post_token;
if (empty($post_token)) {
$request_token = $header_token;
}
$request_token = $post_token ?: $header_token;
if (hash_equals($_SESSION['csrf_token'], $request_token)) {
return true;
} else {
error_log('CSRF violation');
error_log('CSRF token mismatch');
return false;
}
}
}
/**
* Should the request be CSRF-validated?

40
includes/login.php Executable file
View File

@ -0,0 +1,40 @@
<?php
require_once 'includes/config.php';
require_once 'includes/functions.php';
/**
* Handler for administrative user login
*/
function DisplayLogin()
{
// initialize auth object
$auth = new \RaspAP\Auth\HTTPAuth;
$status = null;
$redirectUrl = null;
// handle page action
if (RASPI_AUTH_ENABLED) {
if (isset($_POST['login-auth'])) {
// authenticate user
$username = $_POST['username'];
$password = $_POST['password'];
$redirectUrl = $_POST['redirect-url'];
if ($auth->login($username, $password)) {
$config = $auth->getAuthConfig();
header('Location: ' . $redirectUrl);
die();
} else {
$status = "Login failed";
}
}
}
echo renderTemplate(
"login", compact(
"status",
"redirectUrl"
)
);
}

View File

@ -70,6 +70,9 @@ function handleCorePageAction(string $page, array &$extraFooterScripts): void
case "/about":
DisplayAbout();
break;
case "/login":
DisplayLogin();
break;
default:
DisplayDashboard($extraFooterScripts);
}

0
includes/restapi.php Normal file → Executable file
View File

View File

@ -3,3 +3,8 @@
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['lastActivity'])) {
$_SESSION['lastActivity'] = time();
}

View File

@ -23,6 +23,7 @@
* as you leave these references intact in the header comments of your source files.
*/
require 'includes/session.php';
require 'includes/csrf.php';
ensureCSRFSessionToken();
@ -35,6 +36,7 @@ require_once 'includes/functions.php';
// Default page actions
require_once 'includes/dashboard.php';
require_once 'includes/login.php';
require_once 'includes/authenticate.php';
require_once 'includes/admin.php';
require_once 'includes/dhcp.php';
@ -63,7 +65,7 @@ initializeApp();
<meta name="description" content="">
<meta name="author" content="">
<title><?php echo _("RaspAP WiFi Configuration Portal"); ?></title>
<title><?php echo RASPI_BRAND_TITLE; ?></title>
<!-- Bootstrap Core CSS -->
<link href="dist/bootstrap/css/bootstrap.min.css" rel="stylesheet">
@ -96,6 +98,7 @@ initializeApp();
<body class="sb-nav-fixed">
<!-- Navbar -->
<?php ob_start(); ?>
<?php require_once 'includes/navbar.php'; ?>
<!-- End of Navbar -->
<div id="layoutSidenav">
@ -123,6 +126,7 @@ initializeApp();
</footer>
</div>
</div>
<?php ob_end_flush(); ?>
<!-- jQuery -->
<script src="dist/jquery/jquery.min.js"></script>

Binary file not shown.

View File

@ -1566,3 +1566,23 @@ msgstr "Restarting restapi.service"
msgid "Information provided by restapi.service"
msgstr "Information provided by restapi.service"
#: includes/login.php
msgid "Session Expired"
msgstr "Session Expired"
msgid "Your session has expired. Please login to continue."
msgstr "Your session has expired. Please login to continue."
msgid "Login"
msgstr "Login"
msgid "Administrator login"
msgstr "Administrator login"
msgid "Forgot password"
msgstr "Forgot password"
msgid "Login failed"
msgstr "Login failed"

View File

@ -15,12 +15,6 @@ namespace RaspAP\Auth;
class HTTPAuth
{
/**
* @var string $realm
*/
public $realm = 'Authentication Required';
/**
* Stored login credentials
* @var array $auth_config
@ -57,15 +51,11 @@ class HTTPAuth
public function authenticate()
{
if (!$this->isLogged()) {
header('HTTP/1.0 401 Unauthorized');
header('WWW-Authenticate: Basic realm="'.$this->realm.'"');
if (function_exists('http_response_code')) {
// http_response_code will respond with proper HTTP version
http_response_code(401);
} else {
header('HTTP/1.0 401 Unauthorized');
$redirectUrl = $_SERVER['REQUEST_URI'];
if (strpos($redirectUrl, '/login') === false) {
header('Location: /login?action=' . urlencode($redirectUrl));
exit();
}
exit('Not authorized'.PHP_EOL);
}
}

42
templates/login.php Executable file
View File

@ -0,0 +1,42 @@
<!-- fullscreen modal -->
<div class="modal" id="modal-admin-login" data-bs-backdrop="static" data-bs-keyboard="false" role="dialog" aria-labelledby="ModalLabel" aria-hidden="true">
<div class="modal-dialog modal-fullscreen" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="row h-100 justify-content-center align-items-center">
<div class="col-12">
<!-- branding -->
<div class="text-center mb-3">
<img src="app/img/raspAP-logo.php" class="navbar-logo" alt="RaspAP logo" class="img-fluid" style="max-width: 100px;">
<h2 class="login-brand"><?php echo htmlspecialchars(RASPI_BRAND_TEXT); ?></h2>
<div class="mt-2 admin-login"><?php echo _("Administrator login") ?></div>
<div class="text-center text-danger mt-1 mb-3"><?php echo $status ?></div>
</div>
<div class="text-center mb-4">
<form id="admin-login-form" action="login" method="POST" class="needs-validation" novalidate>
<?php echo CSRFTokenFieldTag() ?>
<div class="form-group">
<input type="hidden" name="login-auth">
<input type="hidden" id="redirect-url" name="redirect-url" value="<?php echo htmlspecialchars($redirectUrl, ENT_QUOTES, 'UTF-8'); ?>">
<input type="text" class="form-control" id="username" name="username" placeholder="<?php echo _("Username") ?>" required>
</div>
<div class="mt-2">
<div class="input-group has-validation">
<input type="password" class="form-control rounded-start border-end-0 no-right-radius" id="password" name="password" placeholder="<?php echo _("Password") ?>" required>
<button class="btn bg-white btn-passwd-append border-start-0 js-toggle-password" type="button" id="passwd-toggle" data-bs-target="[name=password]" data-toggle-with="fas fa-eye-slash text-secondary text-opacity-50">
<i class="fas fa-eye text-secondary text-opacity-50"></i>
</button>
</div>
</div>
<button type="submit" class="btn btn-outline btn-admin-login rounded-pill w-75 mt-4"><?php echo _("Login") ?></button>
<div class="small mt-2"><a href="https://docs.raspap.com/authentication/#restoring-defaults" target="_blank"><?php echo _("Forgot password") ?></a></div>
</form>
</div>
</div><!-- /.col -->
</div><!-- /.row -->
</div><!-- /.modal-body -->
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>

0
templates/restapi.php Normal file → Executable file
View File