Skip to content

Commit c87865d

Browse files
committed
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
1 parent 76afe97 commit c87865d

File tree

6 files changed

+622
-2
lines changed

6 files changed

+622
-2
lines changed

README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ libhttpserver is built upon [libmicrohttpd](https://www.gnu.org/software/libmic
3131
- Support for SHOUTcast
3232
- Support for incremental processing of POST data (optional)
3333
- Support for basic and digest authentication (optional)
34+
- Support for centralized authentication with path-based skip rules
3435
- Support for TLS (requires libgnutls, optional)
3536

3637
## Table of Contents
@@ -990,6 +991,80 @@ You will receive a `SUCCESS` in response (observe the response message from the
990991

991992
You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/digest_authentication.cpp).
992993

994+
### Using Centralized Authentication
995+
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.
996+
997+
libhttpserver provides a centralized authentication mechanism that runs a single authentication handler before any resource's render method is called. This allows you to:
998+
- Define authentication logic once for all resources
999+
- Automatically protect all endpoints by default
1000+
- Specify paths that should bypass authentication (e.g., health checks, public APIs)
1001+
1002+
```cpp
1003+
#include <httpserver.hpp>
1004+
1005+
using namespace httpserver;
1006+
1007+
// Resources no longer need authentication logic
1008+
class hello_resource : public http_resource {
1009+
public:
1010+
std::shared_ptr<http_response> render_GET(const http_request&) {
1011+
return std::make_shared<string_response>("Hello, authenticated user!", 200, "text/plain");
1012+
}
1013+
};
1014+
1015+
class health_resource : public http_resource {
1016+
public:
1017+
std::shared_ptr<http_response> render_GET(const http_request&) {
1018+
return std::make_shared<string_response>("OK", 200, "text/plain");
1019+
}
1020+
};
1021+
1022+
// Centralized authentication handler
1023+
// Return nullptr to allow the request, or an http_response to reject it
1024+
std::shared_ptr<http_response> my_auth_handler(const http_request& req) {
1025+
if (req.get_user() != "admin" || req.get_pass() != "secret") {
1026+
return std::make_shared<basic_auth_fail_response>("Unauthorized", "MyRealm");
1027+
}
1028+
return nullptr; // Allow request to proceed to resource
1029+
}
1030+
1031+
int main() {
1032+
webserver ws = create_webserver(8080)
1033+
.auth_handler(my_auth_handler)
1034+
.auth_skip_paths({"/health", "/public/*"});
1035+
1036+
hello_resource hello;
1037+
health_resource health;
1038+
1039+
ws.register_resource("/api", &hello);
1040+
ws.register_resource("/health", &health);
1041+
1042+
ws.start(true);
1043+
return 0;
1044+
}
1045+
```
1046+
1047+
The `auth_handler` callback is called for every request before the resource's render method. It receives the `http_request` and can:
1048+
- Return `nullptr` to allow the request to proceed normally
1049+
- Return an `http_response` (e.g., `basic_auth_fail_response` or `digest_auth_fail_response`) to reject the request
1050+
1051+
The `auth_skip_paths` method accepts a vector of paths that should bypass authentication:
1052+
- Exact matches: `"/health"` matches only `/health`
1053+
- Wildcard suffixes: `"/public/*"` matches `/public/`, `/public/info`, `/public/docs/api`, etc.
1054+
1055+
To test the above example:
1056+
1057+
# Without auth - returns 401 Unauthorized
1058+
curl -v http://localhost:8080/api
1059+
1060+
# With valid auth - returns 200 OK
1061+
curl -u admin:secret http://localhost:8080/api
1062+
1063+
# Health endpoint (skip path) - works without auth
1064+
curl http://localhost:8080/health
1065+
1066+
You can also check this example on [github](https://github.com/etr/libhttpserver/blob/master/examples/centralized_authentication.cpp).
1067+
9931068
[Back to TOC](#table-of-contents)
9941069
9951070
## HTTP Utils
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011, 2012, 2013, 2014, 2015 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
#include <memory>
22+
#include <string>
23+
24+
#include <httpserver.hpp>
25+
26+
using httpserver::http_request;
27+
using httpserver::http_response;
28+
using httpserver::http_resource;
29+
using httpserver::webserver;
30+
using httpserver::create_webserver;
31+
using httpserver::string_response;
32+
using httpserver::basic_auth_fail_response;
33+
34+
// Simple resource that doesn't need to handle auth itself
35+
class hello_resource : public http_resource {
36+
public:
37+
std::shared_ptr<http_response> render_GET(const http_request&) {
38+
return std::make_shared<string_response>("Hello, authenticated user!", 200, "text/plain");
39+
}
40+
};
41+
42+
class health_resource : public http_resource {
43+
public:
44+
std::shared_ptr<http_response> render_GET(const http_request&) {
45+
return std::make_shared<string_response>("OK", 200, "text/plain");
46+
}
47+
};
48+
49+
// Centralized authentication handler
50+
// Returns nullptr to allow the request, or an http_response to reject it
51+
std::shared_ptr<http_response> auth_handler(const http_request& req) {
52+
if (req.get_user() != "admin" || req.get_pass() != "secret") {
53+
return std::make_shared<basic_auth_fail_response>("Unauthorized", "MyRealm");
54+
}
55+
return nullptr; // Allow request
56+
}
57+
58+
int main() {
59+
// Create webserver with centralized authentication
60+
// - auth_handler: called before every resource's render method
61+
// - auth_skip_paths: paths that bypass authentication
62+
webserver ws = create_webserver(8080)
63+
.auth_handler(auth_handler)
64+
.auth_skip_paths({"/health", "/public/*"});
65+
66+
hello_resource hello;
67+
health_resource health;
68+
69+
ws.register_resource("/api", &hello);
70+
ws.register_resource("/health", &health);
71+
72+
ws.start(true);
73+
74+
return 0;
75+
}
76+
77+
// Usage:
78+
// # Start the server
79+
// ./centralized_authentication
80+
//
81+
// # Without auth - should get 401 Unauthorized
82+
// curl -v http://localhost:8080/api
83+
//
84+
// # With valid auth - should get 200 OK
85+
// curl -u admin:secret http://localhost:8080/api
86+
//
87+
// # Health endpoint (skip path) - works without auth
88+
// curl http://localhost:8080/health

src/httpserver/create_webserver.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <limits>
3232
#include <string>
3333
#include <variant>
34+
#include <vector>
3435

3536
#include "httpserver/http_response.hpp"
3637
#include "httpserver/http_utils.hpp"
@@ -52,6 +53,7 @@ typedef std::function<std::string(const std::string&)> psk_cred_handler_callback
5253
namespace http { class file_info; }
5354

5455
typedef std::function<bool(const std::string&, const std::string&, const http::file_info&)> file_cleanup_callback_ptr;
56+
typedef std::function<std::shared_ptr<http_response>(const http_request&)> auth_handler_ptr;
5557

5658
class create_webserver {
5759
public:
@@ -376,6 +378,16 @@ class create_webserver {
376378
return *this;
377379
}
378380

381+
create_webserver& auth_handler(auth_handler_ptr handler) {
382+
_auth_handler = handler;
383+
return *this;
384+
}
385+
386+
create_webserver& auth_skip_paths(const std::vector<std::string>& paths) {
387+
_auth_skip_paths = paths;
388+
return *this;
389+
}
390+
379391
private:
380392
uint16_t _port = DEFAULT_WS_PORT;
381393
http::http_utils::start_method_T _start_method = http::http_utils::INTERNAL_SELECT;
@@ -423,6 +435,8 @@ class create_webserver {
423435
render_ptr _method_not_allowed_resource = nullptr;
424436
render_ptr _internal_error_resource = nullptr;
425437
file_cleanup_callback_ptr _file_cleanup_callback = nullptr;
438+
auth_handler_ptr _auth_handler = nullptr;
439+
std::vector<std::string> _auth_skip_paths;
426440

427441
friend class webserver;
428442
};

src/httpserver/webserver.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include <set>
4646
#include <shared_mutex>
4747
#include <string>
48+
#include <vector>
4849

4950
#ifdef HAVE_GNUTLS
5051
#include <gnutls/gnutls.h>
@@ -182,6 +183,8 @@ class webserver {
182183
const render_ptr method_not_allowed_resource;
183184
const render_ptr internal_error_resource;
184185
const file_cleanup_callback_ptr file_cleanup_callback;
186+
const auth_handler_ptr auth_handler;
187+
const std::vector<std::string> auth_skip_paths;
185188
std::shared_mutex registered_resources_mutex;
186189
std::map<details::http_endpoint, http_resource*> registered_resources;
187190
std::map<std::string, http_resource*> registered_resources_str;
@@ -197,6 +200,7 @@ class webserver {
197200
std::shared_ptr<http_response> method_not_allowed_page(details::modded_request* mr) const;
198201
std::shared_ptr<http_response> internal_error_page(details::modded_request* mr, bool force_our = false) const;
199202
std::shared_ptr<http_response> not_found_page(details::modded_request* mr) const;
203+
bool should_skip_auth(const std::string& path) const;
200204

201205
static void request_completed(void *cls,
202206
struct MHD_Connection *connection, void **con_cls,

src/webserver.cpp

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,9 @@ webserver::webserver(const create_webserver& params):
167167
not_found_resource(params._not_found_resource),
168168
method_not_allowed_resource(params._method_not_allowed_resource),
169169
internal_error_resource(params._internal_error_resource),
170-
file_cleanup_callback(params._file_cleanup_callback) {
170+
file_cleanup_callback(params._file_cleanup_callback),
171+
auth_handler(params._auth_handler),
172+
auth_skip_paths(params._auth_skip_paths) {
171173
ignore_sigpipe();
172174
pthread_mutex_init(&mutexwait, nullptr);
173175
pthread_cond_init(&mutexcond, nullptr);
@@ -635,6 +637,19 @@ std::shared_ptr<http_response> webserver::internal_error_page(details::modded_re
635637
}
636638
}
637639

640+
bool webserver::should_skip_auth(const std::string& path) const {
641+
for (const auto& skip_path : auth_skip_paths) {
642+
if (skip_path == path) return true;
643+
// Support wildcard suffix (e.g., "/public/*")
644+
if (skip_path.size() > 2 && skip_path.back() == '*' &&
645+
skip_path[skip_path.size() - 2] == '/') {
646+
std::string prefix = skip_path.substr(0, skip_path.size() - 1);
647+
if (path.compare(0, prefix.size(), prefix) == 0) return true;
648+
}
649+
}
650+
return false;
651+
}
652+
638653
MHD_Result webserver::requests_answer_first_step(MHD_Connection* connection, struct details::modded_request* mr) {
639654
mr->dhr.reset(new http_request(connection, unescaper));
640655
mr->dhr->set_file_cleanup_callback(file_cleanup_callback);
@@ -747,6 +762,18 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details
747762
}
748763
}
749764

765+
// Check centralized authentication if handler is configured
766+
if (found && auth_handler != nullptr) {
767+
std::string path(mr->dhr->get_path());
768+
if (!should_skip_auth(path)) {
769+
std::shared_ptr<http_response> auth_response = auth_handler(*mr->dhr);
770+
if (auth_response != nullptr) {
771+
mr->dhrs = auth_response;
772+
found = false; // Skip resource rendering, go directly to response
773+
}
774+
}
775+
}
776+
750777
if (found) {
751778
try {
752779
if (mr->pp != nullptr) {
@@ -775,7 +802,7 @@ MHD_Result webserver::finalize_answer(MHD_Connection* connection, struct details
775802
} catch(...) {
776803
mr->dhrs = internal_error_page(mr);
777804
}
778-
} else {
805+
} else if (mr->dhrs == nullptr) {
779806
mr->dhrs = not_found_page(mr);
780807
}
781808

0 commit comments

Comments
 (0)