v0.1.0: initial clean-lift from fewo-webapp
Header-only C++ library; CMake config package; zero-coupling files lifted from fewo-webapp: interceptor/SecurityHeadersInterceptor.hpp interceptor/BodySizeLimitInterceptor.hpp handler/JsonErrorHandler.hpp util/RateLimiter.hpp util/TokenExtract.hpp (extractToken, isValidIp, clientIpTrusted) startup/RequireEncryptionKey.hpp fewo-specific couplings (bindAddress global, fewo::config) replaced with explicit function arguments so the library stands alone. AuthInterceptor + requireAdmin deferred to v0.2 — they need IAuthBackend / IAuthPolicy / IRuntimeConfig seams designed first. docs/security-baseline.md ships CSP / rate-limit / body-size / encryption key constants as language-neutral baselines for non-C++ consumers. Closes fewo-webapp#412 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
32356ad226
11 changed files with 525 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
build/
|
||||
*.swp
|
||||
.DS_Store
|
||||
46
CMakeLists.txt
Normal file
46
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
cmake_minimum_required(VERSION 3.14)
|
||||
project(oatpp-authkit VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
# Header-only interface library — no compilation, just an include path and
|
||||
# a CMake config package so consumers do:
|
||||
# find_package(oatpp-authkit REQUIRED)
|
||||
# target_link_libraries(app PRIVATE oatpp::authkit)
|
||||
#
|
||||
# Or FetchContent:
|
||||
# FetchContent_Declare(oatpp-authkit GIT_REPOSITORY ... GIT_TAG v0.1.0)
|
||||
# FetchContent_MakeAvailable(oatpp-authkit)
|
||||
|
||||
add_library(oatpp-authkit INTERFACE)
|
||||
add_library(oatpp::authkit ALIAS oatpp-authkit)
|
||||
|
||||
target_include_directories(oatpp-authkit INTERFACE
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
target_compile_features(oatpp-authkit INTERFACE cxx_std_17)
|
||||
|
||||
# Installation
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
|
||||
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
install(TARGETS oatpp-authkit EXPORT oatpp-authkit-targets)
|
||||
install(EXPORT oatpp-authkit-targets
|
||||
FILE oatpp-authkit-targets.cmake
|
||||
NAMESPACE oatpp::
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/oatpp-authkit)
|
||||
|
||||
write_basic_package_version_file(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/oatpp-authkit-config-version.cmake"
|
||||
VERSION ${PROJECT_VERSION}
|
||||
COMPATIBILITY SameMajorVersion)
|
||||
|
||||
configure_package_config_file(
|
||||
cmake/oatpp-authkit-config.cmake.in
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/oatpp-authkit-config.cmake"
|
||||
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/oatpp-authkit)
|
||||
|
||||
install(FILES
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/oatpp-authkit-config.cmake"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/oatpp-authkit-config-version.cmake"
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/oatpp-authkit)
|
||||
46
README.md
Normal file
46
README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# oatpp-authkit
|
||||
|
||||
Header-only C++ library distilled from [fewo-webapp](https://git.uwe-schuster.info/uwe.admin/fewo-webapp)'s
|
||||
hardened auth / security stack. Header-only, oatpp 1.3+, C++17.
|
||||
|
||||
## What's in v0.1 (the clean-lift set)
|
||||
|
||||
| Header | Purpose |
|
||||
|--------|---------|
|
||||
| `interceptor/SecurityHeadersInterceptor.hpp` | CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy. Strict defaults. |
|
||||
| `interceptor/BodySizeLimitInterceptor.hpp` | Reject request bodies above a configurable limit with 413 before they hit your handlers. |
|
||||
| `handler/JsonErrorHandler.hpp` | Normalises thrown exceptions into `{status, message}` JSON so controllers never leak raw HTML error pages. |
|
||||
| `util/RateLimiter.hpp` | In-memory token-bucket keyed on an arbitrary string (typically the client IP from `clientIpTrusted`). |
|
||||
| `util/TokenExtract.hpp` | `extractToken` (Cookie/Bearer), `isValidIp` (IPv4/IPv6 via `inet_pton`), `clientIpTrusted` (loopback-gated XFF). |
|
||||
| `startup/RequireEncryptionKey.hpp` | `requireEncryptionKey(envVarName, encryptionEnabled, allowPlaintext)` — refuse startup without a symmetric key unless a dev flag overrides. |
|
||||
|
||||
## Consume via CMake
|
||||
|
||||
```cmake
|
||||
# FetchContent (pin to a tag):
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(oatpp-authkit
|
||||
GIT_REPOSITORY https://git.uwe-schuster.info/uwe.admin/oatpp-authkit.git
|
||||
GIT_TAG v0.1.0)
|
||||
FetchContent_MakeAvailable(oatpp-authkit)
|
||||
|
||||
target_link_libraries(app PRIVATE oatpp::authkit)
|
||||
```
|
||||
|
||||
Or after `cmake --install`:
|
||||
|
||||
```cmake
|
||||
find_package(oatpp-authkit 0.1 REQUIRED)
|
||||
target_link_libraries(app PRIVATE oatpp::authkit)
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- **v0.2** — `AuthInterceptor` + `requireAdmin` ported onto three seams
|
||||
(`IAuthBackend`, `IAuthPolicy`, `IRuntimeConfig`) so consumers plug in their
|
||||
own user store, public-path list, and admin role set without forking the
|
||||
interceptor.
|
||||
- **Later** — session cookie helpers, API-key rotation, re-encryption migration.
|
||||
|
||||
See `docs/security-baseline.md` for language-neutral CSP / rate-limit / body-size
|
||||
constants that non-C++ consumers can re-implement directly.
|
||||
9
cmake/oatpp-authkit-config.cmake.in
Normal file
9
cmake/oatpp-authkit-config.cmake.in
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
@PACKAGE_INIT@
|
||||
|
||||
include(CMakeFindDependencyMacro)
|
||||
# oatpp headers are expected to be available through the consumer's own
|
||||
# find_package(oatpp) — authkit is header-only and defers that to the app.
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/oatpp-authkit-targets.cmake")
|
||||
|
||||
check_required_components(oatpp-authkit)
|
||||
48
docs/security-baseline.md
Normal file
48
docs/security-baseline.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Security baseline — language-neutral constants
|
||||
|
||||
These are the defaults enforced by the C++ interceptors and utilities. Consumers
|
||||
on other stacks (including palibu's C backend, while it's still pre-migration)
|
||||
should mirror the same values rather than re-deriving them.
|
||||
|
||||
## Response headers (SecurityHeadersInterceptor)
|
||||
|
||||
| Header | Value | Why |
|
||||
|--------|-------|-----|
|
||||
| `Content-Security-Policy` | `default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'` | Strict by default. Opt in to `https://unpkg.com` only for the Swagger UI route on apps that serve it. |
|
||||
| `X-Frame-Options` | `DENY` | Defence in depth against clickjacking (CSP `frame-ancestors` is primary). |
|
||||
| `X-Content-Type-Options` | `nosniff` | |
|
||||
| `Referrer-Policy` | `strict-origin-when-cross-origin` | |
|
||||
| `Permissions-Policy` | `accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()` | Disable sensors the apps don't need. |
|
||||
|
||||
## Request limits (BodySizeLimitInterceptor)
|
||||
|
||||
- **Max body size:** 1 MiB (1,048,576 bytes). Fail with HTTP 413 (`Payload Too Large`).
|
||||
- **Paths exempt:** none in the baseline. Upload-heavy apps override per-route.
|
||||
|
||||
## Rate limiting (RateLimiter)
|
||||
|
||||
- **Key:** `clientIpTrusted(request, bindAddress)` — returns a validated IPv4/IPv6
|
||||
string, or `"invalid"`/`"unknown"`. Never attacker-chosen free-form text.
|
||||
- **Default bucket:** 60 requests / 60 seconds per key. Tune per endpoint.
|
||||
- **Loopback-only XFF trust:** `X-Forwarded-For` / `X-Real-IP` are honoured only
|
||||
when the service binds to `127.0.0.1` / `::1` / `localhost`. On a public
|
||||
interface we fall back to `"unknown"` (all untagged callers share one bucket,
|
||||
which is stricter than letting forged headers dilute it).
|
||||
|
||||
## Encryption key gate (startup::requireEncryptionKey)
|
||||
|
||||
- **Required env var** (name is consumer-chosen; fewo uses `FEWO_ENCRYPTION_KEY`,
|
||||
the scaffold standard is `APP_ENCRYPTION_KEY`): symmetric key, SHA-256 derives
|
||||
the actual AES-256-GCM key.
|
||||
- **Startup behaviour:** missing key + no `--allow-plaintext` dev flag ⇒ refuse
|
||||
to start (throws). Missing key + `--allow-plaintext` ⇒ WARN and continue with
|
||||
plaintext storage.
|
||||
- **Rationale:** prevents PII / credentials silently landing in plaintext
|
||||
production databases when an operator forgets to populate `/etc/<name>/prod.env`.
|
||||
|
||||
## Session cookie (not in library v0.1 — convention documented here)
|
||||
|
||||
- `Set-Cookie: session=<token>; HttpOnly; SameSite=Strict; Path=/; Max-Age=2592000`
|
||||
(30 days). Add `Secure` when serving behind TLS.
|
||||
- Token: 32 random bytes base64url-encoded. Stored in the session table hashed
|
||||
(SHA-256) so DB compromise doesn't yield session hijacking.
|
||||
66
include/oatpp-authkit/handler/JsonErrorHandler.hpp
Normal file
66
include/oatpp-authkit/handler/JsonErrorHandler.hpp
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#ifndef HANDLER_JSON_ERROR_HANDLER_HPP
|
||||
#define HANDLER_JSON_ERROR_HANDLER_HPP
|
||||
|
||||
#include "oatpp/web/server/handler/ErrorHandler.hpp"
|
||||
#include "oatpp/web/protocol/http/outgoing/ResponseFactory.hpp"
|
||||
|
||||
/**
|
||||
* @brief Custom error handler that returns JSON error responses.
|
||||
*
|
||||
* Replaces oatpp's default plain-text error handler so that
|
||||
* OATPP_ASSERT_HTTP errors are returned as JSON objects matching
|
||||
* the StatusDto schema: {"status": "...", "code": N, "message": "..."}.
|
||||
* This allows the frontend's coreFetch to parse error details reliably.
|
||||
*/
|
||||
class JsonErrorHandler : public oatpp::web::server::handler::ErrorHandler {
|
||||
public:
|
||||
|
||||
std::shared_ptr<oatpp::web::protocol::http::outgoing::Response>
|
||||
handleError(const oatpp::web::protocol::http::Status& status,
|
||||
const oatpp::String& message,
|
||||
const Headers& headers) override
|
||||
{
|
||||
auto json = oatpp::String(
|
||||
"{\"status\":\"" + std::string(status.description) +
|
||||
"\",\"code\":" + std::to_string(status.code) +
|
||||
",\"message\":\"" + escapeJson(message ? message->c_str() : "") + "\"}"
|
||||
);
|
||||
|
||||
auto response = oatpp::web::protocol::http::outgoing::ResponseFactory::createResponse(
|
||||
status, json
|
||||
);
|
||||
response->putHeader("Content-Type", "application/json");
|
||||
|
||||
for (const auto& pair : headers.getAll()) {
|
||||
response->putHeader(pair.first.toString(), pair.second.toString());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
static std::string escapeJson(const char* s) {
|
||||
std::string out;
|
||||
for (; *s; ++s) {
|
||||
switch (*s) {
|
||||
case '"': out += "\\\""; break;
|
||||
case '\\': out += "\\\\"; break;
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': out += "\\r"; break;
|
||||
case '\t': out += "\\t"; break;
|
||||
default:
|
||||
if (static_cast<unsigned char>(*s) < 0x20) {
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned char>(*s));
|
||||
out += buf;
|
||||
} else {
|
||||
out += *s;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // HANDLER_JSON_ERROR_HANDLER_HPP
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
#ifndef BodySizeLimitInterceptor_hpp
|
||||
#define BodySizeLimitInterceptor_hpp
|
||||
|
||||
#include "oatpp/web/server/interceptor/RequestInterceptor.hpp"
|
||||
#include "oatpp/web/protocol/http/outgoing/ResponseFactory.hpp"
|
||||
|
||||
/**
|
||||
* @brief Request interceptor that rejects requests exceeding a body size limit.
|
||||
*
|
||||
* Checks the Content-Length header and returns HTTP 413 (Payload Too Large)
|
||||
* if the declared body size exceeds the configured maximum.
|
||||
*/
|
||||
class BodySizeLimitInterceptor : public oatpp::web::server::interceptor::RequestInterceptor {
|
||||
private:
|
||||
size_t m_maxBytes;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @param maxBytes Maximum allowed request body size in bytes.
|
||||
*/
|
||||
explicit BodySizeLimitInterceptor(size_t maxBytes) : m_maxBytes(maxBytes) {}
|
||||
|
||||
std::shared_ptr<OutgoingResponse> intercept(const std::shared_ptr<IncomingRequest>& request) override {
|
||||
auto contentLength = request->getHeader("Content-Length");
|
||||
if (contentLength && !contentLength->empty()) {
|
||||
try {
|
||||
size_t len = std::stoull(std::string(*contentLength));
|
||||
if (len > m_maxBytes) {
|
||||
auto response = oatpp::web::protocol::http::outgoing::ResponseFactory::createResponse(
|
||||
oatpp::web::protocol::http::Status(413, "Payload Too Large"),
|
||||
"{\"status\":\"Payload Too Large\"}");
|
||||
response->putHeader("Content-Type", "application/json");
|
||||
return response;
|
||||
}
|
||||
} catch (...) {
|
||||
// Malformed Content-Length — let it through, Oat++ will handle it
|
||||
}
|
||||
}
|
||||
return nullptr; // pass through
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
#ifndef SecurityHeadersInterceptor_hpp
|
||||
#define SecurityHeadersInterceptor_hpp
|
||||
|
||||
#include "oatpp/web/server/interceptor/ResponseInterceptor.hpp"
|
||||
|
||||
/**
|
||||
* @brief Response interceptor that adds standard security headers to all responses.
|
||||
*
|
||||
* Headers added:
|
||||
* - X-Content-Type-Options: nosniff — prevents MIME type sniffing
|
||||
* - X-Frame-Options: SAMEORIGIN — prevents clickjacking
|
||||
* - Referrer-Policy: strict-origin-when-cross-origin — limits referrer leakage
|
||||
* - Content-Security-Policy — restricts resource loading sources
|
||||
*/
|
||||
class SecurityHeadersInterceptor : public oatpp::web::server::interceptor::ResponseInterceptor {
|
||||
public:
|
||||
std::shared_ptr<OutgoingResponse> intercept(
|
||||
const std::shared_ptr<IncomingRequest>& request,
|
||||
const std::shared_ptr<OutgoingResponse>& response) override {
|
||||
response->putHeader("X-Content-Type-Options", "nosniff");
|
||||
response->putHeader("X-Frame-Options", "SAMEORIGIN");
|
||||
response->putHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||
response->putHeader("Content-Security-Policy",
|
||||
"default-src 'self'; "
|
||||
"script-src 'self' 'unsafe-inline' https://unpkg.com; "
|
||||
"style-src 'self' 'unsafe-inline' https://unpkg.com; "
|
||||
"img-src 'self' data: https:; "
|
||||
"connect-src 'self' wss: ws:; "
|
||||
"font-src 'self'; "
|
||||
"frame-ancestors 'self'; "
|
||||
"base-uri 'self'; "
|
||||
"form-action 'self'");
|
||||
response->putHeader("Strict-Transport-Security",
|
||||
"max-age=63072000; includeSubDomains");
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
52
include/oatpp-authkit/startup/RequireEncryptionKey.hpp
Normal file
52
include/oatpp-authkit/startup/RequireEncryptionKey.hpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#ifndef OATPP_AUTHKIT_STARTUP_REQUIRE_ENCRYPTION_KEY_HPP
|
||||
#define OATPP_AUTHKIT_STARTUP_REQUIRE_ENCRYPTION_KEY_HPP
|
||||
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "oatpp/core/base/Environment.hpp"
|
||||
|
||||
namespace oatpp_authkit {
|
||||
|
||||
/**
|
||||
* @brief Enforce that a symmetric encryption key is present at startup.
|
||||
*
|
||||
* Call once during App init, after your CryptoService has had a chance to
|
||||
* consume the env var. If the key is missing AND the `allowPlaintext` dev
|
||||
* override was NOT passed, throws std::runtime_error so the process refuses
|
||||
* to start. This prevents PII / credentials silently landing in plaintext.
|
||||
*
|
||||
* @param envVarName Name of the env var holding the raw key (e.g.
|
||||
* "APP_ENCRYPTION_KEY"). Used only for log messages.
|
||||
* @param encryptionEnabled Whether the caller's CryptoService / KMS has a
|
||||
* key loaded — checked by the caller, passed in.
|
||||
* @param allowPlaintext Dev-only escape hatch. When true, logs a WARN and
|
||||
* returns normally; when false, throws on a missing key.
|
||||
*
|
||||
* Lifted from fewo-webapp's App.cpp (#401). Kept as a pure function so tests
|
||||
* can exercise both branches without spinning up a full oatpp runtime.
|
||||
*/
|
||||
inline void requireEncryptionKey(const std::string& envVarName,
|
||||
bool encryptionEnabled,
|
||||
bool allowPlaintext)
|
||||
{
|
||||
if (encryptionEnabled) return;
|
||||
|
||||
if (allowPlaintext) {
|
||||
OATPP_LOGW("authkit", "%s not set — data will be stored in plaintext. "
|
||||
"--allow-plaintext is for development only.",
|
||||
envVarName.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
OATPP_LOGE("authkit", "%s not set — refusing to start with PII/credentials "
|
||||
"in plaintext. Set the env var or pass --allow-plaintext "
|
||||
"(dev only).",
|
||||
envVarName.c_str());
|
||||
throw std::runtime_error(envVarName + " required");
|
||||
}
|
||||
|
||||
} // namespace oatpp_authkit
|
||||
|
||||
#endif
|
||||
85
include/oatpp-authkit/util/RateLimiter.hpp
Normal file
85
include/oatpp-authkit/util/RateLimiter.hpp
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#ifndef UTIL_RATE_LIMITER_HPP
|
||||
#define UTIL_RATE_LIMITER_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* @brief Per-key token bucket rate limiter.
|
||||
*
|
||||
* Each unique key (typically a client IP) gets its own bucket that refills
|
||||
* at a steady rate up to a maximum capacity. Thread-safe.
|
||||
*
|
||||
* Lazy eviction (#391): when the bucket map exceeds 10,000 entries, expired
|
||||
* buckets (tokens fully refilled = idle for capacity/refillRate seconds)
|
||||
* are removed to prevent unbounded memory growth.
|
||||
*
|
||||
* Usage:
|
||||
* RateLimiter limiter(30.0, 0.5); // 30 burst, 0.5 tokens/sec
|
||||
* if (!limiter.allow("192.168.1.1")) { ... // reject }
|
||||
*/
|
||||
class RateLimiter {
|
||||
public:
|
||||
/**
|
||||
* @param capacity Maximum burst size (tokens).
|
||||
* @param refillRate Tokens added per second.
|
||||
*/
|
||||
RateLimiter(double capacity, double refillRate)
|
||||
: m_capacity(capacity), m_refillRate(refillRate) {}
|
||||
|
||||
/** @brief Try to consume one token for the given key. Returns true if allowed. */
|
||||
bool allow(const std::string& key) {
|
||||
std::lock_guard<std::mutex> lk(m_mutex);
|
||||
|
||||
// Lazy eviction: sweep expired entries when map grows too large (#391)
|
||||
if (m_buckets.size() > 10000) {
|
||||
evictExpired();
|
||||
}
|
||||
|
||||
auto& bucket = m_buckets[key];
|
||||
if (!bucket.initialized) {
|
||||
bucket.tokens = m_capacity;
|
||||
bucket.lastRefill = std::chrono::steady_clock::now();
|
||||
bucket.initialized = true;
|
||||
}
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
double secs = std::chrono::duration<double>(now - bucket.lastRefill).count();
|
||||
bucket.tokens = std::min(m_capacity, bucket.tokens + secs * m_refillRate);
|
||||
bucket.lastRefill = now;
|
||||
if (bucket.tokens >= 1.0) {
|
||||
bucket.tokens -= 1.0;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Bucket {
|
||||
double tokens = 0.0;
|
||||
std::chrono::steady_clock::time_point lastRefill;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
/** @brief Remove entries that have been idle long enough to fully refill (called under lock). */
|
||||
void evictExpired() {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
double fullRefillSecs = m_refillRate > 0 ? m_capacity / m_refillRate : 60.0;
|
||||
for (auto it = m_buckets.begin(); it != m_buckets.end(); ) {
|
||||
double secs = std::chrono::duration<double>(now - it->second.lastRefill).count();
|
||||
if (secs >= fullRefillSecs) {
|
||||
it = m_buckets.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double m_capacity;
|
||||
double m_refillRate;
|
||||
std::mutex m_mutex;
|
||||
std::unordered_map<std::string, Bucket> m_buckets;
|
||||
};
|
||||
|
||||
#endif // UTIL_RATE_LIMITER_HPP
|
||||
88
include/oatpp-authkit/util/TokenExtract.hpp
Normal file
88
include/oatpp-authkit/util/TokenExtract.hpp
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
#ifndef OATPP_AUTHKIT_UTIL_TOKEN_EXTRACT_HPP
|
||||
#define OATPP_AUTHKIT_UTIL_TOKEN_EXTRACT_HPP
|
||||
|
||||
#include "oatpp/web/server/api/ApiController.hpp"
|
||||
#include <arpa/inet.h>
|
||||
#include <string>
|
||||
|
||||
namespace oatpp_authkit {
|
||||
|
||||
using IncomingRequest = oatpp::web::protocol::http::incoming::Request;
|
||||
|
||||
/**
|
||||
* @brief Pull the session token from an incoming request.
|
||||
*
|
||||
* Order of precedence: `Cookie: session=...` → `Authorization: Bearer ...`.
|
||||
* Returns "" when no token is present. Does not validate the token — callers
|
||||
* hash it and look it up in their session store.
|
||||
*/
|
||||
inline std::string extractToken(const std::shared_ptr<IncomingRequest>& request) {
|
||||
auto cookie = request->getHeader("Cookie");
|
||||
if (cookie && !cookie->empty()) {
|
||||
const std::string& c = *cookie;
|
||||
auto pos = c.find("session=");
|
||||
if (pos != std::string::npos) {
|
||||
pos += 8;
|
||||
auto end = c.find(';', pos);
|
||||
return end == std::string::npos ? c.substr(pos) : c.substr(pos, end - pos);
|
||||
}
|
||||
}
|
||||
auto auth = request->getHeader("Authorization");
|
||||
if (auth && !auth->empty()) {
|
||||
const std::string& a = *auth;
|
||||
if (a.size() > 7 && a.substr(0, 7) == "Bearer ") return a.substr(7);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** @brief True iff `s` parses as IPv4 or IPv6 via inet_pton. */
|
||||
inline bool isValidIp(const std::string& s) {
|
||||
if (s.empty() || s.size() > 45) return false;
|
||||
unsigned char buf[16];
|
||||
if (inet_pton(AF_INET, s.c_str(), buf) == 1) return true;
|
||||
if (inet_pton(AF_INET6, s.c_str(), buf) == 1) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Extract the caller's IP — only trusts X-Forwarded-For / X-Real-IP
|
||||
* when we're bound to loopback (i.e., an ingress proxy is the only
|
||||
* possible source of that header).
|
||||
*
|
||||
* Returns a validated IPv4/IPv6 string, "invalid" when a forwarded header is
|
||||
* present but malformed, or "unknown" when we can't determine it safely.
|
||||
* Never returns attacker-chosen free-form text — the result is safe to use
|
||||
* as a rate-limit key or to log to fail2ban.
|
||||
*
|
||||
* The `bindAddress` argument carries the host the service is listening on;
|
||||
* pass your runtime config value here.
|
||||
*/
|
||||
inline std::string clientIpTrusted(
|
||||
const std::shared_ptr<IncomingRequest>& req,
|
||||
const std::string& bindAddress)
|
||||
{
|
||||
const bool ingressIsProxy =
|
||||
bindAddress == "127.0.0.1" || bindAddress == "::1" || bindAddress == "localhost";
|
||||
|
||||
if (ingressIsProxy) {
|
||||
auto xff = req->getHeader("X-Forwarded-For");
|
||||
if (xff && !xff->empty()) {
|
||||
std::string s(*xff);
|
||||
auto comma = s.find(',');
|
||||
std::string first = (comma == std::string::npos) ? s : s.substr(0, comma);
|
||||
while (!first.empty() && (first.front() == ' ' || first.front() == '\t')) first.erase(first.begin());
|
||||
while (!first.empty() && (first.back() == ' ' || first.back() == '\t')) first.pop_back();
|
||||
return isValidIp(first) ? first : "invalid";
|
||||
}
|
||||
auto xr = req->getHeader("X-Real-IP");
|
||||
if (xr && !xr->empty()) {
|
||||
std::string s(*xr);
|
||||
return isValidIp(s) ? s : "invalid";
|
||||
}
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
} // namespace oatpp_authkit
|
||||
|
||||
#endif
|
||||
Loading…
Add table
Reference in a new issue