Skip to content
Fusion

Enterprise SSO & directory

In a secure on-prem deployment, users don't type a password into the app — a reverse proxy at the edge terminates Kerberos/SPNEGO or smartcard (mTLS) auth and forwards the established identity as request headers. Two packages turn that into a real session, and both are off by default (a plain checkout keeps email/password login) — the same graceful-degradation contract as every optional integration in the stack.

  • @tikab-interactive/fusion-edge-sso answers "who is it" — a pure, isomorphic resolver that verifies a request came through the trusted edge and reads the identity out of the headers. Header alone is not enough: a shared secret (or IP allow-list) proves the request is really from the edge, so the headers can't be spoofed by anything that reaches the app directly.
  • @tikab-interactive/fusion-ldap answers "what does the directory know" — it reads a user's AD groups and computes which projects those groups grant, behind a DirectoryClient interface (a real ldapts adapter, or an in-memory fake for tests). Authentication is never done here; the directory is read with a service-account bind.
Loading diagram...

Issuing the session

A session + cookie can only be minted cleanly inside a Better Auth endpoint (that's where the cookie context lives), so the integration is a Better Auth plugin in fusion-auth: edgeSsoPlugin. It is deliberately generic — the app injects a resolve(headers) callback, so the plugin depends on no specific header library. It mounts a /api/auth/edge/sign-in endpoint that resolves the identity, finds (or, opt-in, provisions) the user, and signs them in — modelled on Better Auth's own first-party anonymous plugin (internalAdapter.createSessionsetSessionCookie).

example/src/lib/auth.ts
export const auth = createAuth({ db, plugins: edgeSsoPlugins() });
 
export const { getSession, requireSession } = createAuthSession(auth, db);

edgeSsoPlugins() returns [] unless SSO_ENABLED, so this is a no-op by default.

The example adapts fusion-edge-sso's SsoResult to the plugin's library-agnostic EdgeResolution in src/lib/edge-sso.ts, and the login page shows a "Sign in with SSO" affordance only when SSO is enabled (a loader reads the flag server-side).

The endpoint handles all four outcomes explicitly: authenticated → sign in and redirect; challenge401 WWW-Authenticate: Negotiate (domain browsers retry with a Kerberos ticket); anonymous → redirect to the form-login fallback; error → 403.

Directory sync

On edge sign-in, the plugin's best-effort afterSignIn hook reconciles the user's AD-group → ProcessN-project memberships (src/lib/ldap-sync.ts): it reads the directory groups, runs the pure planProjectMemberships diff, and applies the change set to processn_projectuser (granting/revoking only is_ad_synced rows — manual grants are never touched). A failure here never blocks an otherwise-valid sign-in.

Configuration

Everything is environment-driven; unset means off.

VariablePurpose
SSO_ENABLEDMaster switch for edge SSO.
SSO_TRUST_MODEsecret (default) · ip · secret_and_ip · none.
SSO_EDGE_SECRETThe shared secret the edge injects (anti-spoofing).
SSO_EMAIL_DOMAINDerives username@domain to match/provision the app user.
SSO_PROVISION_USERSCreate users the directory knows but the app hasn't seen (default off).
LDAP_ENABLED, LDAP_URL, LDAP_BIND_DN, …Service-account directory access (see fusion-ldap).

This is the air-gapped delivery posture: the reverse proxy is the security boundary, the app trusts it via the secret, and no model or cloud call is involved — sign-in works fully offline.