Improve CSRF handling and session management

This commit is contained in:
Florine W. Dekker 2022-08-12 17:06:21 +02:00
parent 7d0b08e6b7
commit c8322d09c4
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
4 changed files with 149 additions and 80 deletions

View File

@ -34,59 +34,77 @@ if (!file_exists($config["database"]["filename"])) {
// Start session
session_start();
if (!isset($_SESSION["token"])) {
$_SESSION["token"] = bin2hex(random_bytes(32));
}
// Read JSON from POST, if it's there
if (empty($_POST)) {
$_POST = json_decode(file_get_contents("php://input"), associative: true);
}
$response = array();
$response["satisfied"] = false;
$response["token"] = $_SESSION["token"];
if (isset($_POST["action"])) {
// Process POST
switch ($_POST["action"]) {
case "register":
if (!isset($_POST["token"]) || !isset($_SESSION["token"]) || $_POST["token"] !== $_SESSION["token"]) {
exit("\"no token, or invalid token\"");
$response["message"] = "no token, or invalid token";
break;
}
if (isset($_SESSION["uuid"])) {
exit("\"already logged in\"");
$response["message"] = "already logged in";
break;
}
if (!isset($_POST["email"], $_POST["password"], $_POST["password_confirm"])) {
exit("\"missing inputs\"");
$response["message"] = "missing inputs";
break;
}
if (!filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)) {
exit("\"invalid email\"");
$response["message"] = "invalid email";
break;
}
if ($_POST["password"] !== $_POST["password_confirm"]) {
exit("\"differing passwords\"");
$response["message"] = "differing passwords";
break;
}
$db = new Database($config["database"]["filename"], SQLITE3_OPEN_READONLY);
$email_is_in_use = $db->get_user_by_email($_POST["email"]) !== null;
$db->close();
if ($email_is_in_use) {
exit("\"email already in use\"");
$response["message"] = "email already in use";
break;
}
$db = new Database($config["database"]["filename"], SQLITE3_OPEN_READWRITE);
try {
$uuid = bin2hex(random_bytes(16));
} catch (\Exception) {
exit("false");
} catch (\Exception $exception) {
$response["message"] = "unknown database error";
break;
}
$db->add_user($uuid, $_POST["email"], $_POST["password"]);
$db->close();
exit("true");
$response["satisfied"] = true;
break;
case "login":
if (!isset($_POST["token"]) || !isset($_SESSION["token"]) || $_POST["token"] !== $_SESSION["token"]) {
exit("\"no token, or invalid token\"");
$response["message"] = "no token, or invalid token";
break;
}
if (!isset($_POST["email"], $_POST["password"])) {
exit("\"missing inputs\"");
$response["message"] = "missing inputs";
break;
}
$db = new Database($config["database"]["filename"], SQLITE3_OPEN_READWRITE);
@ -94,58 +112,77 @@ if (isset($_POST["action"])) {
$db->close();
if ($user === null || !password_verify($_POST["password"], $user["password"])) {
exit("\"wrong password\"");
$response["message"] = "wrong password";
break;
}
$_SESSION["uuid"] = $user["uuid"];
exit("true");
$response["satisfied"] = true;
break;
case "logout":
if (!isset($_POST["token"]) || !isset($_SESSION["token"]) || $_POST["token"] !== $_SESSION["token"]) {
exit("\"no token, or invalid token\"");
$response["message"] = "no token, or invalid token";
break;
}
session_destroy();
exit("true");
session_start();
$_SESSION["token"] = bin2hex(random_bytes(32));
$response["satisfied"] = true;
$response["token"] = $_SESSION["token"];
break;
case "user-update-email":
if (!isset($_POST["token"]) || !isset($_SESSION["token"]) || $_POST["token"] !== $_SESSION["token"]) {
exit("\"no token, or invalid token\"");
$response["message"] = "no token, or invalid token";
break;
}
if (!isset($_SESSION["uuid"])) {
exit("\"not logged in\"");
$response["message"] = "not logged in";
break;
}
if (!isset($_POST["email"])) {
exit("\"missing inputs\"");
$response["message"] = "missing inputs";
break;
}
if (!filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)) {
exit("\"invalid email\"");
$response["message"] = "invalid email";
break;
}
// TODO: Check if user exists
$db = new Database($config["database"]["filename"], SQLITE3_OPEN_READONLY);
if ($db->get_user_by_email($_POST["email"]) !== null) {
exit("\"email already in use\"");
$response["message"] = "email already in use";
break;
}
$db->close();
$db = new Database($config["database"]["filename"], SQLITE3_OPEN_READWRITE);
$db->set_user_email($_SESSION["uuid"], $_POST["email"]);
$db->close();
exit("\"true\"");
$response["satisfied"] = "true";
break;
case "user-update-password":
if (!isset($_POST["token"]) || !isset($_SESSION["token"]) || $_POST["token"] !== $_SESSION["token"]) {
exit("\"no token, or invalid token\"");
$response["message"] = "no token, or invalid token";
break;
}
if (!isset($_SESSION["uuid"])) {
exit("\"not logged in\"");
$response["message"] = "not logged in";
break;
}
if (!isset($_POST["password_old"], $_POST["password_new"], $_POST["password_confirm"])) {
exit("\"missing inputs\"");
$response["message"] = "missing inputs";
break;
}
// TODO: Check if user exists
@ -155,24 +192,30 @@ if (isset($_POST["action"])) {
$db->close();
if ($user === null || !password_verify($_POST["password_old"], $user["password"])) {
exit("\"wrong password\"");
$response["message"] = "wrong password";
break;
}
if ($_POST["password_new"] !== $_POST["password_confirm"]) {
exit("\"differing passwords\"");
$response["message"] = "differing passwords";
break;
}
$db = new Database($config["database"]["filename"], SQLITE3_OPEN_READWRITE);
$db->set_user_password($_SESSION["uuid"], $_POST["password"]);
$db->close();
exit("true");
$response["satisfied"] = true;
break;
case "user-delete":
if (!isset($_POST["token"]) || !isset($_SESSION["token"]) || $_POST["token"] !== $_SESSION["token"]) {
exit("\"no token, or invalid token\"");
$response["message"] = "no token, or invalid token";
break;
}
if (!isset($_SESSION["uuid"])) {
exit("\"not logged in\"");
$response["message"] = "not logged in";
break;
}
// TODO: Check if user exists
@ -182,43 +225,52 @@ if (isset($_POST["action"])) {
$db->close();
session_destroy();
exit("true");
$response["satisfied"] = true;
break;
case "add-tracking":
if (!isset($_POST["token"]) || !isset($_SESSION["token"]) || $_POST["token"] !== $_SESSION["token"]) {
exit("\"no token, or invalid token\"");
$response["message"] = "no token, or invalid token";
break;
}
if (!isset($_SESSION["uuid"])) {
exit("\"not logged in\"");
$response["message"] = "not logged in";
break;
}
if (!isset($_POST["person_name"])) {
exit("\"missing inputs\"");
$response["message"] = "missing inputs";
break;
}
$db = new Database($config["database"]["filename"], SQLITE3_OPEN_READONLY);
$tracking_already_exists = $db->has_tracking($_SESSION["uuid"], $_POST["person_name"]);
$db->close();
if ($tracking_already_exists) {
exit("\"tracking already exists\"");
$response["message"] = "tracking already exists";
break;
}
$db = new Database($config["database"]["filename"], SQLITE3_OPEN_READWRITE);
$db->add_tracking($_SESSION["uuid"], $_POST["person_name"]);
$db->close();
exit("true");
$response["satisfied"] = true;
break;
case "delete-tracking":
if (!isset($_POST["token"]) || !isset($_SESSION["token"]) || $_POST["token"] !== $_SESSION["token"]) {
exit("\"no token, or invalid token\"");
$response["message"] = "no token, or invalid token";
break;
}
if (!isset($_SESSION["uuid"])) {
exit("\"not logged in\"");
$response["message"] = "not logged in";
break;
}
if (!isset($_POST["person_name"])) {
exit("\"missing inputs\"");
$response["message"] = "missing inputs";
break;
}
// TODO: Check if tracking exists
@ -227,10 +279,12 @@ if (isset($_POST["action"])) {
$db->remove_tracking($_SESSION["uuid"], $_POST["person_name"]);
$db->close();
exit("true");
$response["satisfied"] = true;
break;
case "send-test-email":
if (!isset($_POST["token"]) || !isset($_SESSION["token"]) || $_POST["token"] !== $_SESSION["token"]) {
exit("\"no token, or invalid token\"");
$response["message"] = "no token, or invalid token";
break;
}
// TODO: Send this to logged-in user
@ -250,7 +304,8 @@ if (isset($_POST["action"])) {
$mail->setFrom($config["mail"]["username"], $config["mail"]["from_name"]);
$mail->addAddress($config["mail"]["to_address_test"]);
} catch (Exception) {
exit("false");
$response["message"] = "unknown mail error occurred";
break;
}
$mail->Subject = "Test mail";
@ -259,37 +314,55 @@ if (isset($_POST["action"])) {
try {
$mail->send();
} catch (Exception) {
exit("false");
$response["message"] = "unknown mail error occurred";
break;
}
exit("true");
$response["satisfied"] = true;
break;
default:
$response["message"] = "unknown POST action '" . $_POST["action"] . "'";
break;
}
} else if (isset($_GET["action"])) {
// Process GET
switch ($_GET["action"]) {
case "get-user-data":
if (!isset($_SESSION["uuid"])) {
exit("\"not logged in\"");
$response["message"] = "not logged in";
break;
}
$db = new Database($config["database"]["filename"], SQLITE3_OPEN_READONLY);
$user_data = $db->get_user_by_uuid($_SESSION["uuid"]);
$db->close();
exit(json_encode($user_data));
$response["message"] = $user_data;
$response["satisfied"] = true;
break;
case "list-trackings":
if (!isset($_SESSION["uuid"])) {
exit("\"not logged in\"");
$response["message"] = "not logged in";
break;
}
$db = new Database($config["database"]["filename"], SQLITE3_OPEN_READONLY);
$trackings = $db->list_trackings($_SESSION["uuid"]);
$db->close();
exit(json_encode($trackings));
$response["message"] = $trackings;
$response["satisfied"] = true;
break;
case "is-alive":
exit(json_encode((new MyMediawiki())->people_are_alive(array("Janelle Monáe", "John Malkovich", "Adolf Hitler"))));
$response["message"] = ((new MyMediawiki())->people_are_alive(array("Janelle Monáe", "John Malkovich", "Adolf Hitler")));
$response["satisfied"] = true;
break;
default:
$response["message"] = "unknown GET action '" . $_GET["action"] . "'";
break;
}
} else {
$response["message"] = "unknown method";
}
exit("\"unknown action\"");
exit(json_encode($response));

View File

@ -1,9 +1,3 @@
<?php
session_start();
$_SESSION["token"] = bin2hex(random_bytes(32));
?>
<!DOCTYPE html>
<html lang="en">
<head>
@ -45,7 +39,6 @@ $_SESSION["token"] = bin2hex(random_bytes(32));
<p>Already have an account? Welcome back!</p>
<form id="loginForm" onsubmit="return false;">
<p class="error"></p>
<input type="hidden" name="token" value="<?= $_SESSION["token"] ?>" />
<label>
Email
<input type="email" name="email" />
@ -67,7 +60,6 @@ $_SESSION["token"] = bin2hex(random_bytes(32));
</p>
<form id="registerForm" onsubmit="return false;">
<p class="error"></p>
<input type="hidden" name="token" value="<?= $_SESSION["token"] ?>" />
<label>
Email
<input type="email" name="email" />
@ -103,7 +95,6 @@ $_SESSION["token"] = bin2hex(random_bytes(32));
<h2>Manage account</h2>
<form id="logoutForm" onsubmit="return false;">
<p class="error"></p>
<input type="hidden" name="token" value="<?= $_SESSION["token"] ?>" />
<button>Log out</button>
</form>
</div>
@ -113,7 +104,6 @@ $_SESSION["token"] = bin2hex(random_bytes(32));
Verified? TODO
<form action="api.php" method="post">
<input type="hidden" name="action" value="user-update-email" />
<input type="hidden" name="token" value="<?= $_SESSION["token"] ?>" />
<label>
Email
<input type="email" name="email" />
@ -126,7 +116,6 @@ $_SESSION["token"] = bin2hex(random_bytes(32));
Last changed: TODO
<form action="api.php" method="post">
<input type="hidden" name="action" value="user-update-password" />
<input type="hidden" name="token" value="<?= $_SESSION["token"] ?>" />
<label>
Old password
<input type="password" name="password_old" />

View File

@ -1,11 +1,13 @@
// @ts-ignore
const {$, doAfterLoad, footer, header, nav} = window.fwdekker;
let csrfToken: string|null = null;
function refreshTrackings() {
fetch("api.php?action=list-trackings")
.then(it => it.json())
.then(trackings => {
.then(response => {
$("#trackings tbody").remove();
$("#trackings").append(document.createElement("tbody"));
@ -18,7 +20,7 @@ function refreshTrackings() {
headerRow.append(headerPersonName, headerIsDeceased, headerDelete);
$("#trackings tbody").append(headerRow);
trackings.forEach((tracking: any) => {
response["message"].forEach((tracking: any) => {
const trackingRow = document.createElement("tr");
const trackingPersonName = document.createElement("td");
trackingPersonName.innerText = tracking["person_name"];
@ -38,14 +40,14 @@ function refreshTrackings() {
},
body: JSON.stringify({
action: "delete-tracking",
token: $("#loginForm input[name=token]").value,
token: csrfToken,
person_name: tracking["person_name"],
})
})
.then(it => it.json())
.then(it => {
if (it !== true) {
$("#trackingsError").innerText = it;
if (!it["satisfied"]) {
$("#trackingsError").innerText = it["message"];
return;
}
refreshTrackings();
@ -72,14 +74,14 @@ function refreshTrackings() {
},
body: JSON.stringify({
action: "add-tracking",
token: $("#loginForm input[name=token]").value,
token: csrfToken,
person_name: createPersonInput.value,
})
})
.then(it => it.json())
.then(it => {
if (it !== true) {
$("#trackingsError").innerText = it;
if (!it["satisfied"]) {
$("#trackingsError").innerText = it["message"];
return;
}
refreshTrackings();
@ -127,15 +129,15 @@ doAfterLoad(async () => {
body: JSON.stringify({
action: "login",
// TODO: Deal with tokens in a smarter way. Allow refreshing them! Also remove duplication...
token: $("#loginForm input[name=token]").value,
token: csrfToken,
email: $("#loginForm input[name=email]").value,
password: $("#loginForm input[name=password]").value,
})
})
.then(it => it.json())
.then(it => {
if (it !== true) {
$("#loginForm .error").innerText = it;
if (!it["satisfied"]) {
$("#loginForm .error").innerText = it["message"];
return;
}
$("#loginForm").reset();
@ -157,7 +159,7 @@ doAfterLoad(async () => {
},
body: JSON.stringify({
action: "register",
token: $("#registerForm input[name=token]").value,
token: csrfToken,
email: $("#registerForm input[name=email]").value,
password: $("#registerForm input[name=password]").value,
password_confirm: $("#registerForm input[name=password_confirm]").value,
@ -165,8 +167,8 @@ doAfterLoad(async () => {
})
.then(it => it.json())
.then(it => {
if (it !== true) {
$("#registerForm .error").innerText = it;
if (!it["satisfied"]) {
$("#registerForm .error").innerText = it["message"];
return;
}
$("#registerForm").reset();
@ -185,13 +187,15 @@ doAfterLoad(async () => {
},
body: JSON.stringify({
action: "logout",
token: $("#logoutForm input[name=token]").value,
token: csrfToken,
})
})
.then(it => it.json())
.then(it => {
if (it !== true) {
$("#logoutForm .error").innerText = it;
csrfToken = it["token"];
if (!it["satisfied"]) {
$("#logoutForm .error").innerText = it["message"];
return;
}
$("#logoutForm").reset();
@ -202,9 +206,11 @@ doAfterLoad(async () => {
});
fetch("api.php?action=get-user-data")
.then(it => it.text())
.then(it => it.json())
.then(it => {
if (it === "\"not logged in\"") {
csrfToken = it["token"];
if (!it["satisfied"]) {
loginRow.classList.remove("hidden");
} else {
trackingRow.classList.remove("hidden");

View File

@ -3,7 +3,8 @@
"target": "es6",
"strict": true,
"rootDir": "./src/main/js/",
"outDir": "./dist/js/"
"outDir": "./dist/js/",
"allowSyntheticDefaultImports": true
},
"include": [
"src/main/js/**/*.ts"