This document describes the authentication, authorization, and input validation architecture in flAPI.
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)
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
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);
};auth:
enabled: true
type: jwt
jwt_secret: ${JWT_SECRET}
jwt_issuer: my-appToken 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...
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=
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: groupsOIDC components:
OIDCDiscoveryClient- Fetches.well-known/openid-configurationOIDCJWKSManager- Caches and refreshes JWKS keysOIDCProviderPresets- Pre-configured settings for Google, Microsoft, etc.
auth:
enabled: true
from_aws_secretmanager:
secret_name: my-app/api-users
region: us-east-1Loads user credentials from AWS Secrets Manager at startup.
Endpoints can require specific roles:
# sqls/admin_report.yaml
url-path: /admin/report
auth:
required: true
roles: [admin] # Only admin role can accessRole 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;
}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]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);
};| 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 |
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 ]+$"validators:
- type: string
preventSqlInjection: true # Default: trueWhen enabled, rejects inputs containing:
- SQL keywords in dangerous positions
- Comment sequences (
--,/*) - Common injection patterns
Triple braces {{{ }}} escape quotes for safe SQL interpolation:
-- Template
WHERE name = '{{{ params.name }}}'
-- Input: O'Brien
-- Output: WHERE name = 'O''Brien'Double braces {{ }} for numeric values (no escaping):
-- Template
LIMIT {{ params.limit }}
-- Input: 100
-- Output: LIMIT 100SELECT * 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}}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]
RateLimitMiddleware (src/rate_limit_middleware.cpp) prevents abuse:
rate_limit:
enabled: true
max: 100 # Max requests
interval: 60 # Per minutePer-endpoint override:
url-path: /expensive-endpoint
rate_limit:
enabled: true
max: 10
interval: 60MCP 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- 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.pemWhen 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.
| 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 |
- DESIGN_DECISIONS.md - Security philosophy
- ../../CONFIG_REFERENCE.md - Auth configuration options