-
Notifications
You must be signed in to change notification settings - Fork 0
Authentication
Authentication runs on Better Auth, backed by
the project's own Prisma/PostgreSQL database. Configuration lives in
src/lib/auth/server.ts.
Gardik used Auth.js (next-auth v5) until the
better_authmigration.AUTH_SECRETandAUTH_URLwere replaced byBETTER_AUTH_SECRETandBETTER_AUTH_URL. See Configuration.
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.
| 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.
Two company-level switches drive the policy, both on the Company model:
-
allowedAuthMethods: which ofTOTP,EMAIL_OTP,PASSKEYmay 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.
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.
The SSO plugin exposes provider management to any authenticated session, which
would let a Viewer enroll an identity provider on their own company. Gardik
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.
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.
A deployed image has no company and no account, and public sign-up is disabled,
so nobody can create one. src/lib/auth/bootstrap.ts, wired through
src/instrumentation.ts, closes that gap at start-up when
BOOTSTRAP_ADMIN_EMAIL and BOOTSTRAP_INVITE_TOKEN are both set.
It issues an invitation rather than setting a password, so everything above about validation, hashing, verification and the forced second factor applies to it unchanged. The token is supplied by the operator rather than generated, which is what keeps the container from ever printing a secret.
The gate asks whether any account already carries a password, not whether any user exists. The stricter-looking rule is the more fragile one: a link lost before use would lock the operator out permanently. Testing for a password instead lets a restart reissue the invitation, while still closing the door the instant anybody can genuinely sign in.
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.
- Roles and Permissions for what a signed-in user is allowed to do
- Audit Log for what gets recorded
- Security for encryption, rate limiting and the CI gates
Gardik is source-available software by Melvin PETIT (WhiteMuush). Live demo, read only, no signup.
Getting started
Architecture
Features
- Breach Scanning
- Risk Scoring
- Directory Integrations
- MFA Coverage
- SCIM Provisioning
- Dashboard and Widgets
- Reports
- Exposure Register
Integrations
Identity and access
Reference
Contributing