Security¶
CORS Configuration¶
The server restricts cross-origin requests to a whitelist of allowed origins. By default only the FRONTEND_URL is allowed.
| Env var | Default | Description |
|---|---|---|
ALLOWED_ORIGINS |
FRONTEND_URL value |
Comma-separated list of allowed origins |
Origins can also be managed at runtime via Admin > Security Settings in the web UI (settings:manage permission required).
CSRF Protection¶
State-changing requests (POST, PUT, PATCH, DELETE) require a valid CSRF token when cookie-based sessions are active (OIDC enabled).
- On login, the server sets a
floh_csrfcookie (not httpOnly). - The frontend reads this cookie and sends it as the
X-CSRF-Tokenheader on mutating requests. - The server validates that the header matches the cookie value.
- CSRF protection is automatically disabled when OIDC is not configured (dev mode).
API clients using Bearer tokens are not affected by CSRF checks.
Webhook Authentication¶
Connector webhook endpoints require HMAC-SHA256 signature verification.
- Configure a webhook secret on the connector (stored encrypted in the DB).
- The caller computes
HMAC-SHA256(secret, request_body)and sends it as theX-Webhook-Signatureheader. - The server verifies the signature before processing the event.
Requests without a valid signature receive a 401 Unauthorized response.
Rate Limiting¶
The server applies rate limiting via @fastify/rate-limit:
| Scope | Limit | Window |
|---|---|---|
| Global | 200 req | 1 minute |
/api/auth/* |
20 req | 1 minute |
/api/entitlements/webhook/* |
30 req | 1 minute |
Localhost (127.0.0.1, ::1) can be excluded from global rate limits in local
development. When TRUST_PROXY=true, localhost allowlisting is disabled to prevent
forwarded-header spoofing from bypassing per-IP throttling.
Set TRUST_PROXY=true only when your reverse proxy/load balancer is trusted and
sanitizes X-Forwarded-* headers.
Session Security¶
| Property | Value |
|---|---|
| Cookie name | floh_sid |
httpOnly |
true |
secure |
true when FRONTEND_URL starts with https |
sameSite |
lax |
| TTL | 24 hours |
| Encryption | Optional via SESSION_ENCRYPTION_KEY (64-char hex) |
| Storage | Redis with key floh:session:{id} |
Step-Up Authentication¶
Floh enforces step-up (re-authenticated MFA) on a curated set of high-risk endpoints. The flow follows the Authifi step-up contract:
- The server returns
401 { "code": "MFA_OR_AAL_2_REQUIRED", "windowSeconds": N }. - The Angular SPAs intercept that response. By default a same-origin popup handles the challenge so the opener SPA never unmounts and all in-progress form state is preserved:
- A PrimeNG dialog (
StepUpDialogComponent, shared from@floh/web-shared) asks the user to click Verify. The click is a user gesture, which is required forwindow.opento bypass popup blockers. - The popup navigates to
/api/auth/login?popup=1&acr_values=mod-mf. The server encodespopup: trueinto the OAuthstate. - After the IdP round-trip,
/api/auth/callbackseesstate.popupand redirects the popup to/auth/step-up-done. That page posts afloh:step-up:donemessage back towindow.openerand closes. stepUpInterceptorresolves the pending request and retries the original HTTP call automatically.- When popups are unavailable (blocked by browser, disabled by admin via
STEP_UP_POPUP_ENABLED=false, or the user clicks "Continue with full page" after a block), the interceptor falls back to the legacy full-page redirect:/api/auth/login?acr_values=mod-mf&return_to=<currentPath>. - The OIDC
/callbackendpoint extractsamrandauth_timefrom the new ID token and writesamr+mfaLoginTsinto the cookie session. - The original action is retried (in the popup flow, silently; in the redirect flow, by the user resuming the action).
Verification model¶
The guard trusts the amr claim once it has been recorded in the session, but
rejects the call when the most recent step-up is older than the configured
freshness window. Refresh-token rotation does not advance mfaLoginTs —
a fresh interactive /callback is required.
| Setting | Default | Override |
|---|---|---|
STEP_UP_AUTH_WINDOW_SECONDS |
300 |
Per-call (consent step, catalog workflow). |
STEP_UP_POPUP_ENABLED |
true |
Set to false to force the legacy full-page redirect flow for every step-up challenge. Exposed to SPAs via GET /api/auth/config.stepUpPopupEnabled so the interceptor can bypass the popup entirely. |
Popup flow HTTP headers¶
The popup flow depends on Cross-Origin-Opener-Policy: same-origin-allow-popups
so the popup retains window.opener long enough to postMessage back. The
server sets this explicitly in the @fastify/helmet registration; do not
downgrade it to same-origin without also disabling STEP_UP_POPUP_ENABLED.
Guarded endpoints¶
- PAM:
GET /pam/sessions/:id/credential(credential reveal),POST/PUT/DELETE /pam/policies(policy mutations). - API tokens:
POST /auth/tokens(token creation). - Permission overrides:
PUT/DELETE /users/:id/permission-overrides. - Connectors:
POST /connectors,PUT /connectors/:id,POST /connectors/rotate-keys. - Config transfer:
POST /config-transfer/import. - Workflows (per-workflow opt-in): consent step (
requireStepUpAuth) and catalog submission (catalogRequireStepUpAuth) — each may set its ownstepUpWindowSecondsoverride.
Bearer-token caller policy (two tiers)¶
- Admin-issued infrastructure operations (PAM credential reveal, PAM policy
mutations, API token creation, permission overrides, connector secret
writes/key rotation, config import) — bearer-token callers (API tokens,
IDP-issued JWTs) are exempt from per-request step-up enforcement. The
trust boundary is
POST /auth/tokens, which is itselfrequireStepUp()-guarded, so a token can only be issued by an admin with a fresh MFA session and subsequent automation reuses that issuance-time MFA. - Per-workflow opt-in step-up (consent step
requireStepUpAuth, catalogcatalogRequireStepUpAuth) — bearer-token callers are rejected with401 MFA_OR_AAL_2_REQUIRED. The workflow author explicitly required per-action MFA, so automation that cannot prove a fresh interactive MFA event must not satisfy the gate. Operate these workflows from an interactive session.
OIDC Startup Guard¶
Dev auth bypass has been removed. The server refuses to start in all environments when required OIDC fields are missing:
OIDC_ISSUEROIDC_CLIENT_IDOIDC_CLIENT_SECRETOIDC_REDIRECT_URI
Secret Management¶
The following secrets must be explicitly set in production. The server will refuse to start if any of them use their default/fallback values:
| Secret | Env var | Default (dev only) |
|---|---|---|
| JWT secret | JWT_SECRET |
dev-jwt-secret |
| Database password | DB_PASSWORD |
floh_secret |
| Session secret | SESSION_SECRET |
Same as JWT_SECRET |
| Connector encryption | CONNECTOR_ENCRYPTION_KEY |
Insecure dev key |
| Session encryption | SESSION_ENCRYPTION_KEY |
Unencrypted storage |
Generate encryption keys with:
Error Handling¶
In production, API error responses contain only statusCode, error, and a generic message. Stack traces and internal details are never exposed. In development, stack traces are included when SHOW_ERROR_DETAILS=true.
Connector and sync-management routes now use a shared API error envelope helper
so explicit route-level failures (for example 400/403/404/409 paths) follow the
same { statusCode, error, message } shape as global error-handler responses.
Sensitive Logging Controls¶
Authentication integrations must not log session payloads, bearer tokens, or raw response bodies from identity providers. Auth-related logs should include only sanitized URLs and status metadata.
OIDC and MCP outbound identity-provider/API calls should enforce bounded timeouts (AbortSignal) so network hangs fail fast and are observable.
Permission payloads loaded from persisted JSON (API tokens and overrides) are validated as string arrays before use. Malformed payloads are rejected or ignored with warning logs.
Production Checklist¶
- [ ]
NODE_ENV=production - [ ]
OIDC_ISSUERis set - [ ]
JWT_SECRETis a strong random value - [ ]
DB_PASSWORDis not the default - [ ]
SESSION_SECRETis a strong random value - [ ]
CONNECTOR_ENCRYPTION_KEYis a 64-char hex key - [ ]
SESSION_ENCRYPTION_KEYis a 64-char hex key - [ ]
AUDIT_CHECKPOINT_KEYis a 64-char hex key - [ ]
ALLOWED_ORIGINSis restricted to known frontend URLs - [ ]
SHOW_ERROR_DETAILS=false - [ ] Rate limiting is enabled