Authentication
flAPI exposes three auth.type values, evaluated by the auth middleware in auth_middleware.cpp:164-171:
type | Use case | Notes |
|---|---|---|
basic | HTTP Basic — inline users or AWS-Secrets-backed user tables | Verify in auth_middleware.cpp (processBasicAuth); user list either inline under auth.users: or loaded from AWS via auth.from-aws-secretmanager |
bearer | Stateless JWT bearer tokens (HS256) | Set jwt-secret and jwt-issuer to validate the token; same code path also recognises shared-secret bearer tokens |
oidc | OpenID Connect via Google, Microsoft, Keycloak, Auth0, Okta, GitHub, or a generic provider | Set auth.oidc.provider-type to one of google, microsoft, keycloak, auth0, okta, github, or generic (verified against oidc_provider_presets.cpp) |
There are no other auth.type values. There is no type: jwt (use type: bearer with jwt-secret), no "API key" header (X-API-Key) flow, no custom handler plug-in, and no role-defining config block. JWT and AWS Secrets Manager are not separate schemes — they are configuration options that combine with the three real type values above.
Configuration Key
All authentication is configured under the top-level auth: key (not security: or authentication:).
auth:
enabled: true
type: basic # basic | bearer | oidc
# ...scheme-specific keys
Global vs Per-Endpoint Auth
auth can be set globally in flapi.yaml and overridden per endpoint YAML. An endpoint-level auth: block fully replaces the global one for that endpoint.
# flapi.yaml
auth:
enabled: false # disabled globally
# sqls/customers/customers-rest.yaml
url-path: /customers/
auth: # per-endpoint override
enabled: true
type: bearer
jwt-secret: '${JWT_SECRET}'
jwt-issuer: my-auth-server
When authentication fails, flAPI returns 401 Unauthorized with the WWW-Authenticate: Basic realm="flAPI" header.
Basic Authentication
Inline users with hashed passwords. flAPI auto-detects three formats by prefix:
| Format | Detected by | Status |
|---|---|---|
$pbkdf2-sha256$<iter>$<b64-salt>$<b64-hash> | leading $pbkdf2-sha256$ | Recommended. Modular Crypt Format, compatible with Python passlib and other PBKDF2-SHA256 generators. flAPI uses OpenSSL PKCS5_PBKDF2_HMAC with 600 000 iterations (OWASP 2023), 16-byte salt, 32-byte derived key |
| 32-character lowercase hex | length+charset | MD5 hash. Still accepted, but the startup auditor emits a deprecation warning at boot — MD5 has no salt and is fast to brute-force |
| anything else | fallback | Plaintext. Accepted for local demos only; the startup auditor warns about it |
auth:
enabled: true
type: basic
users:
# Recommended: PBKDF2-SHA256 hash. Generate with `passlib`:
# from passlib.hash import pbkdf2_sha256
# print(pbkdf2_sha256.using(rounds=600000).hash("secret"))
- username: admin
password: '$pbkdf2-sha256$600000$saltsaltsaltsalt$baseekey...'
roles: [admin, read, write]
- username: '{{env.CUSTOMER_API_READ_USER}}'
password: '{{env.CUSTOMER_API_READ_PASSWORD}}'
roles: [read]
Usage:
curl -u admin:secret https://api.example.com/customers/
Bearer (JWT) Authentication
type: bearer validates Authorization: Bearer <token> headers. When jwt-secret is set, the token is parsed as an HS256-signed JWT and the signature is verified against the secret; otherwise the bearer string is treated as an opaque shared secret. jwt-issuer further constrains accepted tokens to a specific issuer.
auth:
enabled: true
type: bearer
jwt-secret: '${JWT_SECRET}'
jwt-issuer: my-auth-server
Expected JWT payload:
{
"sub": "user123",
"iss": "my-auth-server",
"roles": ["user", "admin"],
"exp": 1735689600
}
subis extracted intoauth.usernameroles(a JSON array) is extracted intoauth.roles(joined as a comma-separated string in the Mustache context)
Usage:
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/customers/
OIDC Authentication
OpenID Connect with JWKS-based asymmetric signature validation, claim mapping, and provider presets. Configured under auth.oidc.*.
Provider Presets
flAPI ships with presets that fill in sensible defaults (issuer URL templates, scopes, claim mappings):
provider-type | Issuer template | Default username-claim | Notes |
|---|---|---|---|
google | https://accounts.google.com | email | Workspace SSO |
microsoft | https://login.microsoftonline.com/{tenant}/v2.0 | preferred_username | {tenant} required |
keycloak | https://keycloak.example.com/realms/{realm} | preferred_username | {realm} required; roles in realm_access.roles |
auth0 | https://{domain}.auth0.com | email | {domain} required |
okta | https://{domain}.okta.com/oauth2/default | preferred_username | {domain} required |
github | https://github.com | login | OAuth 2.0 (not full OIDC) |
generic | (must be provided) | sub | Any spec-compliant OIDC IdP |
Full Key Reference
| Key | Default | Description |
|---|---|---|
auth.oidc.issuer-url | (required for generic) | OIDC discovery base URL |
auth.oidc.client-id | (required) | OAuth client ID |
auth.oidc.client-secret | - | OAuth client secret (refresh / client-credentials flows) |
auth.oidc.provider-type | generic | Preset key from the table above |
auth.oidc.allowed-audiences | - | List of acceptable aud claims |
auth.oidc.verify-expiration | true | Enforce exp claim |
auth.oidc.clock-skew-seconds | 300 | Allowed exp/nbf drift |
auth.oidc.username-claim | sub | Claim mapped to auth.username |
auth.oidc.email-claim | email | Claim mapped to auth.email |
auth.oidc.roles-claim | roles | Flat roles claim |
auth.oidc.role-claim-path | - | Dotted path for nested roles (e.g. realm_access.roles) |
auth.oidc.groups-claim | groups | Claim mapped to auth.groups |
auth.oidc.scopes | preset-specific | OAuth scopes requested in token exchange |
auth.oidc.jwks-cache-hours | 24 | JWKS document cache TTL |
auth.oidc.enable-client-credentials | false | Allow client-credentials grant exchange |
auth.oidc.enable-refresh-tokens | false | Allow refresh-token grant exchange |
Keycloak Example (nested roles)
Keycloak puts roles under realm_access.roles. Use role-claim-path:
auth:
enabled: true
type: oidc
oidc:
provider-type: keycloak
issuer-url: https://keycloak.example.com/realms/myrealm
client-id: '${KEYCLOAK_CLIENT_ID}'
client-secret: '${KEYCLOAK_CLIENT_SECRET}'
allowed-audiences:
- account
role-claim-path: realm_access.roles
scopes: [openid, profile, email]
Sample Keycloak access-token payload (only relevant fields shown):
{
"sub": "f:abc:user1",
"iss": "https://keycloak.example.com/realms/myrealm",
"aud": "account",
"preferred_username": "alice",
"email": "alice@example.com",
"realm_access": {
"roles": ["api-user", "report-viewer"]
}
}
Microsoft Azure AD Example
auth:
enabled: true
type: oidc
oidc:
provider-type: microsoft
issuer-url: https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/v2.0
client-id: '${AZURE_CLIENT_ID}'
allowed-audiences:
- api://my-api
roles-claim: roles
Generic OIDC Example
auth:
enabled: true
type: oidc
oidc:
provider-type: generic
issuer-url: https://idp.example.com
client-id: my-api
allowed-audiences: [my-api]
scopes: [openid, profile, email]
jwks-cache-hours: 12
AWS Secrets Manager
Use AWS Secrets Manager as a backing store for Basic Auth credentials. flAPI reads the secret, materializes it into a DuckDB table, and authenticates against that table on each request.
auth:
enabled: true
type: basic
from-aws-secretmanager:
secret-name: prod/api/credentials
secret-table: api_users
region: us-east-1
secret-id: '${AWS_ACCESS_KEY}'
secret-key: '${AWS_SECRET_KEY}'
init: |
INSTALL aws;
LOAD aws;
| Key | Description |
|---|---|
auth.from-aws-secretmanager.secret-name | Name of the AWS secret |
auth.from-aws-secretmanager.secret-table | DuckDB table to materialize secret into |
auth.from-aws-secretmanager.region | AWS region |
auth.from-aws-secretmanager.secret-id | AWS access key ID |
auth.from-aws-secretmanager.secret-key | AWS secret access key |
auth.from-aws-secretmanager.init | Optional SQL run at startup |
The DuckDB Secret Manager backs this lookup; you must define a matching DuckDB secret of type S3 so flAPI can authenticate to AWS.
MCP Per-Method Auth
The MCP server has its own mcp.auth.* block. Authentication can additionally be required or relaxed per MCP method:
mcp:
enabled: true
port: 8081
auth:
enabled: true
type: bearer
jwt-secret: '${MCP_JWT_SECRET}'
methods:
tools/list:
required: false # public method discovery
tools/call:
required: true # auth enforced for invocations
resources/read:
required: true
Each entry under mcp.auth.methods.<method>.required overrides the global mcp.auth.enabled flag for that specific MCP method.
Using Auth Context in SQL Templates
Authenticated user data is available to Mustache templates via the auth context:
| Variable | Description |
|---|---|
{{{auth.username}}} | Authenticated username / subject |
{{{auth.email}}} | Email claim (OIDC) |
{{{auth.roles}}} | Roles array as joined string |
{{{auth.type}}} | Active auth type (basic, bearer, oidc, ...) |
Row-level security example. auth.roles is a comma-joined string (e.g. "read,admin"), so match it with LIKE rather than Mustache section iteration:
SELECT order_id, customer_id, total_amount
FROM orders
WHERE 1=1
AND (
-- Admins see everything
'{{auth.roles}}' LIKE '%admin%'
-- Everyone else sees only their own orders
OR customer_id = '{{{auth.username}}}'
)
ORDER BY order_date DESC
Error Responses
401 Unauthorized
Missing or invalid credentials. flAPI sets WWW-Authenticate: Basic realm="flAPI".
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="flAPI"
403 Forbidden
Returned by your SQL when role checks reject the user (flAPI does not have a config-driven role gate; enforce roles in SQL or at the gateway).
Complete Example
# sqls/customers/customer-common.yaml
auth-prod:
enabled: true
type: bearer
jwt-secret: '{{env.CUSTOMER_API_JWT_SECRET}}'
jwt-issuer: '{{env.CUSTOMER_API_JWT_ISSUER}}'
# sqls/customers/customers-rest.yaml
url-path: /customers/
{{include:request from customer-common.yaml}}
{{include:auth-prod from customer-common.yaml}}
{{include:rate-limit from customer-common.yaml}}
{{include:connection from customer-common.yaml}}
{{include:template-source from customer-common.yaml}}
with-pagination: true
Security Best Practices
- Use environment variables for
jwt-secret,client-secret, and AWS keys. - Set
enforce-https.enabled: truein production (flapi.yaml). - Use
allowed-audienceson every OIDC config to bind tokens to your API. - Keep
jwks-cache-hourslow if your IdP rotates keys frequently. - Prefer OIDC over
jwt/bearerfor asymmetric signature validation. - Never embed plaintext passwords in committed YAML; MD5 hashing is supported by
verifyPasswordfor Basic Auth but is not strong by modern standards.
Next Steps
- Validation: Validate and sanitize inputs
- Response Format: Structure API responses
- Caching Strategy: DuckLake-backed caching