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>
88 lines
3.1 KiB
C++
88 lines
3.1 KiB
C++
#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
|