mirror of
https://github.com/unraid/webgui.git
synced 2026-01-07 10:10:05 -06:00
Merge pull request #2140 from unraid/chore/remove-t2fa-from-login
chore: remove t2fa from .login.php template
This commit is contained in:
@@ -2,10 +2,12 @@
|
||||
// Included in login.php
|
||||
|
||||
// Only start a session to check if they have a cookie that looks like our session
|
||||
$server_name = strtok($_SERVER['HTTP_HOST'],":");
|
||||
$server_name = strtok($_SERVER['HTTP_HOST'], ":");
|
||||
if (!empty($_COOKIE['unraid_'.md5($server_name)])) {
|
||||
// Start the session so we can check if $_SESSION has data
|
||||
if (session_status()==PHP_SESSION_NONE) session_start();
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// Check if the user is already logged in
|
||||
if ($_SESSION && !empty($_SESSION['unraid_user'])) {
|
||||
@@ -15,10 +17,11 @@ if (!empty($_COOKIE['unraid_'.md5($server_name)])) {
|
||||
}
|
||||
}
|
||||
|
||||
function readFromFile($file): string {
|
||||
function readFromFile($file): string
|
||||
{
|
||||
$text = "";
|
||||
if (file_exists($file) && filesize($file) > 0) {
|
||||
$fp = fopen($file,"r");
|
||||
$fp = fopen($file, "r");
|
||||
if (flock($fp, LOCK_EX)) {
|
||||
$text = fread($fp, filesize($file));
|
||||
flock($fp, LOCK_UN);
|
||||
@@ -28,8 +31,9 @@ function readFromFile($file): string {
|
||||
return $text;
|
||||
}
|
||||
|
||||
function appendToFile($file, $text): void {
|
||||
$fp = fopen($file,"a");
|
||||
function appendToFile($file, $text): void
|
||||
{
|
||||
$fp = fopen($file, "a");
|
||||
if (flock($fp, LOCK_EX)) {
|
||||
fwrite($fp, $text);
|
||||
fflush($fp);
|
||||
@@ -38,8 +42,9 @@ function appendToFile($file, $text): void {
|
||||
}
|
||||
}
|
||||
|
||||
function writeToFile($file, $text): void {
|
||||
$fp = fopen($file,"w");
|
||||
function writeToFile($file, $text): void
|
||||
{
|
||||
$fp = fopen($file, "w");
|
||||
if (flock($fp, LOCK_EX)) {
|
||||
fwrite($fp, $text);
|
||||
fflush($fp);
|
||||
@@ -56,7 +61,8 @@ function isValidTimeStamp($timestamp)
|
||||
&& ($timestamp >= ~PHP_INT_MAX);
|
||||
}
|
||||
|
||||
function cleanupFails(string $failFile, int $time): int {
|
||||
function cleanupFails(string $failFile, int $time): int
|
||||
{
|
||||
global $cooldown;
|
||||
|
||||
// Read existing fails
|
||||
@@ -67,8 +73,8 @@ function cleanupFails(string $failFile, int $time): int {
|
||||
// Remove entries older than $cooldown minutes, and entries that are not timestamps
|
||||
$updateFails = false;
|
||||
foreach ((array) $fails as $key => $value) {
|
||||
if ( !isValidTimeStamp($value) || ($time - $value > $cooldown) || ($value > $time) ) {
|
||||
unset ($fails[$key]);
|
||||
if (!isValidTimeStamp($value) || ($time - $value > $cooldown) || ($value > $time)) {
|
||||
unset($fails[$key]);
|
||||
$updateFails = true;
|
||||
}
|
||||
}
|
||||
@@ -81,94 +87,19 @@ function cleanupFails(string $failFile, int $time): int {
|
||||
return count($fails);
|
||||
}
|
||||
|
||||
function verifyUsernamePassword(string $username, string $password): bool {
|
||||
if ($username != "root") return false;
|
||||
function verifyUsernamePassword(string $username, string $password): bool
|
||||
{
|
||||
if ($username != "root") {
|
||||
return false;
|
||||
}
|
||||
|
||||
$output = exec("/usr/bin/getent shadow $username");
|
||||
if ($output === false) return false;
|
||||
if ($output === false) {
|
||||
return false;
|
||||
}
|
||||
$credentials = explode(":", $output);
|
||||
return password_verify($password, $credentials[1]);
|
||||
}
|
||||
|
||||
function verifyTwoFactorToken(string $username, string $token): bool {
|
||||
try {
|
||||
// Create curl client
|
||||
$curlClient = curl_init();
|
||||
curl_setopt($curlClient, CURLOPT_HEADER, true);
|
||||
curl_setopt($curlClient, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curlClient, CURLOPT_UNIX_SOCKET_PATH, '/var/run/unraid-api.sock');
|
||||
curl_setopt($curlClient, CURLOPT_URL, 'http://unixsocket/verify');
|
||||
curl_setopt($curlClient, CURLOPT_BUFFERSIZE, 256);
|
||||
curl_setopt($curlClient, CURLOPT_TIMEOUT, 5);
|
||||
curl_setopt($curlClient, CURLOPT_HTTPHEADER, array('Content-Type:application/json', 'Origin: /var/run/unraid-notifications.sock'));
|
||||
curl_setopt($curlClient, CURLOPT_POSTFIELDS, json_encode([
|
||||
'username' => $username,
|
||||
'token' => $token
|
||||
]));
|
||||
|
||||
// Send the request
|
||||
curl_exec($curlClient);
|
||||
|
||||
// Get the http status code
|
||||
$httpCode = curl_getinfo($curlClient, CURLINFO_HTTP_CODE);
|
||||
|
||||
// Close the connection
|
||||
curl_close($curlClient);
|
||||
|
||||
// Error
|
||||
// This should accept 200 or 204 status codes
|
||||
if ($httpCode !== 200 && $httpCode !== 204) {
|
||||
// Log error to syslog
|
||||
my_logger("2FA code for {$username} is invalid, blocking access!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log success to syslog
|
||||
my_logger("2FA code for {$username} is valid, allowing login!");
|
||||
|
||||
// Success
|
||||
return true;
|
||||
} catch (Exception $exception) {
|
||||
// Error
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a haystack ends in a needle
|
||||
function endsWith($haystack, $needle): bool {
|
||||
return substr_compare($haystack, $needle, -strlen($needle)) === 0;
|
||||
}
|
||||
|
||||
// Check if we're accessing this via a wildcard cert
|
||||
function isWildcardCert(): bool {
|
||||
global $server_name;
|
||||
return endsWith($server_name, '.myunraid.net');
|
||||
}
|
||||
|
||||
// Check if we're accessing this locally via the expected myunraid.net url
|
||||
function isLocalAccess(): bool {
|
||||
global $nginx, $server_name;
|
||||
return isWildcardCert() && $nginx['NGINX_LANFQDN'] === $server_name;
|
||||
}
|
||||
|
||||
// Check if we're accessing this remotely via the expected myunraid.net url
|
||||
function isRemoteAccess(): bool {
|
||||
global $nginx, $server_name;
|
||||
return isWildcardCert() && $nginx['NGINX_WANFQDN'] === $server_name;
|
||||
}
|
||||
|
||||
// Check if 2fa is enabled for local (requires USE_SSL to be "auto" so no alternate urls can access the server)
|
||||
function isLocalTwoFactorEnabled(): bool {
|
||||
global $nginx, $my_servers;
|
||||
return $nginx['NGINX_USESSL'] === "auto" && ($my_servers['local']['2Fa']??'') === 'yes';
|
||||
}
|
||||
|
||||
// Check if 2fa is enabled for remote
|
||||
function isRemoteTwoFactorEnabled(): bool {
|
||||
global $my_servers;
|
||||
return ($my_servers['remote']['2Fa']??'') === 'yes';
|
||||
}
|
||||
|
||||
// Load configs into memory
|
||||
$my_servers = @parse_ini_file('/boot/config/plugins/dynamix.my.servers/myservers.cfg', true);
|
||||
$nginx = @parse_ini_file('/var/local/emhttp/nginx.ini');
|
||||
@@ -180,38 +111,34 @@ $remote_addr = $_SERVER['REMOTE_ADDR'] ?? "unknown";
|
||||
$failFile = "/var/log/pwfail/{$remote_addr}";
|
||||
|
||||
// Get the credentials
|
||||
$username = $_POST['username']??'';
|
||||
$password = $_POST['password']??'';
|
||||
$token = $_REQUEST['token']??'';
|
||||
|
||||
// Check if we need 2fa
|
||||
$twoFactorRequired = (isLocalAccess() && isLocalTwoFactorEnabled()) || (isRemoteAccess() && isRemoteTwoFactorEnabled());
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
// If we have a username + password combo attempt to login
|
||||
if (!empty($username) && !empty($password)) {
|
||||
try {
|
||||
// Bail if we're missing the 2FA token and we expect one
|
||||
if (isWildcardCert() && $twoFactorRequired && empty($token)) throw new Exception(_('No 2FA token detected'));
|
||||
|
||||
// Read existing fails, cleanup expired ones
|
||||
$time = time();
|
||||
$failCount = cleanupFails($failFile, $time);
|
||||
|
||||
// Check if we're limited
|
||||
if ($failCount >= $maxFails) {
|
||||
if ($failCount == $maxFails) my_logger("Ignoring login attempts for {$username} from {$remote_addr}");
|
||||
if ($failCount == $maxFails) {
|
||||
my_logger("Ignoring login attempts for {$username} from {$remote_addr}");
|
||||
}
|
||||
throw new Exception(_('Too many invalid login attempts'));
|
||||
}
|
||||
|
||||
// Bail if username + password combo doesn't work
|
||||
if (!verifyUsernamePassword($username, $password)) throw new Exception(_('Invalid username or password'));
|
||||
|
||||
// Bail if we need a token but it's invalid
|
||||
if (isWildcardCert() && $twoFactorRequired && !verifyTwoFactorToken($username, $token)) throw new Exception(_('Invalid 2FA token'));
|
||||
if (!verifyUsernamePassword($username, $password)) {
|
||||
throw new Exception(_('Invalid username or password'));
|
||||
}
|
||||
|
||||
// Successful login, start session
|
||||
@unlink($failFile);
|
||||
if (session_status()==PHP_SESSION_NONE) session_start();
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
$_SESSION['unraid_login'] = time();
|
||||
$_SESSION['unraid_user'] = $username;
|
||||
session_regenerate_id(true);
|
||||
@@ -274,8 +201,8 @@ $theme_dark = in_array($display['theme'], ['black', 'gray']);
|
||||
/
|
||||
/************************/
|
||||
body {
|
||||
background: <?=$theme_dark?'#1C1B1B':'#F2F2F2'?>;
|
||||
color: <?=$theme_dark?'#fff':'#1c1b1b'?>;
|
||||
background: <?=$theme_dark ? '#1C1B1B' : '#F2F2F2'?>;
|
||||
color: <?=$theme_dark ? '#fff' : '#1c1b1b'?>;
|
||||
font-family: clear-sans, sans-serif;
|
||||
font-size: .875rem;
|
||||
padding: 0;
|
||||
@@ -359,7 +286,7 @@ $theme_dark = in_array($display['theme'], ['black', 'gray']);
|
||||
width: 500px;
|
||||
margin: 6rem auto;
|
||||
border-radius: 10px;
|
||||
background: <?=$theme_dark?'#2B2A29':'#fff'?>;
|
||||
background: <?=$theme_dark ? '#2B2A29' : '#fff'?>;
|
||||
}
|
||||
#login::after {
|
||||
content: "";
|
||||
@@ -392,7 +319,7 @@ $theme_dark = in_array($display['theme'], ['black', 'gray']);
|
||||
}
|
||||
#login .error {
|
||||
color: red;
|
||||
margin-top: -20px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#login .content {
|
||||
padding: 2rem;
|
||||
@@ -451,7 +378,7 @@ $theme_dark = in_array($display['theme'], ['black', 'gray']);
|
||||
/************************/
|
||||
@media (max-width: 500px) {
|
||||
body {
|
||||
background: <?=$theme_dark?'#2B2A29':'#fff'?>;
|
||||
background: <?=$theme_dark ? '#2B2A29' : '#fff'?>;
|
||||
}
|
||||
[type=email], [type=number], [type=password], [type=search], [type=tel], [type=text], [type=url], textarea {
|
||||
font-size: 16px; /* This prevents the mobile browser from zooming in on the input-field. */
|
||||
@@ -483,7 +410,7 @@ $theme_dark = in_array($display['theme'], ['black', 'gray']);
|
||||
<?=htmlspecialchars($var['NAME'])?>
|
||||
</h1>
|
||||
<h2>
|
||||
<?=htmlspecialchars($var['COMMENT'])?>
|
||||
<?=htmlspecialchars($var['COMMENT'])?>
|
||||
</h2>
|
||||
|
||||
<div class="case">
|
||||
@@ -497,82 +424,38 @@ $theme_dark = in_array($display['theme'], ['black', 'gray']);
|
||||
</div>
|
||||
|
||||
<div class="form">
|
||||
<form class="js-removeTimeout" action="/login" method="POST">
|
||||
<? if (($twoFactorRequired && !empty($token)) || !$twoFactorRequired) { ?>
|
||||
<p>
|
||||
<input name="username" type="text" placeholder="<?=_('Username')?>" autocapitalize="none" autocomplete="off" spellcheck="false" autofocus required>
|
||||
<input name="password" type="password" placeholder="<?=_('Password')?>" required>
|
||||
<? if ($twoFactorRequired && !empty($token)) { ?>
|
||||
<input name="token" type="hidden" value="<?= $token ?>">
|
||||
<? } ?>
|
||||
</p>
|
||||
<? if ($error) echo "<p class='error'>$error</p>"; ?>
|
||||
<p>
|
||||
<button type="submit" class="button button--small"><?=_('Login')?></button>
|
||||
</p>
|
||||
<? } else { ?>
|
||||
<? if ($error) { ?>
|
||||
<div>
|
||||
<p class="error" style="padding-top:10px;"><?= $error ?></p>
|
||||
</div>
|
||||
<? } else { ?>
|
||||
<div>
|
||||
<p class="error" style="padding-top:10px;" title="<?= _('Please access this server via the My Servers Dashboard') ?>"><?= _('No 2FA token detected') ?></p>
|
||||
</div>
|
||||
<? } ?>
|
||||
<div>
|
||||
<a href="https://forums.unraid.net/my-servers/" class="button button--small" title="<?=_('Go to My Servers Dashboard')?>"><?=_('Go to My Servers Dashboard')?></a>
|
||||
</div>
|
||||
<? } ?>
|
||||
<script type="text/javascript">
|
||||
document.cookie = "cookietest=1";
|
||||
cookieEnabled = document.cookie.indexOf("cookietest=")!=-1;
|
||||
document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
|
||||
if (!cookieEnabled) {
|
||||
const errorElement = document.createElement('p');
|
||||
errorElement.classList.add('error');
|
||||
errorElement.textContent = "<?=_('Please enable cookies to use the Unraid webGUI')?>";
|
||||
|
||||
document.body.textContent = '';
|
||||
document.body.appendChild(errorElement);
|
||||
}
|
||||
</script>
|
||||
<form action="/login" method="POST">
|
||||
<p>
|
||||
<input name="username" type="text" placeholder="<?=_('Username')?>" autocapitalize="none" autocomplete="off" spellcheck="false" autofocus required>
|
||||
<input name="password" type="password" placeholder="<?=_('Password')?>" required>
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit" class="button button--small"><?=_('Login')?></button>
|
||||
</p>
|
||||
<?php if ($error) { ?>
|
||||
<p class="error"><?= $error ?></p>
|
||||
<?php } ?>
|
||||
</form>
|
||||
|
||||
<? if (($twoFactorRequired && !empty($token)) || !$twoFactorRequired) { ?>
|
||||
<div class="js-addTimeout hidden">
|
||||
<p class="error" style="padding-top:10px;"><?=_('Transparent 2FA Token timed out')?></p>
|
||||
<a href="https://forums.unraid.net/my-servers/" class="button button--small" title="<?=_('Go to My Servers Dashboard')?>"><?=_('Go to My Servers Dashboard')?></a>
|
||||
</div>
|
||||
<? } ?>
|
||||
</div>
|
||||
|
||||
<? if (($twoFactorRequired && !empty($token)) || !$twoFactorRequired) { ?>
|
||||
<p class="js-removeTimeout"><a href="https://docs.unraid.net/go/lost-root-password/" target="_blank"><?=_('Password recovery')?></a></p>
|
||||
<? } ?>
|
||||
<a href="https://docs.unraid.net/go/lost-root-password/" target="_blank"><?=_('Password recovery')?></a>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<? if ($twoFactorRequired && !empty($token)) { ?>
|
||||
<script type="text/javascript">
|
||||
const $elsToRemove = document.querySelectorAll('.js-removeTimeout');
|
||||
const $elsToShow = document.querySelectorAll('.js-addTimeout');
|
||||
/**
|
||||
* A user can manually refresh the page or submit with the wrong username/password
|
||||
* the t2fa token will be re-used on these page refreshes. We need to keep track of the timeout across potential page
|
||||
* loads rather than setting the timer with a fresh timeout each page load
|
||||
*/
|
||||
const tokenName = '<?=$token?>'.slice(-20);
|
||||
const ts = Date.now();
|
||||
const timeoutStarted = sessionStorage.getItem(tokenName) ? Number(sessionStorage.getItem(tokenName)) : ts;
|
||||
const timeoutDiff = ts - timeoutStarted; // current timestamp minus timestamp when token first set
|
||||
const timeoutMS = 297000 - timeoutDiff; // 5 minutes minus 3seconds or (5*60)*1000ms - 3000ms = 297000
|
||||
sessionStorage.setItem(tokenName, timeoutStarted);
|
||||
const tokenTimeout = setTimeout(() => {
|
||||
$elsToRemove.forEach(z => z.remove()); // remove elements
|
||||
$elsToShow.forEach(z => z.classList.remove('hidden')); // add elements
|
||||
}, timeoutMS); // if timeoutMS is negative value the timeout will trigger immediately
|
||||
</script>
|
||||
<? } ?>
|
||||
|
||||
<script type="text/javascript">
|
||||
document.cookie = "cookietest=1";
|
||||
cookieEnabled = document.cookie.indexOf("cookietest=")!=-1;
|
||||
document.cookie = "cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
|
||||
if (!cookieEnabled) {
|
||||
const formParentElement = document.querySelector('.form');
|
||||
const errorElement = document.createElement('p');
|
||||
errorElement.classList.add('error');
|
||||
errorElement.textContent = "<?=_('Please enable cookies to use the Unraid webGUI')?>";
|
||||
|
||||
document.body.textContent = '';
|
||||
document.body.appendChild(errorElement);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user