From 7e1e36040a3799ea1c0b3ff8e21382c6574c8c13 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Jan 2026 10:14:56 -0800 Subject: [PATCH] Add centralized authentication handler (issue #102) Add auth_handler callback to webserver that runs before any resource's render method. This allows defining authentication logic once for all resources instead of duplicating it in every render method. Features: - auth_handler: callback that returns nullptr to allow request or http_response to reject - auth_skip_paths: vector of paths to bypass auth (supports exact match and wildcard suffix like "/public/*") Includes comprehensive tests and example in examples/centralized_authentication.cpp --- README.md | 75 +++++ examples/centralized_authentication.cpp | 88 +++++ src/httpserver/create_webserver.hpp | 14 + src/httpserver/webserver.hpp | 4 + src/webserver.cpp | 31 +- test/integ/authentication.cpp | 412 ++++++++++++++++++++++++ 6 files changed, 622 insertions(+), 2 deletions(-) create mode 100644 examples/centralized_authentication.cpp diff --git a/README.md b/README.md index 1add295c..13dba9c3 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ libhttpserver is built upon [libmicrohttpd](https://www.gnu.org/software/libmic - Support for SHOUTcast - Support for incremental processing of POST data (optional) - Support for basic and digest authentication (optional) +- Support for centralized authentication with path-based skip rules - Support for TLS (requires libgnutls, optional) ## Table of Contents @@ -990,6 +991,80 @@ You will receive a `SUCCESS` in response (observe the response message from the You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/digest_authentication.cpp). +### Using Centralized Authentication +The examples above show authentication handled within each resource's `render_*` method. This approach requires duplicating authentication logic in every resource, which is error-prone and violates DRY (Don't Repeat Yourself) principles. + +libhttpserver provides a centralized authentication mechanism that runs a single authentication handler before any resource's render method is called. This allows you to: +- Define authentication logic once for all resources +- Automatically protect all endpoints by default +- Specify paths that should bypass authentication (e.g., health checks, public APIs) + +```cpp + #include + + using namespace httpserver; + + // Resources no longer need authentication logic + class hello_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + return std::make_shared("Hello, authenticated user!", 200, "text/plain"); + } + }; + + class health_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + return std::make_shared("OK", 200, "text/plain"); + } + }; + + // Centralized authentication handler + // Return nullptr to allow the request, or an http_response to reject it + std::shared_ptr my_auth_handler(const http_request& req) { + if (req.get_user() != "admin" || req.get_pass() != "secret") { + return std::make_shared("Unauthorized", "MyRealm"); + } + return nullptr; // Allow request to proceed to resource + } + + int main() { + webserver ws = create_webserver(8080) + .auth_handler(my_auth_handler) + .auth_skip_paths({"/health", "/public/*"}); + + hello_resource hello; + health_resource health; + + ws.register_resource("/api", &hello); + ws.register_resource("/health", &health); + + ws.start(true); + return 0; + } +``` + +The `auth_handler` callback is called for every request before the resource's render method. It receives the `http_request` and can: +- Return `nullptr` to allow the request to proceed normally +- Return an `http_response` (e.g., `basic_auth_fail_response` or `digest_auth_fail_response`) to reject the request + +The `auth_skip_paths` method accepts a vector of paths that should bypass authentication: +- Exact matches: `"/health"` matches only `/health` +- Wildcard suffixes: `"/public/*"` matches `/public/`, `/public/info`, `/public/docs/api`, etc. + +To test the above example: + + # Without auth - returns 401 Unauthorized + curl -v http://localhost:8080/api + + # With valid auth - returns 200 OK + curl -u admin:secret http://localhost:8080/api + + # Health endpoint (skip path) - works without auth + curl http://localhost:8080/health + +You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/centralized_authentication.cpp). + [Back to TOC](#table-of-contents) ## HTTP Utils diff --git a/examples/centralized_authentication.cpp b/examples/centralized_authentication.cpp new file mode 100644 index 00000000..0f965af6 --- /dev/null +++ b/examples/centralized_authentication.cpp @@ -0,0 +1,88 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011, 2012, 2013, 2014, 2015 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#include +#include + +#include + +using httpserver::http_request; +using httpserver::http_response; +using httpserver::http_resource; +using httpserver::webserver; +using httpserver::create_webserver; +using httpserver::string_response; +using httpserver::basic_auth_fail_response; + +// Simple resource that doesn't need to handle auth itself +class hello_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + return std::make_shared("Hello, authenticated user!", 200, "text/plain"); + } +}; + +class health_resource : public http_resource { + public: + std::shared_ptr render_GET(const http_request&) { + return std::make_shared("OK", 200, "text/plain"); + } +}; + +// Centralized authentication handler +// Returns nullptr to allow the request, or an http_response to reject it +std::shared_ptr auth_handler(const http_request& req) { + if (req.get_user() != "admin" || req.get_pass() != "secret") { + return std::make_shared("Unauthorized", "MyRealm"); + } + return nullptr; // Allow request +} + +int main() { + // Create webserver with centralized authentication + // - auth_handler: called before every resource's render method + // - auth_skip_paths: paths that bypass authentication + webserver ws = create_webserver(8080) + .auth_handler(auth_handler) + .auth_skip_paths({"/health", "/public/*"}); + + hello_resource hello; + health_resource health; + + ws.register_resource("/api", &hello); + ws.register_resource("/health", &health); + + ws.start(true); + + return 0; +} + +// Usage: +// # Start the server +// ./centralized_authentication +// +// # Without auth - should get 401 Unauthorized +// curl -v http://localhost:8080/api +// +// # With valid auth - should get 200 OK +// curl -u admin:secret http://localhost:8080/api +// +// # Health endpoint (skip path) - works without auth +// curl http://localhost:8080/health diff --git a/src/httpserver/create_webserver.hpp b/src/httpserver/create_webserver.hpp index ddea0d58..f18ed5b6 100644 --- a/src/httpserver/create_webserver.hpp +++ b/src/httpserver/create_webserver.hpp @@ -31,6 +31,7 @@ #include #include #include +#include #include "httpserver/http_response.hpp" #include "httpserver/http_utils.hpp" @@ -52,6 +53,7 @@ typedef std::function psk_cred_handler_callback namespace http { class file_info; } typedef std::function file_cleanup_callback_ptr; +typedef std::function(const http_request&)> auth_handler_ptr; class create_webserver { public: @@ -376,6 +378,16 @@ class create_webserver { return *this; } + create_webserver& auth_handler(auth_handler_ptr handler) { + _auth_handler = handler; + return *this; + } + + create_webserver& auth_skip_paths(const std::vector& paths) { + _auth_skip_paths = paths; + return *this; + } + private: uint16_t _port = DEFAULT_WS_PORT; http::http_utils::start_method_T _start_method = http::http_utils::INTERNAL_SELECT; @@ -423,6 +435,8 @@ class create_webserver { render_ptr _method_not_allowed_resource = nullptr; render_ptr _internal_error_resource = nullptr; file_cleanup_callback_ptr _file_cleanup_callback = nullptr; + auth_handler_ptr _auth_handler = nullptr; + std::vector _auth_skip_paths; friend class webserver; }; diff --git a/src/httpserver/webserver.hpp b/src/httpserver/webserver.hpp index 262849af..7ba48b73 100644 --- a/src/httpserver/webserver.hpp +++ b/src/httpserver/webserver.hpp @@ -45,6 +45,7 @@ #include #include #include +#include #ifdef HAVE_GNUTLS #include @@ -182,6 +183,8 @@ class webserver { const render_ptr method_not_allowed_resource; const render_ptr internal_error_resource; const file_cleanup_callback_ptr file_cleanup_callback; + const auth_handler_ptr auth_handler; + const std::vector auth_skip_paths; std::shared_mutex registered_resources_mutex; std::map registered_resources; std::map registered_resources_str; @@ -197,6 +200,7 @@ class webserver { std::shared_ptr method_not_allowed_page(details::modded_request* mr) const; std::shared_ptr internal_error_page(details::modded_request* mr, bool force_our = false) const; std::shared_ptr not_found_page(details::modded_request* mr) const; + bool should_skip_auth(const std::string& path) const; static void request_completed(void *cls, struct MHD_Connection *connection, void **con_cls, diff --git a/src/webserver.cpp b/src/webserver.cpp index a60af1d2..7d902453 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -167,7 +167,9 @@ webserver::webserver(const create_webserver& params): not_found_resource(params._not_found_resource), method_not_allowed_resource(params._method_not_allowed_resource), internal_error_resource(params._internal_error_resource), - file_cleanup_callback(params._file_cleanup_callback) { + file_cleanup_callback(params._file_cleanup_callback), + auth_handler(params._auth_handler), + auth_skip_paths(params._auth_skip_paths) { ignore_sigpipe(); pthread_mutex_init(&mutexwait, nullptr); pthread_cond_init(&mutexcond, nullptr); @@ -635,6 +637,19 @@ std::shared_ptr webserver::internal_error_page(details::modded_re } } +bool webserver::should_skip_auth(const std::string& path) const { + for (const auto& skip_path : auth_skip_paths) { + if (skip_path == path) return true; + // Support wildcard suffix (e.g., "/public/*") + if (skip_path.size() > 2 && skip_path.back() == '*' && + skip_path[skip_path.size() - 2] == '/') { + std::string prefix = skip_path.substr(0, skip_path.size() - 1); + if (path.compare(0, prefix.size(), prefix) == 0) return true; + } + } + return false; +} + MHD_Result webserver::requests_answer_first_step(MHD_Connection* connection, struct details::modded_request* mr) { mr->dhr.reset(new http_request(connection, unescaper)); mr->dhr->set_file_cleanup_callback(file_cleanup_callback); @@ -747,6 +762,18 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details } } + // Check centralized authentication if handler is configured + if (found && auth_handler != nullptr) { + std::string path(mr->dhr->get_path()); + if (!should_skip_auth(path)) { + std::shared_ptr auth_response = auth_handler(*mr->dhr); + if (auth_response != nullptr) { + mr->dhrs = auth_response; + found = false; // Skip resource rendering, go directly to response + } + } + } + if (found) { try { if (mr->pp != nullptr) { @@ -775,7 +802,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details } catch(...) { mr->dhrs = internal_error_page(mr); } - } else { + } else if (mr->dhrs == nullptr) { mr->dhrs = not_found_page(mr); } diff --git a/test/integ/authentication.cpp b/test/integ/authentication.cpp index eba87172..05d1f254 100644 --- a/test/integ/authentication.cpp +++ b/test/integ/authentication.cpp @@ -239,6 +239,418 @@ LT_END_AUTO_TEST(digest_auth_wrong_pass) #endif +// Simple resource for centralized auth tests +class simple_resource : public http_resource { + public: + shared_ptr render_GET(const http_request&) { + return std::make_shared("SUCCESS", 200, "text/plain"); + } +}; + +// Centralized authentication handler +std::shared_ptr centralized_auth_handler(const http_request& req) { + if (req.get_user() != "admin" || req.get_pass() != "secret") { + return std::make_shared("Unauthorized", "testrealm"); + } + return nullptr; // Allow request +} + +LT_BEGIN_AUTO_TEST(authentication_suite, centralized_auth_fail) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("protected", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + LT_CHECK_EQ(s, "Unauthorized"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(centralized_auth_fail) + +LT_BEGIN_AUTO_TEST(authentication_suite, centralized_auth_success) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("protected", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + curl_easy_setopt(curl, CURLOPT_USERNAME, "admin"); + curl_easy_setopt(curl, CURLOPT_PASSWORD, "secret"); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(centralized_auth_success) + +LT_BEGIN_AUTO_TEST(authentication_suite, auth_skip_paths) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({"/health", "/public/*"}); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("health", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("public/info", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("protected", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + CURL *curl; + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + std::string s; + + // Test /health (exact match skip path) - should succeed without auth + curl = curl_easy_init(); + s = ""; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/health"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + // Test /public/info (wildcard skip path) - should succeed without auth + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/public/info"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + // Test /protected (not in skip paths) - should fail without auth + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_skip_paths) + +// Test that wildcard doesn't match partial prefix +// /publicinfo should NOT match /public/* (wildcard requires the slash) +LT_BEGIN_AUTO_TEST(authentication_suite, auth_skip_paths_no_partial_match) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({"/public/*"}); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("publicinfo", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // /publicinfo should NOT be skipped (doesn't match /public/*) + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/publicinfo"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); // Should require auth + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_skip_paths_no_partial_match) + +// Test deeply nested wildcard paths +LT_BEGIN_AUTO_TEST(authentication_suite, auth_skip_paths_deep_nested) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({"/api/v1/public/*"}); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("api/v1/public/users/list", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // Deep nested path should be skipped + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/api/v1/public/users/list"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_skip_paths_deep_nested) + +// Test POST method with centralized auth +class post_resource : public http_resource { + public: + shared_ptr render_POST(const http_request&) { + return std::make_shared("POST_SUCCESS", 200, "text/plain"); + } +}; + +LT_BEGIN_AUTO_TEST(authentication_suite, centralized_auth_post_method) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler); + + post_resource pr; + LT_ASSERT_EQ(true, ws.register_resource("data", &pr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // POST without auth should fail + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/data"); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "test=data"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + curl_easy_cleanup(curl); + + // POST with auth should succeed + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_USERNAME, "admin"); + curl_easy_setopt(curl, CURLOPT_PASSWORD, "secret"); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/data"); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "test=data"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "POST_SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(centralized_auth_post_method) + +// Test wrong credentials (different from no credentials) +LT_BEGIN_AUTO_TEST(authentication_suite, centralized_auth_wrong_credentials) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("protected", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // Wrong username + curl_easy_setopt(curl, CURLOPT_USERNAME, "wronguser"); + curl_easy_setopt(curl, CURLOPT_PASSWORD, "secret"); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + curl_easy_cleanup(curl); + + // Wrong password + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_USERNAME, "admin"); + curl_easy_setopt(curl, CURLOPT_PASSWORD, "wrongpass"); + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(centralized_auth_wrong_credentials) + +// Test that 404 is returned for non-existent resources (auth doesn't interfere) +LT_BEGIN_AUTO_TEST(authentication_suite, centralized_auth_not_found) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("exists", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // Non-existent resource without auth - should get 401 (auth checked first) + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/nonexistent"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + // Note: Auth is only checked when resource is found, so 404 should be returned + LT_CHECK_EQ(http_code, 404); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(centralized_auth_not_found) + +// Test no auth handler (default behavior - no auth required) +LT_BEGIN_AUTO_TEST(authentication_suite, no_auth_handler_default) + webserver ws = create_webserver(PORT); // No auth_handler + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("open", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + std::string s; + CURL *curl = curl_easy_init(); + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + + // Should succeed without any auth + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/open"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + LT_CHECK_EQ(s, "SUCCESS"); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(no_auth_handler_default) + +// Test multiple skip paths +LT_BEGIN_AUTO_TEST(authentication_suite, auth_multiple_skip_paths) + webserver ws = create_webserver(PORT) + .auth_handler(centralized_auth_handler) + .auth_skip_paths({"/health", "/metrics", "/status", "/public/*"}); + + simple_resource sr; + LT_ASSERT_EQ(true, ws.register_resource("health", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("metrics", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("status", &sr)); + LT_ASSERT_EQ(true, ws.register_resource("protected", &sr)); + ws.start(false); + + curl_global_init(CURL_GLOBAL_ALL); + CURL *curl; + CURLcode res; + long http_code = 0; // NOLINT(runtime/int) + std::string s; + + // All skip paths should work without auth + const char* skip_urls[] = {"/health", "/metrics", "/status"}; + for (const char* url : skip_urls) { + curl = curl_easy_init(); + s = ""; + http_code = 0; + std::string full_url = std::string("localhost:" PORT_STRING) + url; + curl_easy_setopt(curl, CURLOPT_URL, full_url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 200); + curl_easy_cleanup(curl); + } + + // Protected should still require auth + curl = curl_easy_init(); + s = ""; + http_code = 0; + curl_easy_setopt(curl, CURLOPT_URL, "localhost:" PORT_STRING "/protected"); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writefunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s); + res = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + LT_ASSERT_EQ(res, 0); + LT_CHECK_EQ(http_code, 401); + curl_easy_cleanup(curl); + + ws.stop(); +LT_END_AUTO_TEST(auth_multiple_skip_paths) + LT_BEGIN_AUTO_TEST_ENV() AUTORUN_TESTS() LT_END_AUTO_TEST_ENV()