Security Architecture
May 22, 2026 · View on GitHub
This document describes the authentication, authorization, and input validation architecture in flAPI.
Overview
flAPI implements defense-in-depth security:
- Authentication - Verify identity (JWT, Basic, OIDC)
- Authorization - Check permissions (roles)
- Input Validation - Sanitize inputs (validators)
- Output Escaping - Prevent injection (Mustache braces)
Architecture
graph TB
subgraph "Request Flow"
REQ[HTTP Request]
end
subgraph "Authentication Layer"
AM[AuthMiddleware]
JWT[JWT Validator]
Basic[Basic Auth]
OIDC[OIDC Handler]
end
subgraph "Authorization Layer"
RoleCheck[Role Check]
EndpointAuth[Endpoint Auth Config]
end
subgraph "Validation Layer"
RV[RequestValidator]
TypeVal[Type Validators]
PatternVal[Pattern Validators]
end
subgraph "Execution Layer"
STP[SQLTemplateProcessor]
Escape[Quote Escaping]
end
REQ --> AM
AM --> JWT
AM --> Basic
AM --> OIDC
AM --> RoleCheck
RoleCheck --> EndpointAuth
EndpointAuth --> RV
RV --> TypeVal
RV --> PatternVal
RV --> STP
STP --> Escape
Authentication
AuthMiddleware (src/auth_middleware.cpp)
Middleware that validates credentials before request processing.
class AuthMiddleware {
public:
struct context {
bool authenticated = false;
std::string username;
std::vector<std::string> roles;
};
void before_handle(crow::request& req, crow::response& res, context& ctx);
void after_handle(crow::request& req, crow::response& res, context& ctx);
};
Authentication Types
JWT Authentication
auth:
enabled: true
type: jwt
jwt_secret: ${JWT_SECRET}
jwt_issuer: my-app
Token validation:
bool validateJWT(const std::string& token) {
auto decoded = jwt::decode(token);
auto verifier = jwt::verify()
.allow_algorithm(jwt::algorithm::hs256{jwt_secret_})
.with_issuer(jwt_issuer_);
verifier.verify(decoded);
return true;
}
Expected header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Basic Authentication
auth:
enabled: true
type: basic
users:
- username: admin
password: ${ADMIN_PASSWORD}
roles: [admin, user]
- username: reader
password: ${READER_PASSWORD}
roles: [user]
Expected header:
Authorization: Basic YWRtaW46cGFzc3dvcmQ=
OIDC Authentication (src/oidc_auth_handler.cpp)
auth:
enabled: true
type: oidc
oidc:
provider_type: google # or microsoft, keycloak, generic
issuer_url: https://accounts.google.com
client_id: ${GOOGLE_CLIENT_ID}
allowed_audiences:
- my-app-client-id
username_claim: email
roles_claim: groups
OIDC components:
OIDCDiscoveryClient- Fetches.well-known/openid-configurationOIDCJWKSManager- Caches and refreshes JWKS keysOIDCProviderPresets- Pre-configured settings for Google, Microsoft, etc.
AWS Secrets Manager Integration
auth:
enabled: true
from_aws_secretmanager:
secret_name: my-app/api-users
region: us-east-1
Loads user credentials from AWS Secrets Manager at startup.
Authorization
Role-Based Access Control
Endpoints can require specific roles:
# sqls/admin_report.yaml
url-path: /admin/report
auth:
required: true
roles: [admin] # Only admin role can access
Role checking:
bool checkRoles(const context& ctx, const EndpointConfig& endpoint) {
if (!endpoint.auth.required) return true;
if (endpoint.auth.roles.empty()) return ctx.authenticated;
for (const auto& required_role : endpoint.auth.roles) {
if (std::find(ctx.roles.begin(), ctx.roles.end(), required_role)
!= ctx.roles.end()) {
return true;
}
}
return false;
}
Per-Endpoint Auth Override
Each endpoint can override global auth settings:
# Public endpoint (no auth)
url-path: /public/status
auth:
required: false
# Protected endpoint with specific roles
url-path: /admin/users
auth:
required: true
roles: [admin]
Input Validation
RequestValidator (src/request_validator.cpp)
Validates all incoming parameters against configured rules.
class RequestValidator {
public:
ValidationResult validate(const std::map<std::string, std::string>& params,
const std::vector<RequestFieldConfig>& fields);
private:
bool validateInt(const std::string& value, const ValidatorConfig& config);
bool validateString(const std::string& value, const ValidatorConfig& config);
bool validateEmail(const std::string& value);
bool validateUUID(const std::string& value);
bool validateEnum(const std::string& value, const ValidatorConfig& config);
bool validateDate(const std::string& value, const ValidatorConfig& config);
bool validatePattern(const std::string& value, const std::string& pattern);
};
Validator Types
| Type | Options | Example |
|---|---|---|
int | min, max | min: 1, max: 1000 |
string | min-length, max-length, pattern | max-length: 200 |
email | - | Validates email format |
uuid | - | Validates UUID format |
enum | values | values: [active, inactive] |
date | min, max | min: 2020-01-01 |
time | min, max | min: 09:00:00 |
Validator Configuration
request:
- field-name: customer_id
field-in: query
required: true
validators:
- type: int
min: 1
max: 999999
- field-name: email
field-in: body
validators:
- type: email
- field-name: status
field-in: query
validators:
- type: enum
values: [active, inactive, pending]
- field-name: name
field-in: body
validators:
- type: string
max-length: 200
pattern: "^[a-zA-Z0-9 ]+$"
SQL Injection Prevention
validators:
- type: string
preventSqlInjection: true # Default: true
When enabled, rejects inputs containing:
- SQL keywords in dangerous positions
- Comment sequences (
--,/*) - Common injection patterns
Output Escaping (Mustache)
Triple Braces for Strings
Triple braces {{{ }}} escape quotes for safe SQL interpolation:
-- Template
WHERE name = '{{{ params.name }}}'
-- Input: O'Brien
-- Output: WHERE name = 'O''Brien'
Double Braces for Numbers
Double braces {{ }} for numeric values (no escaping):
-- Template
LIMIT {{ params.limit }}
-- Input: 100
-- Output: LIMIT 100
Safe Query Pattern
SELECT * FROM customers
WHERE 1=1
{{#params.name}}
AND name = '{{{ params.name }}}' -- String: escaped
{{/params.name}}
{{#params.id}}
AND id = {{ params.id }} -- Number: raw
{{/params.id}}
Security Layers Summary
Request arrives
↓
[Layer 1: Authentication]
- Validate JWT/Basic/OIDC token
- Extract identity and roles
- Reject if auth fails → 401
↓
[Layer 2: Authorization]
- Check if endpoint requires auth
- Verify user has required roles
- Reject if unauthorized → 403
↓
[Layer 3: Input Validation]
- Apply type validators
- Check patterns and constraints
- Reject SQL injection attempts
- Reject if invalid → 400
↓
[Layer 4: Template Escaping]
- Triple braces escape quotes
- Prevents injection in SQL
↓
[Query Execution]
Rate Limiting
RateLimitMiddleware (src/rate_limit_middleware.cpp) prevents abuse:
rate_limit:
enabled: true
max: 100 # Max requests
interval: 60 # Per minute
Per-endpoint override:
url-path: /expensive-endpoint
rate_limit:
enabled: true
max: 10
interval: 60
MCP Authentication
MCP has separate auth configuration:
mcp:
auth:
enabled: true
type: bearer
jwt_secret: ${MCP_JWT_SECRET}
methods:
tools/call:
required: true
resources/read:
required: false # Allow anonymous reads
Security Best Practices
- Always use validators - Define type and constraints for all inputs
- Use triple braces - For string values in SQL templates
- Require authentication - For sensitive endpoints
- Define roles - Use RBAC for admin/write operations
- Rate limit - Protect against abuse
- Whitelist env vars - Only expose needed environment variables
- Use HTTPS - Enable TLS in production
- Never bundle secrets - Credentials come from environment variables at runtime, never from a config file checked into version control. See Secrets and the bundle below.
https:
enabled: true
ssl_cert_file: /path/to/cert.pem
ssl_key_file: /path/to/key.pem
Secrets and the bundle
When flapi pack produces a self-contained binary, it must not
include credentials. A bundled binary that contains a database
password is itself a secret: it can be scp-ed, shared, attached to
a bug report, or pushed to a public registry by mistake. The single
artifact value disappears the moment the artifact is sensitive.
Two mechanisms enforce this:
-
Pack-time refusal of the default deny list.
Pack()walks the input tree and aborts with a non-zero exit if any file matches:*.envat any depth (e.g..env,config/.env)secrets/segment at any depth (e.g.secrets/db.token,nested/secrets/api.key)*.pemat any depth*.keyat any depth
The error message names the offending file and points at the
--allow-secretstesting-only override. The override exists so we can integration-test the deny list; production users must not flip it. -
Runtime expects credentials from the environment. Every credential flapi understands is read from a documented environment variable -- never from a bundled YAML field:
- AWS S3:
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_REGION - GCS:
GOOGLE_APPLICATION_CREDENTIALS/GOOGLE_CLOUD_PROJECT - Azure:
AZURE_STORAGE_CONNECTION_STRING/AZURE_STORAGE_ACCOUNT/AZURE_STORAGE_KEY - Mgmt API:
FLAPI_CONFIG_SERVICE_TOKEN - JWT: configured via
{{env.JWT_SECRET}}against a whitelisted env var
YAML strings may interpolate
{{env.VARNAME}}for any env var declared inenvironment-whitelist. The whitelist is itself part of the configuration, so it ships in the bundle -- which is fine because it names env vars by key, not value. - AWS S3:
See DESIGN_DECISIONS.md §9 for the broader rationale and CONFIG_REFERENCE.md §1.4 for the 12-factor checklist of every env var flapi reads.
Source Files
| File | Purpose |
|---|---|
src/auth_middleware.cpp | HTTP auth middleware |
src/mcp_auth_handler.cpp | MCP auth handler |
src/oidc_auth_handler.cpp | OIDC token validation |
src/oidc_discovery_client.cpp | OIDC discovery |
src/oidc_jwks_manager.cpp | JWKS key management |
src/request_validator.cpp | Input validation |
src/rate_limit_middleware.cpp | Rate limiting |
src/pack.cpp (IsSecretExcluded) | Default secret deny list for flapi pack |
Related Documentation
- DESIGN_DECISIONS.md - Security philosophy
- ../../CONFIG_REFERENCE.md - Auth configuration options