oatpp-authkit/include/oatpp-authkit/util/TokenExtract.hpp
Uwe Schuster 32356ad226 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>
2026-04-21 21:42:53 +02:00

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