Web Server with esp8266 and esp32: manage security and authentication – 4

Spread the love

WebServer Esp8266 ESP32 security authentication realm and token
WebServer Esp8266 ESP32 security authentication realm and token

One of the simpliest authentication type is Basic Auth, the implementation is quite simple too.

For esp32 you must only change this lines

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>
[...]
ESP8266WebServer server(80);

// Comment this line for esp8266 to prevent cache 
// httpServer.serveStatic("/", SPIFFS, "/", "max-age=31536000");

to

#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>
[...]
WebServer server(80);

But be careful now on the ESP32 WiFiClient there is a big bug, you can check here, so if you try to start this server you will have some problem with many simultaneous requests. Therefore I have written a port of this sketch using the ESPAsyncWebServer library which uses AsyncTCP where this problem does not arise. But now I explain the actual solution for esp8266 and at the end of the article the equivalent solution for ESP32.

Fixed on esp32 core version 1.0.5.

Basic authentication

Take the previous implementation of web server and add the builtin implementation of authentication

/*
 *  WeMos D1 mini (esp8266)
 *  Simple web server that read from SPIFFS and
 *  stream on browser various type of file
 *  and manage gzip format also now authentication in basic
 *  WITH BASIC AUTHENTICATION
 *  by Mischianti Renzo <https://mischianti.org>
 *
 *  https://mischianti.org/category/tutorial/how-to-create-a-web-server-with-esp8266-and-esp32/
 *
 */
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>

const char* ssid = "<YOUR-SSID>";
const char* password = "<YOUR-PASSWD>";

// allows you to set the realm of authentication Default:"Login Required"
const char* www_realm = "Custom Auth Realm";
// the Content of the HTML response in case of Unautherized Access Default:empty
String authFailResponse = "Authentication Failed";

const char* www_username = "admin";
const char* www_password = "esp8266";

ESP8266WebServer httpServer(80);


bool loadFromSPIFFS(String path, String dataType) {
  Serial.print("Requested page -> ");
  Serial.println(path);
  if (SPIFFS.exists(path)){
	  File dataFile = SPIFFS.open(path, "r");
	  if (!dataFile) {
		  handleNotFound();
		  return false;
	  }

	  if (httpServer.streamFile(dataFile, dataType) != dataFile.size()) {
	    Serial.println("Sent less data than expected!");
	  }else{
		  Serial.println("Page served!");
	  }

	  dataFile.close();
  }else{
	  handleNotFound();
	  return false;
  }
  return true;
}

void serverRouting();

void setup(void) {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  Serial.print(F("Inizializing FS..."));
  if (SPIFFS.begin()){
	Serial.println(F("done."));
  }else{
	Serial.println(F("fail."));
  }

  Serial.println("Set routing for http server!");
  serverRouting();
  httpServer.begin();
  Serial.println("HTTP server started");
}

void loop(void) {
	httpServer.handleClient();
}

String getContentType(String filename){
  if(filename.endsWith(F(".htm"))) 			return F("text/html");
  else if(filename.endsWith(F(".html"))) 	return F("text/html");
  else if(filename.endsWith(F(".css"))) 	return F("text/css");
  else if(filename.endsWith(F(".js"))) 		return F("application/javascript");
  else if(filename.endsWith(F(".json"))) 	return F("application/json");
  else if(filename.endsWith(F(".png"))) 	return F("image/png");
  else if(filename.endsWith(F(".gif"))) 	return F("image/gif");
  else if(filename.endsWith(F(".jpg"))) 	return F("image/jpeg");
  else if(filename.endsWith(F(".jpeg"))) 	return F("image/jpeg");
  else if(filename.endsWith(F(".ico"))) 	return F("image/x-icon");
  else if(filename.endsWith(F(".xml"))) 	return F("text/xml");
  else if(filename.endsWith(F(".pdf"))) 	return F("application/x-pdf");
  else if(filename.endsWith(F(".zip"))) 	return F("application/x-zip");
  else if(filename.endsWith(F(".gz"))) 		return F("application/x-gzip");
  return F("text/plain");
}

bool handleFileRead(String path){
  Serial.print(F("handleFileRead: "));
  Serial.println(path);

  if (!httpServer.authenticate(www_username, www_password)) {
	  httpServer.requestAuthentication();
  }

  if(path.endsWith("/")) path += F("index.html");           // If a folder is requested, send the index file
  String contentType = getContentType(path);             	// Get the MIME type
  String pathWithGz = path + F(".gz");
  if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){  	// If the file exists, either as a compressed archive, or normal
    if(SPIFFS.exists(pathWithGz))                          	// If there's a compressed version available
      path += F(".gz");                                     // Use the compressed version
    fs::File file = SPIFFS.open(path, "r");                 // Open the file
    size_t sent = httpServer.streamFile(file, contentType); // Send it to the client
    file.close();                                          	// Close the file again
    Serial.println(String(F("\tSent file: ")) + path + String(F(" of size ")) + sent);
    return true;
  }
  Serial.println(String(F("\tFile Not Found: ")) + path);
  return false;                                          	// If the file doesn't exist, return false
}

void handleNotFound() {
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += httpServer.uri();
  message += "\nMethod: ";
  message += (httpServer.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += httpServer.args();
  message += "\n";

  for (uint8_t i = 0; i < httpServer.args(); i++) {
    message += " " + httpServer.argName(i) + ": " + httpServer.arg(i) + "\n";
  }

  httpServer.send(404, "text/plain", message);
}

void serverRouting() {
	  httpServer.onNotFound([]() {                              // If the client requests any URI
		  Serial.println(F("On not found"));
	    if (!handleFileRead(httpServer.uri())){                  // send it if it exists
	    	handleNotFound(); // otherwise, respond with a 404 (Not Found) error
	    }
	  });

	  Serial.println(F("Set cache!"));
	  // Serve a file with no cache so every tile It's downloaded
	  httpServer.serveStatic("/configuration.json", SPIFFS, "/configuration.json","no-cache, no-store, must-revalidate");
	  // Server all other page with long cache so browser chaching they
      // Comment this line for esp8266
	  httpServer.serveStatic("/", SPIFFS, "/","max-age=31536000");
}

Now you have a login box

Basic Auth esp8266 WebServer
Basic Auth esp8266 WebServer

AsyncWebServer

Now the prev sketch with the very popular library AsyncWebServer, exist a version for esp8266 and esp32, you must only change the base library.

For ESP8266 it requires ESPAsyncTCP To use this library you might need to have the latest git versions of ESP8266 Arduino Core

For ESP32 it requires AsyncTCP to work To use this library you might need to have the latest git versions of ESP32 Arduino Core

This is fully asynchronous server and as such does not run on the loop thread.

Here the previous example for esp32 with this library.

/*
 *  ESP32
 *  Simple web server that read from SPIFFS and
 *  stream on browser various type of file
 *  and manage gzip format also
 *  WITH BASIC AUTHENTICATION
 *  by Mischianti Renzo <https://mischianti.org>
 *
 *  https://mischianti.org/category/tutorial/how-to-create-a-web-server-with-esp8266-and-esp32/
 *
 */

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>

const char* ssid = "<YOUR-SSID>";
const char* password = "<YOUR-PASSWD>";

// allows you to set the realm of authentication Default:"Login Required"
const char* www_realm = "Custom Auth Realm";
// the Content of the HTML response in case of Unautherized Access Default:empty
String authFailResponse = "Authentication Failed";

const char* www_username = "admin";
const char* www_password = "esp8266";

AsyncWebServer httpServer(80);

void serverRouting();

void setup(void) {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  Serial.print(F("Inizializing FS..."));
  if (SPIFFS.begin()){
	Serial.println(F("done."));
  }else{
	Serial.println(F("fail."));
  }

  Serial.println("Set routing for http server!");
  serverRouting();
  httpServer.begin();
  Serial.println("HTTP server started");
}

void loop(void) {

}

String getContentType(String filename){
  if(filename.endsWith(F(".htm"))) 			return F("text/html");
  else if(filename.endsWith(F(".html"))) 	return F("text/html");
  else if(filename.endsWith(F(".css"))) 	return F("text/css");
  else if(filename.endsWith(F(".js"))) 		return F("application/javascript");
  else if(filename.endsWith(F(".json"))) 	return F("application/json");
  else if(filename.endsWith(F(".png"))) 	return F("image/png");
  else if(filename.endsWith(F(".gif"))) 	return F("image/gif");
  else if(filename.endsWith(F(".jpg"))) 	return F("image/jpeg");
  else if(filename.endsWith(F(".jpeg"))) 	return F("image/jpeg");
  else if(filename.endsWith(F(".ico"))) 	return F("image/x-icon");
  else if(filename.endsWith(F(".xml"))) 	return F("text/xml");
  else if(filename.endsWith(F(".pdf"))) 	return F("application/x-pdf");
  else if(filename.endsWith(F(".zip"))) 	return F("application/x-zip");
  else if(filename.endsWith(F(".gz"))) 		return F("application/x-gzip");
  return F("text/plain");
}

bool handleFileRead(AsyncWebServerRequest *request, String path){
  Serial.print(F("handleFileRead: "));
  Serial.println(path);

  if (!request->authenticate(www_username, www_password)) {
	  Serial.print(F("NOT AUTHENTICATE!"));
	  request->requestAuthentication();
  }

  if(path.endsWith("/")) path += F("index.html");           // If a folder is requested, send the index file
  String contentType = getContentType(path);             	// Get the MIME type
  String pathWithGz = path + F(".gz");
  if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){  	// If the file exists, either as a compressed archive, or normal
	bool gzipped = false;

    if(SPIFFS.exists(pathWithGz)) {                        	// If there's a compressed version available
      path += F(".gz");                                     // Use the compressed version
      gzipped = true;
    }
    AsyncWebServerResponse *response = request->beginResponse(SPIFFS, path, contentType);
    if (gzipped){
    	response->addHeader("Content-Encoding", "gzip");
    }
    Serial.print("Real file path: ");
    Serial.println(path);

    request->send(response);

    return true;
  }
  Serial.println(String(F("\tFile Not Found: ")) + path);
  return false;                                          	// If the file doesn't exist, return false
}

void handleNotFound(AsyncWebServerRequest *request) {
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += request->url();
  message += "\nMethod: ";
  message += (request->method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += request->args();
  message += "\n";

  for (uint8_t i = 0; i < request->args(); i++) {
    message += " " + request->argName(i) + ": " + request->arg(i) + "\n";
  }

  request->send(404, "text/plain", message);
}

void serverRouting() {
	  httpServer.onNotFound([](AsyncWebServerRequest *request) {                              // If the client requests any URI
		  Serial.println(F("On not found"));
	    if (!handleFileRead(request, request->url())){                  // send it if it exists
	    	handleNotFound(request); // otherwise, respond with a 404 (Not Found) error
	    }
	  });

	  Serial.println(F("Set cache!"));
	  // Serve a file with no cache so every tile It's downloaded
	  httpServer.serveStatic("/configuration.json", SPIFFS, "/configuration.json","no-cache, no-store, must-revalidate");
	  // Server all other page with long cache so browser chaching they
      // Comment this line for esp8266
	  httpServer.serveStatic("/", SPIFFS, "/","max-age=31536000");
}

Simple token authentication

Ok, this was a solution, but the Basic Auth login form is really bad, so my idea is to manage a simple token authentication, and use a cookie to store out token.

The token will be generated with a simple cryptographic hash function (SHA1), it will be useful for not making the token decode in an easy way. For ESP32 Hash.h not exist, so we are going to use core function to recreate the sha1 function.

SHA1 implementation for ESP32

Now for ESP32 so you must remove "hash.h" and import "mbedtls/md.h".
Here the esp8266 code.

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>
#include "Hash.h"
[...]
ESP8266WebServer httpServer(80);

Here the resulting code.

#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>
#include "mbedtls/md.h"
[...]
WebServer httpServer(80);
[...]
String sha1(String payloadStr){
	const char *payload = payloadStr.c_str();

	int size = 20;

	byte shaResult[size];

	mbedtls_md_context_t ctx;
	mbedtls_md_type_t md_type = MBEDTLS_MD_SHA1;

	const size_t payloadLength = strlen(payload);

	mbedtls_md_init(&ctx);
	mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0);
	mbedtls_md_starts(&ctx);
	mbedtls_md_update(&ctx, (const unsigned char *) payload, payloadLength);
	mbedtls_md_finish(&ctx, shaResult);
	mbedtls_md_free(&ctx);

    String hashStr = "";

    for(uint16_t i = 0; i < size; i++) {
        String hex = String(shaResult[i], HEX);
        if(hex.length() < 2) {
            hex = "0" + hex;
        }
        hashStr += hex;
    }

    return hashStr;
}

I add the complete code and I’ll go explain the complete sketch and login form.

/*
 *  WeMos D1 mini (esp8266)
 *  Simple web server that read from SPIFFS and
 *  stream on browser various type of file
 *  and manage gzip format also.
 *  Here I add a management of a token authentication
 *  with a custom login form, and relative logout.
 *
 *  by Mischianti Renzo <https://mischianti.org>
 *
 *  https://mischianti.org/category/tutorial/how-to-create-a-web-server-with-esp8266-and-esp32/
 *
 */
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <FS.h>
#include "Hash.h"

const char* ssid = "<YOUR-SSID>";
const char* password = "<YOUR-PASSWD>";

const char* www_username = "admin";
const char* www_password = "esp8266";

ESP8266WebServer httpServer(80);

void serverRouting();

void setup(void) {
	Serial.begin(115200);
	WiFi.mode(WIFI_STA);
	WiFi.begin(ssid, password);
	Serial.println("");

	// Wait for connection
	while (WiFi.status() != WL_CONNECTED) {
		delay(500);
		Serial.print(".");
	}

	Serial.println("");
	Serial.print("Connected to ");
	Serial.println(ssid);
	Serial.print("IP address: ");
	Serial.println(WiFi.localIP());

	Serial.print(F("Inizializing FS..."));
	if (SPIFFS.begin()) {
		Serial.println(F("done."));
	} else {
		Serial.println(F("fail."));
	}

	Serial.println("Set routing for http server!");
	serverRouting();
	httpServer.begin();
	Serial.println("HTTP server started");
}

void loop(void) {
	httpServer.handleClient();
}

String getContentType(String filename) {
	if (filename.endsWith(F(".htm"))) return F("text/html");
	else if (filename.endsWith(F(".html"))) return F("text/html");
	else if (filename.endsWith(F(".css"))) return F("text/css");
	else if (filename.endsWith(F(".js"))) return F("application/javascript");
	else if (filename.endsWith(F(".json"))) return F("application/json");
	else if (filename.endsWith(F(".png"))) return F("image/png");
	else if (filename.endsWith(F(".gif"))) return F("image/gif");
	else if (filename.endsWith(F(".jpg"))) return F("image/jpeg");
	else if (filename.endsWith(F(".jpeg"))) return F("image/jpeg");
	else if (filename.endsWith(F(".ico"))) return F("image/x-icon");
	else if (filename.endsWith(F(".xml"))) return F("text/xml");
	else if (filename.endsWith(F(".pdf"))) return F("application/x-pdf");
	else if (filename.endsWith(F(".zip"))) return F("application/x-zip");
	else if (filename.endsWith(F(".gz"))) return F("application/x-gzip");
	return F("text/plain");
}

bool handleFileRead(String path) {
	Serial.print(F("handleFileRead: "));
	Serial.println(path);

	if (!is_authenticated()) {
		Serial.println(F("Go on not login!"));
		path = "/login.html";
	} else {
		if (path.endsWith("/")) path += F("index.html"); // If a folder is requested, send the index file
	}
	String contentType = getContentType(path);             	// Get the MIME type
	String pathWithGz = path + F(".gz");
	if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { // If the file exists, either as a compressed archive, or normal
		if (SPIFFS.exists(pathWithGz)) // If there's a compressed version available
			path += F(".gz");                      // Use the compressed version
		fs::File file = SPIFFS.open(path, "r");                 // Open the file
		size_t sent = httpServer.streamFile(file, contentType); // Send it to the client
		file.close();                                    // Close the file again
		Serial.println(
				String(F("\tSent file: ")) + path + String(F(" of size "))
						+ sent);
		return true;
	}
	Serial.println(String(F("\tFile Not Found: ")) + path);
	return false;                     // If the file doesn't exist, return false
}

void handleNotFound() {
	String message = "File Not Found\n\n";
	message += "URI: ";
	message += httpServer.uri();
	message += "\nMethod: ";
	message += (httpServer.method() == HTTP_GET) ? "GET" : "POST";
	message += "\nArguments: ";
	message += httpServer.args();
	message += "\n";

	for (uint8_t i = 0; i < httpServer.args(); i++) {
		message += " " + httpServer.argName(i) + ": " + httpServer.arg(i)
				+ "\n";
	}

	httpServer.send(404, "text/plain", message);
}

void handleLogin() {
	Serial.println("Handle login");
	String msg;
	if (httpServer.hasHeader("Cookie")) {
		// Print cookies
		Serial.print("Found cookie: ");
		String cookie = httpServer.header("Cookie");
		Serial.println(cookie);
	}

	if (httpServer.hasArg("username") && httpServer.hasArg("password")) {
		Serial.print("Found parameter: ");

		if (httpServer.arg("username") == String(www_username) && httpServer.arg("password") == String(www_password)) {
			httpServer.sendHeader("Location", "/");
			httpServer.sendHeader("Cache-Control", "no-cache");

			String token = sha1(String(www_username) + ":" + String(www_password) + ":" + httpServer.client().remoteIP().toString());
			httpServer.sendHeader("Set-Cookie", "ESPSESSIONID=" + token);

			httpServer.send(301);
			Serial.println("Log in Successful");
			return;
		}
		msg = "Wrong username/password! try again.";
		Serial.println("Log in Failed");
		httpServer.sendHeader("Location", "/login.html?msg=" + msg);
		httpServer.sendHeader("Cache-Control", "no-cache");
		httpServer.send(301);
		return;
	}
}

/**
 * Manage logout (simply remove correct token and redirect to login form)
 */
void handleLogout() {
	Serial.println("Disconnection");
	httpServer.sendHeader("Location", "/login.html?msg=User disconnected");
	httpServer.sendHeader("Cache-Control", "no-cache");
	httpServer.sendHeader("Set-Cookie", "ESPSESSIONID=0");
	httpServer.send(301);
	return;
}
//Check if header is present and correct
bool is_authenticated() {
	Serial.println("Enter is_authenticated");
	if (httpServer.hasHeader("Cookie")) {
		Serial.print("Found cookie: ");
		String cookie = httpServer.header("Cookie");
		Serial.println(cookie);

		String token = sha1(String(www_username) + ":" +
				String(www_password) + ":" +
				httpServer.client().remoteIP().toString());
//	token = sha1(token);

		if (cookie.indexOf("ESPSESSIONID=" + token) != -1) {
			Serial.println("Authentication Successful");
			return true;
		}
	}
	Serial.println("Authentication Failed");
	return false;
}

void serverRouting() {
	// External rest end point (out of authentication)
	httpServer.on("/login", HTTP_POST, handleLogin);
	httpServer.on("/logout", HTTP_GET, handleLogout);

	Serial.println(F("Go on not found!"));
	httpServer.onNotFound([]() {               // If the client requests any URI
				Serial.println(F("On not found"));
				if (!handleFileRead(httpServer.uri())) { // send it if it exists
					handleNotFound();// otherwise, respond with a 404 (Not Found) error
				}
			});

	Serial.println(F("Set cache!"));
	// Serve a file with no cache so every tile It's downloaded
	httpServer.serveStatic("/configuration.json", SPIFFS, "/configuration.json", "no-cache, no-store, must-revalidate");
	// Server all other page with long cache so browser chaching they
      // Comment this line for esp8266
	httpServer.serveStatic("/", SPIFFS, "/", "max-age=31536000");

	//here the list of headers to be recorded
	const char * headerkeys[] = { "User-Agent", "Cookie" };
	size_t headerkeyssize = sizeof(headerkeys) / sizeof(char*);
	//ask server to track these headers
	httpServer.collectHeaders(headerkeys, headerkeyssize);
}

And a login page

<!DOCTYPE html>
<html lang="en">
<head>
    <style>
        @import url(https://fonts.googleapis.com/css?family=Roboto:300);

        .login-page {
            width: 360px;
            padding: 8% 0 0;
            margin: auto;
        }

        .form {
            position: relative;
            z-index: 1;
            background: #FFFFFF;
            max-width: 360px;
            margin: 0 auto 100px;
            padding: 45px;
            text-align: center;
            box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
        }

        .form input {
            font-family: "Roboto", sans-serif;
            outline: 0;
            background: #f2f2f2;
            width: 100%;
            border: 0;
            margin: 0 0 15px;
            padding: 15px;
            box-sizing: border-box;
            font-size: 14px;
        }

        .form button {
            font-family: "Roboto", sans-serif;
            text-transform: uppercase;
            outline: 0;
            background: #4CAF50;
            width: 100%;
            border: 0;
            padding: 15px;
            color: #FFFFFF;
            font-size: 14px;
            -webkit-transition: all 0.3 ease;
            transition: all 0.3 ease;
            cursor: pointer;
        }

        .form button:hover, .form button:active, .form button:focus {
            background: #43A047;
        }

        .form .message {
            margin: 15px 0 0;
            color: #b3b3b3;
            font-size: 12px;
        }

        .form .message a {
            color: #4CAF50;
            text-decoration: none;
        }

        .form .register-form {
            display: none;
        }

        .container {
            position: relative;
            z-index: 1;
            max-width: 300px;
            margin: 0 auto;
        }

        .container:before, .container:after {
            content: "";
            display: block;
            clear: both;
        }

        .container .info {
            margin: 50px auto;
            text-align: center;
        }

        .container .info h1 {
            margin: 0 0 15px;
            padding: 0;
            font-size: 36px;
            font-weight: 300;
            color: #1a1a1a;
        }

        .container .info span {
            color: #4d4d4d;
            font-size: 12px;
        }

        .container .info span a {
            color: #000000;
            text-decoration: none;
        }

        .container .info span .fa {
            color: #EF3B3A;
        }

        body {
            background: #76b852; /* fallback for old browsers */
            background: -webkit-linear-gradient(right, #76b852, #8DC26F);
            background: -moz-linear-gradient(right, #76b852, #8DC26F);
            background: -o-linear-gradient(right, #76b852, #8DC26F);
            background: linear-gradient(to left, #76b852, #8DC26F);
            font-family: "Roboto", sans-serif;
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
        }

        .logo-image {
            background-image: url(./logo256.jpg);
            background-position: center;
            background-size: contain;
            background-repeat: no-repeat;
            height: 120px;
        }
    </style>
    <script>
    </script>
</head>
    <body>
    <div class="login-page">
        <div class="form">
            <div class="logo-image"></div>

            <form action="/login" method="POST" class="login-form" style="padding-top: 20px;">
                <input id="username" name="username" type="text" placeholder="username"/>
                <input id="password" name="password" type="password" placeholder="password"/>
                <button type='submit' name='SUBMIT' value='Submit' >Login</button>
                <div style="margin-top: 10px; color: red;font-size: small;" id="message"></div>
            </form>
        </div>
    </div>
    </body>
<script>
    function getURLParameter(sParam)
    {
        var sPageURL = window.location.search.substring(1);
        var sURLVariables = sPageURL.split('&');
        for (var i = 0; i < sURLVariables.length; i++)
        {
            var sParameterName = sURLVariables[i].split('=');
            if (sParameterName[0] == sParam)
            {
                return sParameterName[1];
            }
        }
    }
    if (getURLParameter("msg")) {
        document.getElementById("message").innerText = decodeURI(getURLParameter("msg"));
    }
</script>
</html>

Here the result

WebServer esp8266 login page
WebServer esp8266 login page

The authentication, like the other programming languages, must work like a filter on all request, in this case that code is:

	if (!is_authenticated()) {
		Serial.println(F("Go on not login!"));
		path = "/login.html";
	} else {
		if (path.endsWith("/")) path += F("index.html"); // If a folder is requested, send the index file
	}

it check is is authenticated and if It isn’t force the page to serve to login.html.

The login page simply POST a username and password on form submit to the /login end point, with httpServer.hasArg("username") you can check if parameter is present, with httpServer.arg("username") you get the parameter value.

	// External rest end point (out of authentication)
	httpServer.on("/login", HTTP_POST, handleLogin);
	httpServer.on("/logout", HTTP_GET, handleLogout);

The login, like logout is out of authentication filter, and It’s processed, inside the method check if the username and password are correct

	if (httpServer.hasArg("username") && httpServer.hasArg("password")) {
		Serial.print("Found parameter: ");

		if (httpServer.arg("username") == String(www_username) && httpServer.arg("password") == String(www_password)) {

if they are correct, generate a Set-Cookie and put the token inside, than send a 301 http status code to a Location to generate a redirect.

			httpServer.sendHeader("Location", "/");
			httpServer.sendHeader("Cache-Control", "no-cache");

			String token = sha1(String(www_username) + ":" + String(www_password) + ":" + WiFi.localIP().toString());
			httpServer.sendHeader("Set-Cookie", "ESPSESSIONID=" + token);

			httpServer.send(301);
			Serial.println("Log in Successful");
			return;

the value of cookie is a token generated with the sha1 of user, password and ip address of the client, for a better security you can add a mac address or a day.

void handleLogin() {
	Serial.println("Handle login");
	String msg;
	if (httpServer.hasHeader("Cookie")) {
		// Print cookies
		Serial.print("Found cookie: ");
		String cookie = httpServer.header("Cookie");
		Serial.println(cookie);
	}

	if (httpServer.hasArg("username") && httpServer.hasArg("password")) {
		Serial.print("Found parameter: ");

		if (httpServer.arg("username") == String(www_username) && httpServer.arg("password") == String(www_password)) {
			httpServer.sendHeader("Location", "/");
			httpServer.sendHeader("Cache-Control", "no-cache");

			String token = sha1(String(www_username) + ":" + String(www_password) + ":" + httpServer.client().remoteIP().toString());
			httpServer.sendHeader("Set-Cookie", "ESPSESSIONID=" + token);

			httpServer.send(301);
			Serial.println("Log in Successful");
			return;
		}
		msg = "Wrong username/password! try again.";
		Serial.println("Log in Failed");
		httpServer.sendHeader("Location", "/login.html?msg=" + msg);
		httpServer.sendHeader("Cache-Control", "no-cache");
		httpServer.send(301);
		return;
	}
}

The result of this operation is that now you have a cookie for your domain with a token.

esp8266 Login cookies management
esp8266 Login cookies management

As you can see if you enter a wrong password you are redirected to a login page with a message to show on It.

The authentication check, at this point, is quite simple

//Check if header is present and correct
bool is_authenticated() {
	Serial.println("Enter is_authenticated");
	if (httpServer.hasHeader("Cookie")) {
		Serial.print("Found cookie: ");
		String cookie = httpServer.header("Cookie");
		Serial.println(cookie);

		String token = sha1(String(www_username) + ":" +
				String(www_password) + ":" +
				httpServer.client().remoteIP().toString());
//	token = sha1(token);

		if (cookie.indexOf("ESPSESSIONID=" + token) != -1) {
			Serial.println("Authentication Successful");
			return true;
		}
	}
	Serial.println("Authentication Failed");
	return false;
}

it retrieve the token from cookies and check if It’s ok with local credential and client IP.

The cookie I use here is a Session cookie, so It’s are persistent only for the current browser session. Every time you do a request from this browser and from the WebServer domain, the cookie, is automatically attach to the header and can be retrieved from the server.

As login, logout simply remove/change the original cookie and invalidate authentication session.

esp8266 Logout cookies management
esp8266 Logout cookies management

So now you have a complete custom handshake login/logout, with quite good security management. But for a complete solution you must add https with certifiate and similar.

I discourage to do this step with the microcontroller for more than one reason:

  • you only add a self signed certificate in your LAN, so more than one modern browser block your site, and the request of authorization is quite tedious.
  • you probably use this device only in the local area network (LAN), and It’s improbably that other peoples start sniffing your traffic.
  • if you want use this site in Internet you can use a border apache (not the American Indians) or NGINX, that work like a proxy and you can attach the certificate to this PC and manage It to do the port change (433 to 80) and relative url rewrite (if you want).

AsyncWebServer

Here the implementation with AsyncWebServer.

/*
 *  ESP32
 *  Simple web server that read from SPIFFS and
 *  stream on browser various type of file
 *  and manage gzip format also.
 *  Here I add a management of a token authentication
 *  with a custom login form, and relative logout.
 *
 *  by Mischianti Renzo <https://mischianti.org>
 *
 *  https://mischianti.org/
 *
 */
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#include "mbedtls/md.h"

const char* ssid = "<YOUR-SSID>";
const char* password = "<YOUR-PASSWD>";

const char* www_username = "admin";
const char* www_password = "esp8266";

AsyncWebServer httpServer(80);

void serverRouting();

String sha1(String payloadStr){
	const char *payload = payloadStr.c_str();

	int size = 20;

	byte shaResult[size];

	mbedtls_md_context_t ctx;
	mbedtls_md_type_t md_type = MBEDTLS_MD_SHA1;

	const size_t payloadLength = strlen(payload);

	mbedtls_md_init(&ctx);
	mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0);
	mbedtls_md_starts(&ctx);
	mbedtls_md_update(&ctx, (const unsigned char *) payload, payloadLength);
	mbedtls_md_finish(&ctx, shaResult);
	mbedtls_md_free(&ctx);

    String hashStr = "";

    for(uint16_t i = 0; i < size; i++) {
        String hex = String(shaResult[i], HEX);
        if(hex.length() < 2) {
            hex = "0" + hex;
        }
        hashStr += hex;
    }

    return hashStr;
}

void setup(void) {
	Serial.begin(115200);
	WiFi.mode(WIFI_STA);
	WiFi.begin(ssid, password);
	Serial.println("");

	// Wait for connection
	while (WiFi.status() != WL_CONNECTED) {
		delay(500);
		Serial.print(".");
	}

	Serial.println("");
	Serial.print("Connected to ");
	Serial.println(ssid);
	Serial.print("IP address: ");
	Serial.println(WiFi.localIP());

	Serial.print(F("Inizializing FS..."));
	if (SPIFFS.begin()) {
		Serial.println(F("done."));
	} else {
		Serial.println(F("fail."));
	}

	Serial.println("Set routing for http server!");
	serverRouting();
	httpServer.begin();
	Serial.println("HTTP server started");
}

void loop(void) {

}

String getContentType(String filename) {
	if (filename.endsWith(F(".htm"))) return F("text/html");
	else if (filename.endsWith(F(".html"))) return F("text/html");
	else if (filename.endsWith(F(".css"))) return F("text/css");
	else if (filename.endsWith(F(".js"))) return F("application/javascript");
	else if (filename.endsWith(F(".json"))) return F("application/json");
	else if (filename.endsWith(F(".png"))) return F("image/png");
	else if (filename.endsWith(F(".gif"))) return F("image/gif");
	else if (filename.endsWith(F(".jpg"))) return F("image/jpeg");
	else if (filename.endsWith(F(".jpeg"))) return F("image/jpeg");
	else if (filename.endsWith(F(".ico"))) return F("image/x-icon");
	else if (filename.endsWith(F(".xml"))) return F("text/xml");
	else if (filename.endsWith(F(".pdf"))) return F("application/x-pdf");
	else if (filename.endsWith(F(".zip"))) return F("application/x-zip");
	else if (filename.endsWith(F(".gz"))) return F("application/x-gzip");
	return F("text/plain");
}

bool handleFileRead(AsyncWebServerRequest *request, String path) {
	Serial.print(F("handleFileRead: "));
	Serial.println(path);

	if (!is_authenticated(request)) {
		Serial.println(F("Go on not login!"));
		path = "/login.html";
	} else {
		if (path.endsWith("/")) path += F("index.html"); // If a folder is requested, send the index file
	}
	String contentType = getContentType(path);             	// Get the MIME type
	String pathWithGz = path + F(".gz");
	  if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){  	// If the file exists, either as a compressed archive, or normal
		bool gzipped = false;

	    if(SPIFFS.exists(pathWithGz)) {                        	// If there's a compressed version available
	      path += F(".gz");                                     // Use the compressed version
	      gzipped = true;
	    }
	    AsyncWebServerResponse *response = request->beginResponse(SPIFFS, path, contentType);
	    if (gzipped){
	    	response->addHeader("Content-Encoding", "gzip");
	    }
	    Serial.print("Real file path: ");
	    Serial.println(path);

	    request->send(response);

	    return true;
	  }
	Serial.println(String(F("\tFile Not Found: ")) + path);
	return false;                     // If the file doesn't exist, return false
}

void handleNotFound(AsyncWebServerRequest *request) {
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += request->url();
  message += "\nMethod: ";
  message += (request->method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += request->args();
  message += "\n";

  for (uint8_t i = 0; i < request->args(); i++) {
    message += " " + request->argName(i) + ": " + request->arg(i) + "\n";
  }

  request->send(404, "text/plain", message);
}

void handleLogin(AsyncWebServerRequest *request) {
	Serial.println("Handle login");
	String msg;
	if (request->hasHeader("Cookie")) {
		// Print cookies
		Serial.print("Found cookie: ");
		String cookie = request->header("Cookie");
		Serial.println(cookie);
	}

	if (request->hasArg("username") && request->hasArg("password")) {
		Serial.print("Found parameter: ");

		if (request->arg("username") == String(www_username) && request->arg("password") == String(www_password)) {
			AsyncWebServerResponse *response = request->beginResponse(301); //Sends 301 redirect

			response->addHeader("Location", "/");
			response->addHeader("Cache-Control", "no-cache");

			String token = sha1(String(www_username) + ":" + String(www_password) + ":" + request->client()->remoteIP().toString());
			Serial.print("Token: ");
			Serial.println(token);
			response->addHeader("Set-Cookie", "ESPSESSIONID=" + token);

			request->send(response);
			Serial.println("Log in Successful");
			return;
		}
		msg = "Wrong username/password! try again.";
		Serial.println("Log in Failed");
		AsyncWebServerResponse *response = request->beginResponse(301); //Sends 301 redirect

		response->addHeader("Location", "/login.html?msg=" + msg);
		response->addHeader("Cache-Control", "no-cache");
		request->send(response);
		return;
	}
}

/**
 * Manage logout (simply remove correct token and redirect to login form)
 */
void handleLogout(AsyncWebServerRequest *request) {
	Serial.println("Disconnection");
	AsyncWebServerResponse *response = request->beginResponse(301); //Sends 301 redirect

	response->addHeader("Location", "/login.html?msg=User disconnected");
	response->addHeader("Cache-Control", "no-cache");
	response->addHeader("Set-Cookie", "ESPSESSIONID=0");
	request->send(response);
	return;
}
//Check if header is present and correct
bool is_authenticated(AsyncWebServerRequest *request) {
	Serial.println("Enter is_authenticated");
	if (request->hasHeader("Cookie")) {
		Serial.print("Found cookie: ");
		String cookie = request->header("Cookie");
		Serial.println(cookie);

		String token = sha1(String(www_username) + ":" +
				String(www_password) + ":" +
				request->client()->remoteIP().toString());
//	token = sha1(token);

		if (cookie.indexOf("ESPSESSIONID=" + token) != -1) {
			Serial.println("Authentication Successful");
			return true;
		}
	}
	Serial.println("Authentication Failed");
	return false;
}

void serverRouting() {
	// External rest end point (out of authentication)
	httpServer.on("/login", HTTP_POST, handleLogin);
	httpServer.on("/logout", HTTP_GET, handleLogout);

	Serial.println(F("Go on not found!"));
	httpServer.onNotFound([](AsyncWebServerRequest *request) {               // If the client requests any URI
				Serial.println(F("On not found"));
			    if (!handleFileRead(request, request->url())){                  // send it if it exists
			    	handleNotFound(request); // otherwise, respond with a 404 (Not Found) error
				}
			});

	Serial.println(F("Set cache!"));
	// Serve a file with no cache so every tile It's downloaded
	httpServer.serveStatic("/configuration.json", SPIFFS, "/configuration.json", "no-cache, no-store, must-revalidate");
	// Server all other page with long cache so browser chaching they
    // Comment this line for esp8266
	httpServer.serveStatic("/", SPIFFS, "/", "max-age=31536000");
}

As you can see the implementation is similar, and there aren’t difficult to switch from an implementation to another.

Thanks

But as you can see there isn’t a fundamental part: we don’t interact with the micro controller data, so next step is to manage this data exchange.

  1. Web Server with esp8266 and esp32: serve pages and manage LEDs
  2. Web Server with esp8266 and esp32: byte array gzipped pages and SPIFFS
  3. Web Server with esp8266 and esp32: multi purpose generic web server
  4. Web Server with esp8266 and esp32: manage security and authentication
  5. Web Server with esp8266 and esp32: add secure REST back-end
  6. Web Server with esp8266 and esp32: DHT temperature humidity on protected Web Interface

Code and examples on this repository GitHub


Spread the love

8 Responses

  1. Cabuzel Thierry says:

    In the line
    httpServer.serveStatic(“/configuration.json”, SPIFFS, “/configuration.json”,”no-cache, no-store, must-revalidate”);

    how the authentication is managed (where do we check for the authentication Cookie)?

  2. Mattia Paletti says:

    Hi, i have a big problem with this project: When i type the ip addess of the esp8266 (I have the esp-01 version and it gives no error when I load the sketch or the folder with the html files) I am shown the index page and not the login page which I can only see if I type /login.html manually on the address bar.
    I copied and pasted the code you posted here without making any changes but it doesn’t work … Even changing browser and deleting cache and cookies the login page is not shown.

    In the serial console this is shown:

    Connected to FRITZ!Box 7590
    IP address: 192.168.2.50
    Inizializing FS…done.
    Set routing for http server!
    Go on not found!
    Set cache!
    HTTP server started
    On not found
    handleFileRead: /favicon.ico
    Enter is_authenticated
    Authentication Failed
    Go on not login!
    Sent file: /login.html of size 4711
    On not found
    handleFileRead: /favicon.ico
    Enter is_authenticated
    Authentication Failed
    Go on not login!
    Sent file: /login.html of size 4711

    • Hi Mattia,
      there is a change on the last versione on esp8266 cache management, to solve the issue the fastest way is to disable the cache for static content by comment this line:

      	// httpServer.serveStatic("/", SPIFFS, "/", "max-age=31536000");
      

      The best solution is to send a 301 with login page.

      Bye Renzo

  3. Zzo says:

    Hello,

    First, many thanks for this tutorial.
    I have downloaded the code from github to test it.
    When I go to my ESP32’s IP I get the login page, so far so good.
    I enter the user/password and I’m redirected to the humidity widget, then I do a logout so the cookie is set to “ESPSESSIONID=0” instead of a long string.
    But if I add directly /index.html in th eURL I get directly the humidity sensor widget bypassing the login user/password verification.
    How can I avoid being able to enter a protected page with an empty token in the cookie?
    The authentication is failed in the serial console but the widget page is displayed anyway.

    20:25:20.539 -> Enter is_authenticated
    20:25:20.539 -> Found cookie: ESPSESSIONID=0
    20:25:20.539 -> Authentication Failed
    20:25:20.539 -> handleTemperatureHumidity security pass!

    Regards

    • Hi Zzo,
      if you check the browser console from REST end point you receive 401 (Unauthorized) but you continue to see index.html (with 0 value) because you must disable the cache by commenting this line

      	// httpServer.serveStatic("/", SPIFFS, "/", "max-age=31536000");
      

      but you must bypass image authorization control to show It.

      If you want to do something more pretty you can move the authentication check on serverRouting and if It’s unauthenticated call handleLogout that force a redirect.
      Bye Renzo

Leave a Reply

Your email address will not be published. Required fields are marked *