Skip to content

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_csrf cookie (not httpOnly).
  • The frontend reads this cookie and sends it as the X-CSRF-Token header 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.

  1. Configure a webhook secret on the connector (stored encrypted in the DB).
  2. The caller computes HMAC-SHA256(secret, request_body) and sends it as the X-Webhook-Signature header.
  3. 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:

  1. The server returns 401 { "code": "MFA_OR_AAL_2_REQUIRED", "windowSeconds": N }.
  2. 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:
  3. A PrimeNG dialog (StepUpDialogComponent, shared from @floh/web-shared) asks the user to click Verify. The click is a user gesture, which is required for window.open to bypass popup blockers.
  4. The popup navigates to /api/auth/login?popup=1&acr_values=mod-mf. The server encodes popup: true into the OAuth state.
  5. After the IdP round-trip, /api/auth/callback sees state.popup and redirects the popup to /auth/step-up-done. That page posts a floh:step-up:done message back to window.opener and closes.
  6. stepUpInterceptor resolves the pending request and retries the original HTTP call automatically.
  7. 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>.
  8. The OIDC /callback endpoint extracts amr and auth_time from the new ID token and writes amr + mfaLoginTs into the cookie session.
  9. 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.

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 own stepUpWindowSeconds override.

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 itself requireStepUp()-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, catalog catalogRequireStepUpAuth) — bearer-token callers are rejected with 401 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_ISSUER
  • OIDC_CLIENT_ID
  • OIDC_CLIENT_SECRET
  • OIDC_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:

openssl rand -hex 32

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_ISSUER is set
  • [ ] JWT_SECRET is a strong random value
  • [ ] DB_PASSWORD is not the default
  • [ ] SESSION_SECRET is a strong random value
  • [ ] CONNECTOR_ENCRYPTION_KEY is a 64-char hex key
  • [ ] SESSION_ENCRYPTION_KEY is a 64-char hex key
  • [ ] AUDIT_CHECKPOINT_KEY is a 64-char hex key
  • [ ] ALLOWED_ORIGINS is restricted to known frontend URLs
  • [ ] SHOW_ERROR_DETAILS=false
  • [ ] Rate limiting is enabled