[web_server] Fix basic auth with long credentials (#18237)

This commit is contained in:
J. Nick Koston
2026-08-10 14:49:14 -05:00
committed by GitHub
parent 334afdefb5
commit 8a16ead8ce
8 changed files with 120 additions and 15 deletions
+15 -3
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import base64
import gzip
import logging
import re
@@ -406,10 +407,21 @@ async def to_code(config):
# The scheme is fixed at build time so the unused Basic/Digest code path is compiled
# out. Basic is the current default (the absence of this define); an explicit
# 'type: digest' opts in early. Default changes to digest in 2027.1.0.
if auth.get(CONF_TYPE) == AUTH_TYPE_DIGEST:
is_digest = auth.get(CONF_TYPE) == AUTH_TYPE_DIGEST
if is_digest:
cg.add_define("USE_WEBSERVER_AUTH_DIGEST")
cg.add(paren.set_auth_username(auth[CONF_USERNAME]))
cg.add(paren.set_auth_password(auth[CONF_PASSWORD]))
if is_digest or CORE.is_esp32:
cg.add(paren.set_auth_username(auth[CONF_USERNAME]))
cg.add(paren.set_auth_password(auth[CONF_PASSWORD]))
else:
# Every non-ESP32 basic auth build takes this path. The ESP8266 and RP2040
# core base64 encoders wrap output every 72 chars, which breaks
# ESPAsyncWebServer's basic auth compare for long credentials.
# Precompute the hash here and let C++ compare the raw header payload.
basic_hash = base64.b64encode(
f"{auth[CONF_USERNAME]}:{auth[CONF_PASSWORD]}".encode()
).decode()
cg.add(paren.set_auth_basic_hash(basic_hash))
if CONF_CSS_INCLUDE in config:
cg.add_define("USE_WEBSERVER_CSS_INCLUDE")
path = CORE.relative_config_path(config[CONF_CSS_INCLUDE])
@@ -7,7 +7,7 @@ WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-av
void WebServerBase::add_handler(AsyncWebHandler *handler) {
#ifdef USE_WEBSERVER_AUTH
if (!credentials_.username.empty()) {
if (credentials_.is_set()) {
handler = new internal::AuthMiddlewareHandler(handler, &credentials_);
}
#endif
@@ -1,7 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR)
#include <utility>
#include <vector>
#include "esphome/core/progmem.h"
@@ -46,9 +45,20 @@ class MiddlewareHandler : public AsyncWebHandler {
};
#ifdef USE_WEBSERVER_AUTH
// All fields point to string literals in generated code; nothing is copied.
struct Credentials {
std::string username;
std::string password;
#if USE_ESP32 || defined(USE_WEBSERVER_AUTH_DIGEST)
const char *username{nullptr};
const char *password{nullptr};
bool is_set() const { return username != nullptr; }
#else
// base64("username:password"), precomputed at codegen time. Used by every non-ESP32 basic
// auth build. The ESP8266 and RP2040 core libb64 wraps base64 output every 72 chars, so
// letting the library encode and compare fails for long credentials; instead the header
// payload is compared against this hash.
const char *basic_auth_hash{nullptr};
bool is_set() const { return basic_auth_hash != nullptr; }
#endif
};
class AuthMiddlewareHandler : public MiddlewareHandler {
@@ -57,10 +67,14 @@ class AuthMiddlewareHandler : public MiddlewareHandler {
: MiddlewareHandler(next), credentials_(credentials) {}
bool check_auth(AsyncWebServerRequest *request) {
bool success = request->authenticate(credentials_->username.c_str(), credentials_->password.c_str());
// The scheme is chosen at build time (USE_WEBSERVER_AUTH_DIGEST); the unused path is
// compiled out. On ESP32 our own server picks the scheme internally.
#if USE_ESP32 || defined(USE_WEBSERVER_AUTH_DIGEST)
bool success = request->authenticate(credentials_->username, credentials_->password);
#else
bool success = request->authenticate(credentials_->basic_auth_hash);
#endif
if (!success) {
// The scheme is chosen at build time (USE_WEBSERVER_AUTH_DIGEST); the unused path is
// compiled out. On ESP32 our own server picks the scheme internally.
#if USE_ESP32
request->requestAuthentication();
#elif defined(USE_WEBSERVER_AUTH_DIGEST)
@@ -125,8 +139,12 @@ class WebServerBase final {
AsyncWebServer *get_server() const { return this->server_; }
#ifdef USE_WEBSERVER_AUTH
void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); }
void set_auth_password(std::string auth_password) { credentials_.password = std::move(auth_password); }
#if USE_ESP32 || defined(USE_WEBSERVER_AUTH_DIGEST)
void set_auth_username(const char *auth_username) { credentials_.username = auth_username; }
void set_auth_password(const char *auth_password) { credentials_.password = auth_password; }
#else
void set_auth_basic_hash(const char *hash) { credentials_.basic_auth_hash = hash; }
#endif
#endif
void add_handler(AsyncWebHandler *handler);
@@ -33,14 +33,49 @@ def test_web_server_auth_explicit_basic_no_warning(
generate_main: Callable[[str], str],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Auth type basic builds Basic and does not warn."""
generate_main("tests/component_tests/web_server/web_server_auth_basic.yaml")
"""Auth type basic on ESP32 uses plaintext credentials and does not warn."""
main_cpp = generate_main(
"tests/component_tests/web_server/web_server_auth_basic.yaml"
)
assert '->set_auth_username("admin");' in main_cpp
assert '->set_auth_password("password");' in main_cpp
assert "set_auth_basic_hash" not in main_cpp
assert _has_define("USE_WEBSERVER_AUTH")
assert not _has_define("USE_WEBSERVER_AUTH_DIGEST")
assert _DEFAULT_CHANGE_WARNING not in caplog.text
def test_web_server_auth_basic_esp8266_uses_precomputed_hash(
generate_main: Callable[[str], str],
) -> None:
"""Auth type basic on ESP8266 emits the precomputed base64 hash, not the credentials."""
main_cpp = generate_main(
"tests/component_tests/web_server/web_server_auth_basic_esp8266.yaml"
)
assert '->set_auth_basic_hash("YWRtaW46cGFzc3dvcmQ=");' in main_cpp
assert "set_auth_username" not in main_cpp
assert "set_auth_password" not in main_cpp
assert _has_define("USE_WEBSERVER_AUTH")
assert not _has_define("USE_WEBSERVER_AUTH_DIGEST")
def test_web_server_auth_digest_esp8266_uses_plaintext_credentials(
generate_main: Callable[[str], str],
) -> None:
"""Auth type digest on ESP8266 uses plaintext credentials, not the basic hash."""
main_cpp = generate_main(
"tests/component_tests/web_server/web_server_auth_digest_esp8266.yaml"
)
assert '->set_auth_username("admin");' in main_cpp
assert '->set_auth_password("password");' in main_cpp
assert "set_auth_basic_hash" not in main_cpp
assert _has_define("USE_WEBSERVER_AUTH")
assert _has_define("USE_WEBSERVER_AUTH_DIGEST")
def test_web_server_auth_explicit_digest(
generate_main: Callable[[str], str],
caplog: pytest.LogCaptureFixture,
@@ -0,0 +1,16 @@
---
esphome:
name: test
esp8266:
board: esp01_1m
wifi:
ssid: MySSID
password: password1
web_server:
auth:
username: admin
password: password
type: basic
@@ -0,0 +1,16 @@
---
esphome:
name: test
esp8266:
board: esp01_1m
wifi:
ssid: MySSID
password: password1
web_server:
auth:
username: admin
password: password
type: digest
@@ -0,0 +1,8 @@
packages:
web_server: !include common_v2.yaml
web_server:
auth:
username: admin
password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
type: basic
@@ -4,5 +4,5 @@ packages:
web_server:
auth:
username: admin
password: password
password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
type: basic