Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 60 additions & 5 deletions content/docs/guides/auth-sso.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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://<your-domain>/api/v1/auth/sso/saml2/sp/acs/acme-saml",
"spMetadataUrl": "https://<your-domain>/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:
Expand Down
77 changes: 73 additions & 4 deletions content/docs/guides/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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`),
Expand Down
7 changes: 7 additions & 0 deletions content/docs/guides/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

```
Expand Down Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions docs/HARDENING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down