Different authentication level for a webpage

Hello,

i made a webpage based on ESP32S3 with authentication.
Now i want to add an option to create new users, but i want the users have different authentication levels.
Is this possible?
If yes and if i want to change the password for an specific user, then should i check for the right user in HTML side code (JavaScript) or in Server side (Arduino IDE) code?

Thanks

Yes, it is possible to implement the different user authentication at different levels to access your webpage at ESP32S3. You have set the assign different permission level of each user when they try to access the pages on your site.

Thanks for you reply.
is there any example so i can have an idea how to implement that in my project?

I'm working on an example for managing RFID tags using a MySQL database (no use of PHP script!) and a management page where only a user with "admin" rights can add, delete and modify users records.

Other logged users can only view the data.

MySQL is not mandatory, it's used for storing users table and access logs.

Since the pages served from ESP32 are static resources (flash stored literal strings), to manage the level of access on the client side I use a cookie that is set on the server side.
Passwords are not stored in the DB as plain text, nor during handshaking between server and client. A SHA256 encryption it's used instead.

The example is almost complete, I'll put it online later.

Thanks for your reply.

That would be much appreciated

I've just uploaded the example I was talking about.

The library esp-fs-webserver.h is just a wrapper for the WebServer.h library already included in the ESP32 Arduino core, so it's not mandatory to use it.

I don't know what your level is with C++ and especially with HTML/Javascript; this example can be quite complex for a novice to fully understand, so if you need clarification don't worry and just ask.

In particular, regarding your original request, I will try to explain in detail how I acted:

  • ESP32 already provides authentication methods based on BASIC_AUTH or DIGEST_AUTH, but since I find the standard login prompt ugly, I created my own /login page;
  • the login page on submit will calculate the hash of password and will require the /rfid webpage passing username and password hash (the request is handled from ESP32 here);
  • the web server will check user/password hash/user role in DB records, then reply to client setting a cookie with data;
  • the cookie value will be parsed from /rfid page at this line to take action in relation to level passed (enable a button and the /setup link).

If you are fine with the standard login prompt, it's possible to simplify the handshaking skipping the custom /login webpage.

@cotestatnt
Thank you for your reply and example.
My level is not good with HTML/Javascript but i will take a look at your example.
I am working with Ethernet and not WiFi but that should not be a problem.
I managed to write a page where i can create new user and delete them but the problem now i am facing is that created cards for new user do not get deleted when i delete one user.
My webpage is based on one example of W3school

<div class="container">
  <form method="post" id="userForm" action="/user">
        
    <label for="newUsername">Neuer Benutzername:</label>
    <input type="text" id="newUsername" name="newUsername" required>
    <label for="newPassword">Neues Passwort:</label>
    <input type="password" name="newPassword" required>
    <label for="confirmPassword">Best&aumltige Passwort:</label>
    <input type="password" name="confirmPassword" required>
    <input type="submit" onclick="createUserCard()" value="Benutzer erstellen">

  </form>
  
  <div id="userList">
  </div>
</div>

</div>
</div>

<script>
document.addEventListener("DOMContentLoaded", function() {
  // Check if there are any stored user cards in local storage
  const storedUsers = JSON.parse(localStorage.getItem("userCards")) || [];

  // Render stored user cards
  storedUsers.forEach(user => {
    createUserCard(user);
  });

  // Add event listener for form submission
  document.getElementById("userForm").addEventListener("submit", function(event) {
    event.preventDefault();

    const newUsername = document.getElementById("newUsername").value;
    createUserCard(newUsername);

    // Save the updated user cards to local storage
    const updatedUsers = [...storedUsers, newUsername];
    localStorage.setItem("userCards", JSON.stringify(updatedUsers));

    // Clear the input field
    document.getElementById("newUsername").value = "";
  });
});

// Function to create a user card
function createUserCard(newUsername) {
  const userList = document.getElementById("userList");
  //var userList = document.getElementById("userList");
  
  // Get the value of the new username input field
  //var newUsername = document.getElementsByName("newUsername")[0].value;

  // Create a new div element for the user card
  const userCard = document.createElement("div");
  //var userCard = document.createElement("div");
  userCard.className = "user-card";

  // Create a paragraph element to display the username
  var usernamePara = document.createElement("p");
  usernamePara.textContent = "Username: " + newUsername;

  // Create a delete icon
  const deleteIcon = document.createElement("i");
  //var deleteIcon = document.createElement("i");
  deleteIcon.className = "fa fa-trash";
  deleteIcon.onclick = function() {
    // Remove the user card from the DOM
    userCard.remove();
    // Remove username from local storage
    const updatedUsers = storedUsers.filter(user => user !== newUsername);
    localStorage.setItem("userCards", JSON.stringify(updatedUsers));
    // Send an HTTP request to delete the user from the server
    deleteUser(newUsername);
  };

  // Append username and delete icon to the user card
  userCard.appendChild(usernamePara);
  userCard.appendChild(deleteIcon);

  // Append the user card to the user list container
  userList.appendChild(userCard);
}

// Function to send an HTTP request to delete the user from the server
function deleteUser(newUsername) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/userdelete?newUsername=" + encodeURIComponent(newUsername), true);
  xhr.onreadystatechange = function() {
    if (xhr.readyState === XMLHttpRequest.DONE) {
      if (xhr.status === 200) {
        // User deleted successfully
        const updatedUsers = storedUsers.filter(user => user !== newUsername);
        localStorage.setItem("userCards", JSON.stringify(updatedUsers));
      } else {
        // Handle error if deletion fails
        console.error("Failed to delete user:", xhr.status);
      }
    }
  };
  xhr.send();
}



  function logout() {
    // Perform any necessary logout actions here
    // For example, clearing session storage or cookies
    localStorage.removeItem('authToken'); // Remove authentication token
    setTimeout(function(){ window.open("/login", "_self");}, 0);
    window.sessionStorage.clear();
    var xhr = new XMLHttpRequest();
    xhr.open("GET", "/logout", true);
    xhr.send();
  }
</script>

Your snippet of code is uncomplete.
It's hard in this way give you a proper answer. Aniway I can see is you are using local storage.

Have you tried using the browser developer tools (F11 key) to see what errors you have on the console?

In my opinion, yours is a scope problem of the storedUsers variable within the lambda function associated with the delete icon click which is not accessible (because it is a local variable of the event handler for DOMContentLoaded)

Try to use as global putting the declaration outside the event handler.

You are doing the same thing here:

     if (xhr.status === 200) {
        // User deleted successfully
        const updatedUsers = storedUsers.filter(user => user !== newUsername);
        localStorage.setItem("userCards", JSON.stringify(updatedUsers));
      } else {

but at this point is useless as you have already removed newUsername before calling this function.

Indeed, it would be better to do it only here, after you have actually received a response from the ESP32.

Finally, you are also calling the function createUserCard() twice for each user inserted and off course is not correct.
The first time because you have defined the button as following

<input type="submit" onclick="createUserCard()" value="Benutzer erstellen">

and the second one because you have added the listener to the form submit (which is the right one, since with the onclick you don't pass the username value to the function).
Remove the onclick from button declaration.

Thanky @cotestatnt for your reply.

Yes you were right. The problem was declaring storedUsers localy.
After i declared it globaly, now the created new user cards do not keep appearing after deleting them.

Now this is the problem:
when i click on Benutzer erstellen the user cards are created on the webpage correctly ( i guess) but the handling function in the server side code does not get triggered.
The server can not read any new created user.

I have removed the onclick from the button declaration.
I think my event listener for the form submissin is not working propperly.
Here is the complete HTML file:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<style>

h2{margin-top: 60px;
   margin-left: 60px;}

body {
  font-family: Arial, Helvetica, sans-serif;
}
* {
  box-sizing: border-box;
}


.container {
  max-width: 450px;
  margin: 50px auto;
  border-radius: 5px;
  background-color: white; /* #f2f2f2 */
  padding: 20px;
  margin-top: 25px; 
}

label {
  display: block;
}

input {
  width: 100%;
  padding: 8px;
  margin-bottom: 10px;
  box-sizing: border-box;
}

input[type=text], select, textarea {
  width: 100%;
  padding: 8px;
  margin-top: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  resize: vertical; 
}

input[type=submit] {
  background-color: #04AA6D;
  color: white;
  padding: 12px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-top: 15px;
}

input[type=submit]:hover {
  background-color: #45a049; 
}

.user-card {
  border: 1px solid #ccc;
  border-radius: 5px;
  padding: 10px;
  margin-bottom: 10px;
  background-color: #f9f9f9;
  position: relative;
}

.fa-trash {
  color: black;
  cursor: pointer;
  position: absolute;
  top: 30%;
  right: 10px;
}

.user-card p {
  margin: 0;
  font-weight: bold;
}

.navbar {
  overflow: hidden;
  background-color: white;
}

.navbar a {
  float: left;
  font-size: 16px;
  color: black;
  text-align: center;
  padding: 14px 16px;
  text-decoration: none;
}

.sidenav {
  height: 70%;
  width: 200px;
  position: fixed;
  z-index: 1;
  top: 120px;
  left: 100px;
  background-color: white;
  overflow-x: hidden;
  padding-top: 20px;
}

.sidenav a {
  padding: 6px 6px 36px 32px;
  text-decoration: none;
  font-size: 16px;
  color: black;
  display: block;
}

.dropdown {
  float: left;
  padding-left: 120px;
  overflow: hidden;
}

.dropdown .dropbtn {
  font-size: 16px;  
  border: none;
  outline: none;
  color: black;
  padding: 14px 16px;
  background-color: inherit;
  font-family: inherit;
  margin: 0;
}

.navbar a:hover, .dropdown:hover .dropbtn {
  color: blue;
}

.dropdown-content {
  display: none;
  position: absolute;
  background-color: white;
  min-width: 160px;
  box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
  z-index: 1;
}

.dropdown-content a {
  float: none;
  color: black;
  padding: 12px 16px;
  text-decoration: none;
  display: block;
  text-align: center;
}

.dropdown-content a:hover {
  background-color: white;
}

.dropdown:hover .dropdown-content {
  display: block;
}

.sidenav a:hover {
  color: blue;
}

.main {
  margin-left: 200px; /* Same as the width of the sidenav */
}

@media screen and (max-height: 450px) {
  .sidenav {padding-top: 15px;}
  .sidenav a {font-size: 18px;}
}

/* Responsive layout - when the screen is less than 600px wide, make the two columns stack on top of each other instead of next to each other */
@media screen and (max-width: 600px) {
  .col-25, .col-75, input[type=submit] {
    width: 100%;
    margin-top: 0;
  }
}

</style>
</head>

<body style="background-color:white;">
<div class="w3-main" style="display: flex; justify-content: center; margin-top: 20px;">

    <span class="w3-button w3-hide-large w3-xxlarge w3-hover-text-grey" onclick="w3_open()"></span>
    <div class="w3-container">


<img src='https://wiki.amberg.deprag.de/allgemein/images/thumb/d/de/Logo-DEPRAG-machinesunlimited.svg/120px-Logo-DEPRAG-machinesunlimited.svg.png' style='margin-right: 20px; float: left;'  id='deprag-logo' width= '100' height= '30'>

<div class="navbar">
  <a href="index">Dashboard</a>
  <a href="aktualisiere">Software</a>
  <a href="network">Einstellungen</a>
  <a href="sysinfo">Systeminfo</a>
  <a href="sensordata">Service</a>
  <div class="dropdown">
    <button class="dropbtn"><i class="fa fa-fw fa-user"></i>Admin<i class="fa fa-caret-down"></i></button>
    <div class="dropdown-content">
      <a class="fa fa-user" href="meinprofil">Mein Profil</a>
      <a class="fa fa-times" href="#" onclick="logout()">Abmelden</a>
    </div>
  </div> 
</div>
<div class="sidenav">
  <a href="network">Netzwerk</a>
  <a href="user">Benutzerverwaltung</a>
  <a href="feldbus">Feldbus</a>
</div>

<h2>Einstellungen<span style='font-size:30px;'>&#8594;</span>Benutzerverwaltung</h2>


<div class="container">
  <form method="post" id="userForm" action="/user">
        
    <label for="newUsername">Neuer Benutzername:</label>
    <input type="text" id="newUsername" name="newUsername" required>
    <label for="newPassword">Neues Passwort:</label>
    <input type="password" name="newPassword" required>
    <label for="confirmPassword">Best&aumltige Passwort:</label>
    <input type="password" name="confirmPassword" required>
    <input type="submit" value="Benutzer erstellen">

  </form>
  
  <div id="userList">
  </div>
</div>

</div>
</div>

<script>
const storedUsers = JSON.parse(localStorage.getItem("userCards")) || [];

document.addEventListener("DOMContentLoaded", function() {

  // Render stored user cards
  storedUsers.forEach(user => {
    createUserCard(user);
  });

  // Add event listener for form submission
  document.getElementById("userForm").addEventListener("submit", function(event) {
    event.preventDefault();

    const newUsername = document.getElementById("newUsername").value;
    createUserCard(newUsername);

    // Save the updated user cards to local storage
    const updatedUsers = [...storedUsers, newUsername];
    localStorage.setItem("userCards", JSON.stringify(updatedUsers));

    // Clear the input field
    document.getElementById("newUsername").value = "";
  });
});

// Function to create a user card
function createUserCard(newUsername) {
  const userList = document.getElementById("userList");

  // Create a new div element for the user card
  const userCard = document.createElement("div");

  userCard.className = "user-card";

  // Create a paragraph element to display the username
  var usernamePara = document.createElement("p");
  usernamePara.textContent = "Username: " + newUsername;

  // Create a delete icon
  const deleteIcon = document.createElement("i");

  deleteIcon.className = "fa fa-trash";
  deleteIcon.onclick = function() {
    // Remove the user card from the DOM
    userCard.remove();

    // Send an HTTP request to delete the user from the server
    deleteUser(newUsername);
  };

  // Append username and delete icon to the user card
  userCard.appendChild(usernamePara);
  userCard.appendChild(deleteIcon);

  // Append the user card to the user list container
  userList.appendChild(userCard);
}

// Function to send an HTTP request to delete the user from the server
function deleteUser(newUsername) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/userdelete?newUsername=" + encodeURIComponent(newUsername), true);
  xhr.onreadystatechange = function() {
    if (xhr.readyState === XMLHttpRequest.DONE) {
      if (xhr.status === 200) {
        // User deleted successfully
        const updatedUsers = storedUsers.filter(user => user !== newUsername);
        localStorage.setItem("userCards", JSON.stringify(updatedUsers));
      } else {
        // Handle error if deletion fails
        console.error("Failed to delete user:", xhr.status);
      }
    }
  };
  xhr.send();
}



  function logout() {
    // Perform any necessary logout actions here
    // For example, clearing session storage or cookies
    localStorage.removeItem('authToken'); // Remove authentication token
    setTimeout(function(){ window.open("/login", "_self");}, 0);
    window.sessionStorage.clear();
    var xhr = new XMLHttpRequest();
    xhr.open("GET", "/logout", true);
    xhr.send();
  }
</script>


</body>
</html>

After i successfully manage this issue i will try with your example on the multiple authentication.

It's not the listener.
You have disabled the default behavior of form with this instruction, so you need to handle the submit manually for example as you have done for user deleting

event.preventDefault();

As before, I think it should be better create the user card after the ESP32 has replied successfully

  // Get reference to HTML element
  var form = document.getElementById('userForm');
  // Get data from fromm fields
  var formData = new FormData(form);
  
  // Send the AJAX request to ESP32 server
  var xhr = new XMLHttpRequest();
  xhr.open("POST", "/user");
  xhr.onreadystatechange = function () {
    if (xhr.readyState === XMLHttpRequest.DONE) {
      if (xhr.status === 200) {
        // Operations to perform if the request is successful
        console.log(xhr.responseText);
      } else {
        // Operations in case of error
        console.error('An error occurred:', xhr.status);
      }
    }
  };
  
  xhr.send(formData);
}

Thank you for your suggestion @cotestatnt

I tried that but it does send empty strings as username and password, so my handle function report error.
Enabling the default behavor did the same thing.

Check the payload with your browser.
I think you need to change the content-type.

With the actual code is a "multipart/form-data", perhaps you need to change the content-type to one that the ESP32 webserver can parse.

I should check in more detail, but with the ESP32 it's probably better to use the "application/x-www-form-urlencoded"

This means that the javascript needs to be modified slightly:

  var form = document.getElementById("userForm");
  var formData = new FormData(form);
  var encodedData = new URLSearchParams(formData).toString(); // Convert FormData to URL-encoded string
  
  var xhr = new XMLHttpRequest();
  xhr.open("POST", "/user");
  xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); // Set the content type
  
  xhr.onreadystatechange = function () {
    if (xhr.readyState === XMLHttpRequest.DONE) {
      if (xhr.status === 200) {
        // Operations to perform if the request is successful
        console.log(xhr.responseText);
      } else {
        // Operations in case of error
        console.error('An error occurred:', xhr.status);
      }
    }
  };
  
  xhr.send(encodedData);

Thank you @cotestatnt,
it has worked.

Now i have two questions:
First: How do you clear the fields (username, password...) after creating new user?

Second: How do you save the new created user in your server?

For the second question, i am using preferences but it looks like it not working properly.
I have a defined max number for the user int numUsers = 0; and everytime a new user is created i increment the number, but somehow it does not keeps its static value although i have declare it global.

ethernetServer.on("/user", HTTP_POST,handleNewUsername ); // the handler is in setup

// the handler function is in a header file defined
void handleNewUsername(){
  if(!ethernetServer.authenticate(ust.m_Benutzername, ust.m_Passwort))
    return ethernetServer.requestAuthentication();

  Serial.println("handleNewUserName get triggered!");

  String response;

  if(ethernetServer.args() == 0){
    response = "<h1>Empty username field</h1>";
    response += "<META http-equiv= 'refresh' content='5;URL=/'>Back to homepage...\n";
    ethernetServer.send(200, "text/html", response);
    return;
  }

  String newUsername = ethernetServer.arg("newUsername");
  String newPassword = ethernetServer.arg("newPassword");
  String confirmPassword = ethernetServer.arg("confirmPassword");

  if(newUsername == ""){
    response = "<h1>Invalid Username format! Try something like: username</h1>";
    response += "<META http-equiv='refresh' content='5;URL=/'>Back to homepage...\n";
    // Send the response to the client
    ethernetServer.send(200, "text/html", response);
    return;
  }

  if(newPassword == confirmPassword){
    // Register the new user
    registerUser(newUsername.c_str(), newPassword.c_str());
    // New user is creadted
    // response = "<h1>New user correctly created</h1>";
    // response += "<META http-equiv='refresh' content='5;URL=/'>Back to homepage...\n";
    ethernetServer.send(200);
  } else {
    // Passwords do not match, handle the error (e.g., return an error message)
    response = "<h1>Password and confirmPassword do not match</h1>";
    response += "<META http-equiv='refresh' content='5;URL=/'>Back to homepage...\n";
    ethernetServer.send(200, "text/html", response);
    return;
  }
}

// The function which uses preferences to save on flash, is in another header defined
void registerUser(const char* newUsername, const char* newPassword){
  // Validate input data (e.g. check username and password)

  if(numUsers > 11){
    return;
  }

  // Store the new user's information in the data structure or database
  strcpy(users[numUsers].m_Benutzername, newUsername);
  // memcpy(users[numUsers].m_Benutzername, newUsername, sizeof(newUsername) + 1);
  Serial.print("New username to be stored: ");
  Serial.println(users[numUsers].m_Benutzername);
  strcpy(users[numUsers].m_Passwort, newPassword);
  // memcpy(users[numUsers].m_Passwort, newPassword, sizeof(newPassword) + 1);
  Serial.print("password for the new user: ");
  Serial.println(users[numUsers].m_Passwort);


  // Store the new username in non-volatile memory
  pref.begin("UserSettings", false);
  pref.putString(("Username_" + String(numUsers)).c_str(), newUsername); // Store the username with a unique key based on the user index
  pref.end();

  // Increment the number of users
  numUsers++;
  Serial.print("numUsers after increment: ");
  Serial.println(numUsers);
}

In my example, I'm using a MySQL DB for all data, users list included.
Regarding the usermame and password fields showed in the webpage I don't take care of it because selecting a new one, it will be filled with the relative text (taken from DB) except the password.

However, if you want to clear the contents of an inputbox you simply need to get the reference to the HTML element and then set the value to an empty string.

document.getElementById("newUsername").value= '';
document.getElementById("newPassword").value= '';
document.getElementById("confirmPassword").value= '';

Thank you for your reply,

document.getElementById("newUsername").value= '';
document.getElementById("newPassword").value= '';
document.getElementById("confirmPassword").value= '';

i have already done that, but only username field get cleared.
To clear password & confirmpassword fields i need to refresh the page.

Hello @cotestatnt ,

i noticed that :
1- You don't have a logout functionality in your webpage.
2- Your handles are not in the setup function.
3- I didn't understand the porpuse of myWebserver.setAuthentication("admin","admin"); , since i am using EthnernetServer i don't have such a function.

Thanks

Hi!

  1. Yes you are right, I haven't implemented it. When I have some free time I will add also the logout.
  2. And why should they be in setup? Handlers are callback functions, you can define them wherever you want.
  3. The method setAuthentication() allows you to set a login using "simple digest" for the /setup and /edit pages (which can possibly be deactivated completely).
    If the library for your Ethernet client does not support this feature, you can simply remove those lines

Hello @cotestatnt

One question:
Where did you get the library: #include "mbedtls/md.h"?
Can you please provide the link to it so i can download it?
And would it work with the Ethernet? I think yes.

Thanks a lot

Hi @hamokhalil
It's included in the ESP32 Arduino Core