#5: add IRuntimeConfig::certAuthTrusted() — gate X-SSL-Client-DN trust

New virtual hook on IRuntimeConfig, defaulting to isLoopback() so existing
consumers keep their current behaviour. AuthInterceptor now consults
certAuthTrusted() (instead of isLoopback() directly) to decide whether to
honour an inbound X-SSL-Client-DN header.

Operators with an SSH tunnel to a loopback bind, or a non-TLS proxy that
forwards X-SSL-Client-DN from untrusted clients, can now override the
hook to require additional gating (e.g. an env var, a TLS-only port).

Bump to 0.3.5 (additive — no consumer break).

Closes #5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Uwe Schuster 2026-04-25 21:39:57 +02:00
parent 950012d946
commit bccd57f47e
3 changed files with 29 additions and 5 deletions

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.14) cmake_minimum_required(VERSION 3.14)
project(oatpp-authkit VERSION 0.3.4 LANGUAGES CXX) project(oatpp-authkit VERSION 0.3.5 LANGUAGES CXX)
# Header-only interface library — no compilation, just an include path and # Header-only interface library — no compilation, just an include path and
# a CMake config package so consumers do: # a CMake config package so consumers do:

View file

@ -27,7 +27,7 @@ using TokenHasher = std::function<std::string(const std::string&)>;
* Order of checks: * Order of checks:
* 1. Public path pass. * 1. Public path pass.
* 2. Setup mode (empty users table + policy->setupModeActive()) pseudo-admin. * 2. Setup mode (empty users table + policy->setupModeActive()) pseudo-admin.
* 3. X-SSL-Client-DN header (only trusted when bound to loopback) cert auth. * 3. X-SSL-Client-DN header (only trusted when `IRuntimeConfig::certAuthTrusted()`) cert auth.
* 4. Session cookie / Bearer token backend->resolveBySessionHash / resolveByApiKeyHash. * 4. Session cookie / Bearer token backend->resolveBySessionHash / resolveByApiKeyHash.
* 5. CSRF defence: sessions reject state-changing requests without X-Requested-With. * 5. CSRF defence: sessions reject state-changing requests without X-Requested-With.
* 6. Readonly roles cannot mutate. * 6. Readonly roles cannot mutate.
@ -208,9 +208,11 @@ public:
return nullptr; return nullptr;
} }
// TLS cert DN — only trusted when we're behind a reverse proxy (loopback). // TLS cert DN — only trusted when the runtime hook says so (#5).
// `certAuthTrusted()` defaults to `isLoopback()`; consumers can override
// it to gate more strictly (e.g. require an env-var or a TLS-only port).
auto certDnH = request->getHeader("X-SSL-Client-DN"); auto certDnH = request->getHeader("X-SSL-Client-DN");
if (m_runtime->isLoopback() && certDnH && !certDnH->empty()) { if (m_runtime->certAuthTrusted() && certDnH && !certDnH->empty()) {
if (auto p = m_backend->resolveByCertDn(std::string(*certDnH))) { if (auto p = m_backend->resolveByCertDn(std::string(*certDnH))) {
writeBundle(request, *p); writeBundle(request, *p);
if (isReadonly(p->role) && isMutation(method)) { if (isReadonly(p->role) && isMutation(method)) {

View file

@ -20,11 +20,33 @@ public:
/** @brief Host the service is bound to ("127.0.0.1", "::1", "0.0.0.0", ...). */ /** @brief Host the service is bound to ("127.0.0.1", "::1", "0.0.0.0", ...). */
virtual std::string bindAddress() = 0; virtual std::string bindAddress() = 0;
/** @brief Convenience: true iff `bindAddress()` is a loopback literal. */ /** @brief Convenience: true iff `bindAddress()` is a loopback literal.
*
* Used as the *binding* gate (e.g. trusting `X-Forwarded-For` / `X-Real-IP`).
* For cert-DN trust, prefer `certAuthTrusted()` operators with an SSH tunnel
* or a misconfigured proxy can forward `X-SSL-Client-DN` from untrusted clients
* even when the service binds to loopback.
*/
virtual bool isLoopback() { virtual bool isLoopback() {
const std::string a = bindAddress(); const std::string a = bindAddress();
return a == "127.0.0.1" || a == "::1" || a == "localhost"; return a == "127.0.0.1" || a == "::1" || a == "localhost";
} }
/** @brief Whether incoming `X-SSL-Client-DN` headers should be trusted (#5).
*
* Default: `isLoopback()` preserves the legacy behaviour for consumers
* that haven't overridden anything. Override to gate more strictly, e.g.:
*
* bool certAuthTrusted() override {
* return isLoopback() && std::getenv("TRUST_CERT_DN") != nullptr;
* }
*
* When this returns `false`, `AuthInterceptor` ignores any inbound
* `X-SSL-Client-DN` header and falls through to token / session auth.
*/
virtual bool certAuthTrusted() {
return isLoopback();
}
}; };
} // namespace oatpp_authkit } // namespace oatpp_authkit