diff --git a/content/docs/guides/auth-sso.mdx b/content/docs/guides/auth-sso.mdx index 30ed253587..51e1b32056 100644 --- a/content/docs/guides/auth-sso.mdx +++ b/content/docs/guides/auth-sso.mdx @@ -113,11 +113,14 @@ Enterprise packages can pass `oidcProviders` into `@objectstack/plugin-auth` or contribute them through `auth:configure`. The open-source package does not ship a generic OIDC settings UI. -> **Per-environment external IdP (ADR-0069).** Recent releases add a -> per-environment external-IdP path built on `@better-auth/sso` (a generic OIDC -> relying party) and surface an `sso` flag in the public `/auth/config` -> (`features.sso`) so a client can show an enterprise-login button when SSO is -> configured. The `oidcProviders` extension below remains the in-process path. +> **Admin-managed external IdP (ADR-0069).** Recent releases add a +> per-environment external-IdP path built on `@better-auth/sso` and surface an +> `sso` flag in the public `/auth/config` (`features.sso`) so a client can show +> an enterprise-login button when SSO is configured. Admins register providers +> **without code** from **Setup → SSO Providers** — both **OIDC** (Okta, Entra, +> Auth0, Keycloak, …) and **SAML 2.0** (see [SAML 2.0](#enterprise-sso-saml-20) +> below). The `oidcProviders` extension shown here remains the in-process path +> for framework/enterprise packages that prefer wiring providers in code. ### Quick start — Okta @@ -204,6 +207,58 @@ const oidcProviders = [ --- +## Enterprise SSO (SAML 2.0) + +SAML 2.0 is provided natively by `@better-auth/sso` (the same package behind the +OIDC path) — **no custom plugin or extension is needed**. Admins register a SAML +IdP from **Setup → SSO Providers → Register SAML Provider**, which posts to the +env-side bridge at `POST /api/v1/auth/admin/sso/register-saml`. + +### Register a SAML provider + +Fill in the IdP details collected by the **Register SAML Provider** action: + +| Field | Required | Description | +|---|---|---| +| `providerId` | ✅ | Stable identifier, e.g. `acme-saml`. | +| `issuer` | ✅ | The IdP's SAML **EntityID** (issuer), e.g. `https://saml.acme.com/entityid`. | +| `domain` | ✅ | Users with this email domain are routed to this IdP, e.g. `acme.com`. | +| `entryPoint` | ✅ | The IdP's SAML single sign-on (redirect) URL that receives the `SAMLRequest`. | +| `cert` | ✅ | The IdP's X.509 signing certificate (PEM body) — used to verify assertion signatures. | +| `identifierFormat` | — | Requested SAML NameID format (defaults to the IdP's configured format), e.g. `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`. | + +On success the response returns the two **Service Provider (SP)** URLs you +configure on the IdP side: + +```json +{ + "success": true, + "data": { "providerId": "acme-saml" }, + "acsUrl": "https:///api/v1/auth/sso/saml2/sp/acs/acme-saml", + "spMetadataUrl": "https:///api/v1/auth/sso/saml2/sp/metadata?providerId=acme-saml" +} +``` + +- **ACS (Assertion Consumer Service) URL** — register this as the SP ACS / reply + URL on the IdP. +- **SP metadata URL** — serves the SP `EntityDescriptor`/`SPSSODescriptor` XML + (many IdPs can import it directly to auto-configure the SP). + +### SAML sign-in flow + +1. User enters their work email (or clicks the enterprise-login button). +2. Client calls `POST /api/v1/auth/sign-in/sso` with `{ email, callbackURL }`. +3. The email **domain** is matched to the registered provider; the response is a + redirect to the IdP's `entryPoint` carrying a signed `SAMLRequest`. +4. The IdP authenticates the user and POSTs a SAML assertion back to the SP ACS + URL; `@better-auth/sso` (samlify) validates the signature/timestamp, then + creates a session and redirects to `callbackURL`. + +> Signature, timestamp, and replay validation are handled by `@better-auth/sso` +> (samlify). You only supply the IdP's signing certificate at registration time. + +--- + ## Verify configuration Restart the server (`pnpm dev`), then: diff --git a/content/docs/guides/authentication.mdx b/content/docs/guides/authentication.mdx index 5052eb6881..2ee27ec237 100644 --- a/content/docs/guides/authentication.mdx +++ b/content/docs/guides/authentication.mdx @@ -33,11 +33,22 @@ The `@objectstack/plugin-auth` package provides enterprise-grade authentication - ✅ **Session Management** - Automatic session handling with configurable expiry - ✅ **Password Reset** - Email-based password reset flow - ✅ **Email Verification** - Email verification workflow -- ⚙️ **2FA backend** - better-auth two-factor plugin wiring is available for custom UIs +- ✅ **Multi-Factor Authentication** - TOTP two-factor, plus org-wide **enforced MFA** with a grace period and a Console enrollment/remediation flow (ADR-0069) - ✅ **Magic Links** - Passwordless authentication - ✅ **Organizations** - Multi-tenant support - ✅ **ObjectQL Integration** - Native ObjectStack data persistence (no ORM required) +**Enterprise hardening (ADR-0069)** — configured from **Setup → Authentication**, each setting backed by real runtime enforcement: + +- ✅ **Password policy** - Complexity (min character classes), history (no-reuse), expiry, and breached-password rejection via Have I Been Pwned (k-anonymity) +- ✅ **Account lockout & rate-limiting** - Per-account lockout after N failed sign-ins (admin **Unlock** action) and per-IP throttling +- ✅ **Enforced MFA** - Org-wide `require_mfa` with a configurable grace period; the session is gated from data access until the user enrolls +- ✅ **Session controls** - Idle timeout, absolute lifetime cap, and concurrent-session limit per user +- ✅ **IP allowlist** - Restrict authenticated access to CIDR ranges +- ✅ **Enterprise SSO** - Admin-managed OIDC and SAML 2.0 trust list (see [Social & Enterprise SSO](/docs/guides/auth-sso)) + +See [Enterprise Authentication Hardening](#enterprise-authentication-hardening-adr-0069) for the full settings reference. + ### Architecture The plugin uses a **direct forwarding** architecture where all authentication requests are forwarded to Better-Auth's universal handler. This ensures: @@ -324,13 +335,71 @@ Better-Auth automatically handles the OAuth callback at `/api/v1/auth/callback/g --- +## Enterprise Authentication Hardening (ADR-0069) + +Beyond the core flows above, `@objectstack/plugin-auth` ships an enterprise +hardening layer. Every toggle is configured from **Setup → Authentication** and +is wired to real runtime enforcement (ADR-0049 — no setting ships without the +code that reads it). Changes take effect immediately; no restart is required. + +### Password policy + +| Setting | Effect | +|---|---| +| `password_reject_breached` | Reject passwords found in the Have I Been Pwned corpus (k-anonymity range check — the password is never sent in full). | +| `password_require_complexity` / `password_min_classes` | Require a minimum number of character classes (upper / lower / digit / symbol). | +| `password_history_count` | Block reuse of the last *N* passwords. | +| `password_expiry_days` | Force a password change after *N* days; expired sessions are gated until the password is rotated. | + +### Anti-abuse (lockout & rate-limiting) + +| Setting | Effect | +|---|---| +| `lockout_threshold` | Lock an account after this many consecutive failed sign-ins. | +| `lockout_duration_minutes` | How long a lockout lasts. Admins can clear it early with the **Unlock** action on the user record. | +| `rate_limit_max` / `rate_limit_window_seconds` | Per-IP request throttle on auth endpoints. | + +### Multi-factor (enforced MFA) + +| Setting | Effect | +|---|---| +| `mfa_required` | Require MFA org-wide (also settable per organization via `require_mfa`). | +| `mfa_grace_period_days` | Grace window for existing users to enroll. After it elapses, the session is allowed to sign in but **gated from data access** until TOTP is enrolled — the Console surfaces a remediation overlay that walks the user through enrollment. | + +### Sessions + +| Setting | Effect | +|---|---| +| `session_idle_timeout_minutes` | Expire a session after inactivity. | +| `session_absolute_max_hours` | Hard cap on total session lifetime regardless of activity. | +| `max_concurrent_sessions_per_user` | Cap simultaneous sessions; the oldest is revoked (expired in place) when the cap is exceeded. | + +### Network + +| Setting | Effect | +|---|---| +| `allowed_ip_ranges` | Restrict authenticated access to one or more IPv4 CIDR ranges. IP is extracted with trust-proxy awareness (`x-forwarded-for` / `cf-connecting-ip` / `x-real-ip`); requests whose IP can't be determined fail open. | + +> **How enforcement works.** Password expiry and enforced MFA share a single +> *session-validation gate*: `customSession` stamps the session with an +> `authGate` posture, and the REST server + runtime dispatcher block gated +> sessions from data access (auth, health, and a small allowlist still pass so +> the user can remediate). This is why a non-compliant user can sign in but +> can't read data until they fix the issue. + +--- + ## Advanced Features ### Two-Factor Authentication (2FA) -ObjectStack wires the better-auth two-factor plugin at the backend layer, but -the open-source Console login UI does not yet ship the complete TOTP challenge -flow. Do not expose 2FA as an end-user feature until your UI handles: +ObjectStack wires the better-auth two-factor (TOTP) plugin at the backend layer. +The endpoints below let you build an **opt-in** 2FA experience in a custom account +UI; for **organization-wide enforced MFA**, see +[Enterprise Authentication Hardening](#enterprise-authentication-hardening-adr-0069) +— the Console ships an enrollment/remediation flow for that path. + +A complete opt-in 2FA UX still needs to handle: - enrollment (`/two-factor/enable`), - TOTP confirmation (`/two-factor/verify-totp`), diff --git a/content/docs/guides/security.mdx b/content/docs/guides/security.mdx index 35a24b9e85..44d4cd2400 100644 --- a/content/docs/guides/security.mdx +++ b/content/docs/guides/security.mdx @@ -23,6 +23,12 @@ Complete guide to implementing enterprise-grade security in ObjectStack with fin ## Security Architecture +> **Authentication is the first layer.** This guide covers *authorization* — +> who can see and do what once signed in (RBAC, RLS, FLS, sharing). Harden +> *authentication* first — password policy, enforced MFA, account lockout, +> session controls, and IP allowlist — in +> [Enterprise Authentication Hardening](./authentication#enterprise-authentication-hardening-adr-0069). + ObjectStack implements a multi-layered security model: ``` @@ -689,6 +695,7 @@ writes. ### Initial Setup +- [ ] Harden authentication (password policy, enforced MFA, lockout, session controls, IP allowlist) — see [Authentication Hardening](./authentication#enterprise-authentication-hardening-adr-0069) - [ ] Define user roles and hierarchies - [ ] Create profiles for each role - [ ] Set organization-wide defaults diff --git a/docs/HARDENING.md b/docs/HARDENING.md index 89efc56c53..976d32e7b2 100644 --- a/docs/HARDENING.md +++ b/docs/HARDENING.md @@ -16,6 +16,7 @@ adapter layer for a production deployment. | Token-bucket rate limit | Off (opt-in) | `RateLimiter` primitive | | CSRF | Adapter-layer concern | `hono/secure-headers` / `hono/csrf` | | Auth (better-auth) | On | `@objectstack/plugin-auth` | +| Auth hardening (ADR-0069) | Off (opt-in) | Setup → Authentication | | Project membership (RBAC) | On when scoped | dispatcher plugin | | Field- and row-level perms | On | SecurityPlugin | | Request id / metrics / 5xx reporting | Noop default | see [Observability](./OBSERVABILITY.md) | @@ -137,6 +138,28 @@ CSRF protection becomes required when you switch to **cookie-based session auth**. In that case wire a CSRF middleware (e.g. `hono/csrf`) and exempt only the auth callback routes. +## Authentication hardening (ADR-0069) + +Beyond transport-layer defences, `@objectstack/plugin-auth` ships an enterprise +authentication-hardening layer. Each control is **opt-in** from **Setup → +Authentication** and backed by real runtime enforcement (ADR-0049). For a new +production deployment, enable at least: + +| Group | Recommended settings | +| ----------- | ------------------------------------------------------------------------------------ | +| Password | `password_reject_breached: true`, `password_require_complexity: true`, `password_history_count: 3–5`, `password_expiry_days: 90` | +| Anti-abuse | `lockout_threshold: 5`, `lockout_duration_minutes: 15`, `rate_limit_max` / `rate_limit_window_seconds` | +| MFA | `mfa_required: true` (or per-org `require_mfa`), `mfa_grace_period_days: 7` | +| Sessions | `session_idle_timeout_minutes: 30–60`, `session_absolute_max_hours: 8–12`, `max_concurrent_sessions_per_user` | +| Network | `allowed_ip_ranges` (IPv4 CIDR) for tenant- or office-scoped access | + +Password expiry and enforced MFA share a *session-validation gate*: a +non-compliant session can authenticate but is blocked from data access (auth + +health + a remediation allowlist still pass) until the user rotates the password +or enrols MFA. See the +[Authentication guide](../content/docs/guides/authentication.mdx#enterprise-authentication-hardening-adr-0069) +for the full per-setting reference. + ## JWT / session lifecycle ObjectStack uses [better-auth](https://www.better-auth.com/) via @@ -148,6 +171,7 @@ ObjectStack uses [better-auth](https://www.better-auth.com/) via | Access token TTL | inherited from session | configure in better-auth | | Refresh | better-auth `/auth/get-session` rolls TTL | `session.updateAge` (seconds) | | Revocation | DELETE on `session` row | `revokeSessionsOnPasswordReset: true` | +| Idle / absolute / concurrent caps | Off (opt-in) | `session_idle_timeout_minutes` / `session_absolute_max_hours` / `max_concurrent_sessions_per_user` (Setup → Authentication) | | Email verify TTL | 1 hour | `emailVerification.expiresIn` | ### Verification checklist (run before going live)