Skip to main content

Authentication

flAPI exposes three auth.type values, evaluated by the auth middleware in auth_middleware.cpp:164-171:

typeUse caseNotes
basicHTTP Basic — inline users or AWS-Secrets-backed user tablesVerify in auth_middleware.cpp (processBasicAuth); user list either inline under auth.users: or loaded from AWS via auth.from-aws-secretmanager
bearerStateless JWT bearer tokens (HS256)Set jwt-secret and jwt-issuer to validate the token; same code path also recognises shared-secret bearer tokens
oidcOpenID Connect via Google, Microsoft, Keycloak, Auth0, Okta, GitHub, or a generic providerSet 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:

FormatDetected byStatus
$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 hexlength+charsetMD5 hash. Still accepted, but the startup auditor emits a deprecation warning at boot — MD5 has no salt and is fast to brute-force
anything elsefallbackPlaintext. 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
}
  • sub is extracted into auth.username
  • roles (a JSON array) is extracted into auth.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-typeIssuer templateDefault username-claimNotes
googlehttps://accounts.google.comemailWorkspace SSO
microsofthttps://login.microsoftonline.com/{tenant}/v2.0preferred_username{tenant} required
keycloakhttps://keycloak.example.com/realms/{realm}preferred_username{realm} required; roles in realm_access.roles
auth0https://{domain}.auth0.comemail{domain} required
oktahttps://{domain}.okta.com/oauth2/defaultpreferred_username{domain} required
githubhttps://github.comloginOAuth 2.0 (not full OIDC)
generic(must be provided)subAny spec-compliant OIDC IdP

Full Key Reference

KeyDefaultDescription
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-typegenericPreset key from the table above
auth.oidc.allowed-audiences-List of acceptable aud claims
auth.oidc.verify-expirationtrueEnforce exp claim
auth.oidc.clock-skew-seconds300Allowed exp/nbf drift
auth.oidc.username-claimsubClaim mapped to auth.username
auth.oidc.email-claimemailClaim mapped to auth.email
auth.oidc.roles-claimrolesFlat roles claim
auth.oidc.role-claim-path-Dotted path for nested roles (e.g. realm_access.roles)
auth.oidc.groups-claimgroupsClaim mapped to auth.groups
auth.oidc.scopespreset-specificOAuth scopes requested in token exchange
auth.oidc.jwks-cache-hours24JWKS document cache TTL
auth.oidc.enable-client-credentialsfalseAllow client-credentials grant exchange
auth.oidc.enable-refresh-tokensfalseAllow 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;
KeyDescription
auth.from-aws-secretmanager.secret-nameName of the AWS secret
auth.from-aws-secretmanager.secret-tableDuckDB table to materialize secret into
auth.from-aws-secretmanager.regionAWS region
auth.from-aws-secretmanager.secret-idAWS access key ID
auth.from-aws-secretmanager.secret-keyAWS secret access key
auth.from-aws-secretmanager.initOptional 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:

VariableDescription
{{{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: true in production (flapi.yaml).
  • Use allowed-audiences on every OIDC config to bind tokens to your API.
  • Keep jwks-cache-hours low if your IdP rotates keys frequently.
  • Prefer OIDC over jwt/bearer for asymmetric signature validation.
  • Never embed plaintext passwords in committed YAML; MD5 hashing is supported by verifyPassword for Basic Auth but is not strong by modern standards.

Next Steps

🍪 Cookie Settings