Skip to content

Authentication

WhiteMuush edited this page Sep 1, 2026 · 1 revision

Authentication

Authentication runs on Better Auth, backed by the project's own Prisma/PostgreSQL database. Configuration lives in src/lib/auth/server.ts.

DataShield used Auth.js (next-auth v5) until the better_auth migration. AUTH_SECRET and AUTH_URL were replaced by BETTER_AUTH_SECRET and BETTER_AUTH_URL. See Configuration.

Why database-backed sessions

Each sign-in writes a row to the Session table rather than relying on a signed cookie alone. Sessions can therefore be listed, revoked and audited server-side, which is what makes Remediation able to cut an exposed employee's access.

Sign-in methods

Method Plugin Notes
Email and password built in Passwords hashed with bcryptjs, set explicitly on emailAndPassword.password rather than the library default
TOTP two-factor twoFactor Per-user enrollment, with backup codes and lockout after repeated failures
Email OTP twoFactor Second factor delivered by email
Passkeys @better-auth/passkey WebAuthn. A passkey sign-in is complete primary authentication, not a second factor
SSO (OIDC) @better-auth/sso One provider per company, with domain verification

A company chooses which second factors it accepts through Company.allowedAuthMethods (AuthMethod[], default [TOTP]). See PATCH /api/company/auth-policy in the API Reference.

The company auth policy

Two company-level switches drive the policy, both on the Company model:

  • allowedAuthMethods: which of TOTP, EMAIL_OTP, PASSKEY may be used.
  • ssoMandatory: when true, local sign-in is refused and members must come through the identity provider.

User.ssoExempt is the anti-lockout valve. An expired IdP certificate must not lock a whole company out of its own security product, so an exempt user can still sign in locally while ssoMandatory is on. The rule is one function, deniesLocalSignIn in src/lib/sso/policy.ts.

The policy is enforced on the passkey path too. A passkey sign-in creates the session itself, so without an explicit check a company that enabled ssoMandatory to cut someone's access would still have let them in through the passkey button.

Two-factor enrollment gate

src/lib/auth/two-factor-gate.ts decides whether a signed-in user is routed to enrollment. It refuses to force enrollment on an account with no password, because enabling TOTP requires confirming the current password: a pre-provisioned SSO account has none, so forcing it would strand the user on a form they cannot satisfy. For those accounts the identity provider owns the second factor.

SSO provider management

The SSO plugin exposes provider management to any authenticated session, which would let a Viewer enroll an identity provider on their own company. DataShield gates four plugin paths behind sso:config (src/lib/sso/policy.ts):

/sso/register
/sso/update-provider
/sso/request-domain-verification
/sso/verify-domain

Sign-in and the callback stay open by design, since they are the login path.

Provider records live in SsoProvider, one per company, with domain and domainVerified. The OIDC client configuration is encrypted at rest through a Prisma extension (src/lib/sso/encryption.ts), so it is sealed on write and opened on read without the call sites having to think about it.

POST /api/sso/resolve runs before sign-in: the login page asks whether a typed email's company has a verified provider, so it can send the user to the right place. It is deliberately unauthenticated, since there is no session yet.

Invitations and forced rotation

New users are invited rather than created with a password (src/lib/auth/invitation.ts):

  • The invitation token is single-use and expires after 72 hours (INVITATION_TTL_HOURS).
  • Only a hash of the token is stored, in UserInvitation.
  • The invitee sets a password between 12 and 72 characters. The upper bound is not cosmetic: bcrypt truncates at 72 bytes.

An administrator can force a rotation with POST /api/users/[id]/require-password-change, which sets User.mustChangePassword. A user in that state is refused by the normal guard everywhere except POST /api/account/password, which re-verifies the current password before writing the new one.

Route protection

src/middleware.ts protects every route except api/auth, static assets and /login. Authorization beyond "is signed in" is permission-based and covered in Roles and Permissions.

Related

Clone this wiki locally