Skip to content

[Auth] ADR-0069 remaining — session-validation gate (password expiry + enforced MFA), session controls, IP allowlist, OIDC/SAML #2375

Description

@os-zhuang

Follow-up to ADR-0069 tracking issue #2281. Collects everything not yet built. The P1 password-policy + anti-brute-force surface is now shipped; what remains is the session-validation gate (P1 leftover), defense-in-depth (P2), and federation breadth (P3).

✅ Already shipped (context)

Pattern established for all of the above: setting (auth manifest) → bindAuthSettings patch → enforcement seam (better-auth before/after hook or native plugin), default-off, ADR-0049-compliant.


🔑 P1 leftover — the session-validation gate (do these together)

Both remaining P1 items enforce at authenticated-request / session-validation time (not the auth endpoints), which today has no denial path. resolveExecutionContext (packages/runtime/src/security/resolve-execution-context.ts) is explicitly "never throws"; the shared authz resolver is resolveAuthzContext (packages/core/src/security/resolve-authz-context.ts); denial happens downstream in SecurityPlugin / the dispatcher. There is no existing MFA/step-up or password-age gate. Building this seam is the bulk of the work and is shared by both items below, so they should land as one focused effort.

  • D1 — password expiry [custom][field]. Setting password_expiry_days (0 = off). Field sys_user.password_changed_at (stamp on every successful sign-up / change / reset). On authenticated requests, if now - password_changed_at > expiry → force /change-password (PASSWORD_EXPIRED). (The history PR feat(plugin-auth): password history / no-reuse (ADR-0069 D1, P1) #2371 already touches the change/reset hooks — stamping can be added there.)
  • D3 — enforced MFA [custom][field]. Settings mfa_required (global) + mfa_grace_period_days (default 7); per-org sys_organization.require_mfa; field sys_user.mfa_required_at. The twoFactor plugin already provides TOTP enroll/verify — requiring it is the gap: a session whose user has two_factor_enabled = false (and whose grace window has elapsed) is gated out of all protected resources until enrollment + verification. The /two-factor/* enrollment endpoints and /sign-out do not flow through resolveExecutionContext, so they need no allowlist there.
    • Needs an objectui follow-up: the Console must handle an MFA_REQUIRED response by showing the TOTP-enrollment prompt, otherwise flipping mfa_required on bricks the UI for un-enrolled users. Backend gate is default-off so existing envs are unaffected.

Reuse, don't reinvent (verified against better-auth 1.6.20): TOTP/2FA mechanism, session.freshAge (re-auth freshness), revokeSessionsOnPasswordReset, password.hash/verify are all native — only the enforcement/requirement layer is custom.


🛡️ P2 — defense in depth (after the gate exists)

  • D4 session controls [custom][field] — idle timeout, absolute max lifetime, concurrent-session cap; fields sys_session.{last_activity_at, revoked_at, revoke_reason}. (multiSession + session.expiresIn/updateAge/freshAge are native; idle/absolute/concurrent caps are custom and also ride the authenticated-request seam.)
  • D5 IP allowlist [custom][field]sys_organization.allowed_ip_ranges (+ optional per-user); Hono auth-route middleware before the better-auth handler (trust-proxy-aware client-IP extraction), reject with IP_NOT_ALLOWED.
  • D6 OIDC trust-list UI [native] — surface admin-managed OIDC providers (issuer / client / scopes / allowed domains) instead of env-only genericOAuth.

🌐 P3 — federation breadth

  • SAML 2.0 — not in better-auth core; assess @better-auth/sso vs a custom plugin. Do not ship a SAML settings surface before an enforcing impl (ADR-0049).
  • Broader settings-driven social providers; per-org override UI polish.

Suggested order

  1. The session-validation gate (D1 expiry + D3 MFA) — unblocks the rest and is the biggest single piece.
  2. D4 session controls (reuses the same authenticated-request seam).
  3. D5 IP allowlist (independent — Hono middleware).
  4. D6 OIDC trust-list UI.
  5. P3 SAML assessment.

Out of scope (separate ADRs): SCIM/provisioning (done — ADR-0071 #2356), passkeys/WebAuthn, a dedicated security-events audit stream.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions