oatpp-authkit/README.md
Uwe Schuster b5e1ea1894 #12: per-decorator migration kit (Prereq.hpp)
Each decorator now bundles its schema prereqs alongside its code via
DecoratorPrereq (additive CREATE-IF-NOT-EXISTS) and ReshapeStep
(non-idempotent reshape gated on a detectSql probe).

applyDecoratorMigrations<Decorators...>(table, probe, exec, recorder)
walks the listed decorators at startup, runs every PREREQ, runs every
reshape step whose probe returns false. Database-agnostic — consumer
wires probe/exec to their DbClient. SCHEMA_MIGRATIONS_TABLE_SQL is
provided for observability; the detect-probe is the source of truth.

TemporalRepository ships add_valid_from / add_valid_until /
drop_unique_entity_id / composite_unique (UNIQUE(entity_id, valid_until)
so close-then-insert can run in a deferred-FK transaction).
AuditLogRepository ships the audit_log CREATE TABLE.
ScopeGuardRepository ships nothing — exposes empty PREREQ + zero-length
RESHAPE_STEPS so it can be listed in applyDecoratorMigrations alongside
the schema-touching decorators without SFINAE.

Closes #12

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:47:03 +02:00

7.1 KiB

oatpp-authkit

Header-only C++ library distilled from 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.
repo/Repository.hpp + IHistoryRepository.hpp + TemporalFieldTraits.hpp + TemporalAt.hpp + ActorContext.hpp Pure-abstract Repository<TDto> interface set distilled from fewo-webapp's per-entity *Db clients. Mixed UUID allocation on save, separate IHistoryRepository<T> for temporal versions, TemporalFieldTraits<T> to map canonical (entity_id, valid_from, valid_until) onto whatever a DTO actually calls them, ActorContext placeholder for the scope-guard decorator.
repo/TemporalRepository.hpp Decorator that wraps any Repository<TDto> and turns it into a temporally-versioned one. save closes the prior live version and inserts a new one; findByEntityIdAt(id, at) returns the version live at a point in time; implements IHistoryRepository<T>. Inner adapter is expected to expose all rows (live + historical) and treat save as upsert keyed by (entity_id, valid_from). DTOs register their three temporal columns via OATPP_AUTHKIT_REGISTER_TEMPORAL.
repo/ScopeGuardRepository.hpp Generic resource-scope decorator. Takes a bool(ActorContext, TDto) predicate at construction; gates every method on it. Throws ScopeDeniedException on deny (catchers translate to 403). Knows nothing about consumer-specific concepts like "property" or "tenant" — the predicate decides.
repo/IQueryable.hpp Optional capability for repos that resolve a typed query AST. field<&Dto::col>().eq(...) style DSL composes via && / `
repo/IAuditSink.hpp + repo/AuditLogRepository.hpp Cross-cutting audit-trail decorator. Emits an AuditEvent (actor, entity type/id, op, timestamp) per mutation through a consumer-supplied IAuditSink. Ops are Create / Update / Delete / Read; pre-write findByEntityId lookup distinguishes Create from Update. Configurable enabled-op set (default {Create,Update,Delete}Read is opt-in, list() never audited). Sink failures are caught and swallowed unless a bool(const std::exception&) handler asks to rethrow. Stacks with TemporalRepository and ScopeGuardRepository.
repo/Prereq.hpp Per-decorator migration kit. Each decorator that touches schema bundles its prereq SQL alongside its code: DecoratorPrereq for additive (CREATE TABLE IF NOT EXISTS …) and ReshapeStep for non-idempotent reshape with a detectSql probe. applyDecoratorMigrations<Decorators...>(table, probe, exec, recorder) walks the listed decorators at startup, runs every PREREQ, runs every reshape step whose probe returns false, optionally records applied steps via oatpp_authkit_schema_migrations. Database-agnostic — consumer wires probe/exec to whatever DbClient they use.

Decorator migrations

Decorator PREREQ RESHAPE_STEPS
TemporalRepository<T> (none) add_valid_from, add_valid_until, drop_unique_entity_id (consumer-overridable noop), composite_unique — composite UNIQUE(entity_id, valid_until) so close-then-insert can run inside a deferred-FK transaction.
AuditLogRepository<T> CREATE TABLE IF NOT EXISTS audit_log (…) — fixed shape, no {table} placeholder. (none)
ScopeGuardRepository<T> (none) (none)

Wiring it up:

#include "oatpp-authkit/repo/Prereq.hpp"

// probe: returns true iff the SQL yields ≥1 row
auto probe = [&](const std::string& sql) { /* run SELECT, return bool */ };
auto exec  = [&](const std::string& sql) { /* run DDL */ };

oatpp_authkit::repo::applyDecoratorMigrations<
    oatpp_authkit::repo::TemporalRepository<PersonDto>,
    oatpp_authkit::repo::AuditLogRepository<PersonDto>>(
    "persons", probe, exec);

Re-running on every startup is safe by construction.

Consume via 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:

find_package(oatpp-authkit 0.1 REQUIRED)
target_link_libraries(app PRIVATE oatpp::authkit)

Browser-friendly 401/403

By default AuthInterceptor returns application/json for every rejection, which is correct for /api/* callers but breaks browser navigation: a user following a stale link or an expired password-reset URL sees a raw {"status":"Unauthorized"} instead of a real page.

Override IAuthPolicy::unauthenticatedRedirect(path) to redirect browser navigations to a login or landing page while keeping JSON responses for fetch/axios callers (detected via path prefix /api/, X-Requested-With: XMLHttpRequest, or an Accept header that prefers application/json):

class AppAuthPolicy : public oatpp_authkit::IAuthPolicy {
public:
    std::optional<std::string>
    unauthenticatedRedirect(const std::string& path) override {
        return "/?next=" + oatpp_authkit::AuthInterceptor::urlEncode(path);
    }
};

Returning std::nullopt (the default) preserves the legacy JSON behaviour for all responses.

Tests

cmake -B build -DOATPP_AUTHKIT_BUILD_TESTS=ON
cmake --build build
ctest --test-dir build --output-on-failure

Tests are off by default so consumers pulling the library in via FetchContent don't pay the cost.

Roadmap

  • v0.2AuthInterceptor + 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.