Web Server with esp8266 and esp32: manage security and authentication – 4
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
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
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.
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.
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.
- Web Server with esp8266 and esp32: serve pages and manage LEDs
- Web Server with esp8266 and esp32: byte array gzipped pages and SPIFFS
- Web Server with esp8266 and esp32: multi purpose generic web server
- Web Server with esp8266 and esp32: manage security and authentication
- Web Server with esp8266 and esp32: add secure REST back-end
- Web Server with esp8266 and esp32: DHT temperature humidity on protected Web Interface
Code and examples on this repository GitHub
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)?
Hi Cabuz,
you can check in the middle/end of the article.
Bye Renzo
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:
The best solution is to send a 301 with login page.
Bye Renzo
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
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
Hi good job,
Any way to use the AsyncWebServer but with ssl ?
Hi,
I find this issue and seems that there are some problem.
But in the code of ESPAsyncWebserver library exist a define to enable some specified features for SSL, check this.
Bye Renzo