Skip to main content

2. Authentication

  • Authentication is by Better Auth session cookie, with basePath /api/v1/auth.
  • Cookie prefix is dffms. In production secure cookies are enabled; the cookie name is __Secure-dffms.session_token. Cookies are host-only (cross-subdomain cookies are disabled).
  • The Better Auth plugins configured are twoFactor and passkey. No bearer-token plugin is configured, so the API reads only the session cookie. (Inferred from the plugin list; the code does not state this explicitly.)
  • Sign-in methods:
    • Email and password. requireEmailVerification is true, so unverified accounts cannot sign in. Minimum password length 6, maximum 128.
    • Google sign-in, when enabled for the platform. disableImplicitSignUp is true, so Google sign-in does not create accounts.
    • Passkeys (WebAuthn), through @better-auth/passkey.
  • Session lifetime is 4 hours. The session is refreshed every 15 minutes of activity. freshAge is 5 minutes (Better Auth default in this configuration).
  • Staff accounts are invite-only (accepted invitation converts to a membership). Public accounts have accessType PUBLIC_USER.

Example of the cookie header sent on every authenticated request:

Cookie: __Secure-dffms.session_token=<session-cookie>

2.2 Multi-factor authentication and step-up​

Two different MFA concepts appear in the code and must not be confused:

ConceptWhere enforcedRule
Tenant MFA enrolmentresolveActorFromRequest, requireAuthorization, requireReportingAccess, requirePlatformSuperAdminIf the tenant's security policy sets requireMfaEnrollment, the account must have 2FA enrolled, otherwise MFA_REQUIRED. Default policy value is false.
Step-up for high-risk actionsauthorize(); finance service requireStepUpActions approve, reverse, refund, payout, configure, export require an MFA-enrolled account and a session younger than the action's freshness window.

Freshness windows as coded:

PathWindowFailure
Domain authorize() step-up actions15 minutes since session.createdAt403 with reason fresh_auth_required (mapped to FRESH_AUTH_REQUIRED or FORBIDDEN depending on route)
Finance service (requireStepUp) and finance wrapper (requireFreshSession)5 minutes since session.createdAt401 FRESH_AUTH_REQUIRED from the service; the wrapper returns 401 UNAUTHENTICATED
Platform Super Administrator access management and marketplace decision5 minutes (requireFreshSession)401 FRESH_AUTH_REQUIRED

Passkeys: when a tenant enforces MFA enrolment, requirePlatformSuperAdmin also requires at least one registered passkey for the Platform Super Administrator (MFA_REQUIRED, 401).

To satisfy a fresh-authentication requirement, the client signs in again (POST /api/v1/auth/sign-in/email, or the passkey or Google flow) and then calls the endpoint within the window.

2.3 Tenant selection​

  • X-DFFMS-Tenant-Id: <tenantId> is required on: generic resources (/api/v1/{resource}), finance applications, financing cases, finance transactions, reconciliation sync.
    • The header must be a UUID. Missing or invalid values return 400 VALIDATION_ERROR on generic and finance-application routes, and 422 VALIDATION_ERROR on the finance-transaction and reconciliation routes (see Section 4).
    • The actor must hold an active role assignment in that tenant. Otherwise 403 FORBIDDEN.
  • Reporting, marketplace decision and push-subscription routes ignore the header. They resolve the tenant from the account:
    • Staff: the oldest ACTIVE membership of the account (getPortalAccess).
    • Public users (push subscriptions): the public marketplace tenant 00000000-0000-0000-0000-000000000000.
    • Platform Super Administrator decision: the portal access tenant as above.
  • Tenant identifiers are never inferred from client-supplied record identifiers. Record lookups are constrained by tenantId in every query.

2.4 Unauthenticated endpoints​

  • GET /api/v1/health, GET /api/v1/ready, GET /api/v1/openapi.
  • POST /api/v1/contact: no session, but a mutation Origin check applies.
  • POST /api/v1/webhooks/{provider}: no session; authenticated by provider HMAC signature.
  • /api/v1/auth/*: Better Auth endpoints, each with its own rules.

2.5 Origin checks (CSRF defence for mutations)​

Every state-changing request that uses a session cookie must send an Origin header that is in the allowed-origin set.

  • Requests from an origin that is not allowed are rejected: 400 VALIDATION_ERROR on the generic-resource and finance-application routes, 403 ORIGIN_DENIED elsewhere.
  • CORS preflight (OPTIONS) on /api/* from an origin that is not allowed returns 403.

Example:

Origin: https://dffms.org