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.
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)
🌐 P3 — federation breadth
Suggested order
- The session-validation gate (D1 expiry + D3 MFA) — unblocks the rest and is the biggest single piece.
- D4 session controls (reuses the same authenticated-request seam).
- D5 IP allowlist (independent — Hono middleware).
- D6 OIDC trust-list UI.
- P3 SAML assessment.
Out of scope (separate ADRs): SCIM/provisioning (done — ADR-0071 #2356), passkeys/WebAuthn, a dedicated security-events audit stream.
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) →
bindAuthSettingspatch → enforcement seam (better-authbefore/afterhook 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 isresolveAuthzContext(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.[custom][field]. Settingpassword_expiry_days(0 = off). Fieldsys_user.password_changed_at(stamp on every successful sign-up / change / reset). On authenticated requests, ifnow - 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.)[custom][field]. Settingsmfa_required(global) +mfa_grace_period_days(default 7); per-orgsys_organization.require_mfa; fieldsys_user.mfa_required_at. ThetwoFactorplugin already provides TOTP enroll/verify — requiring it is the gap: a session whose user hastwo_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-outdo not flow throughresolveExecutionContext, so they need no allowlist there.MFA_REQUIREDresponse by showing the TOTP-enrollment prompt, otherwise flippingmfa_requiredon bricks the UI for un-enrolled users. Backend gate is default-off so existing envs are unaffected.🛡️ P2 — defense in depth (after the gate exists)
[custom][field]— idle timeout, absolute max lifetime, concurrent-session cap; fieldssys_session.{last_activity_at, revoked_at, revoke_reason}. (multiSession+session.expiresIn/updateAge/freshAgeare native; idle/absolute/concurrent caps are custom and also ride the authenticated-request seam.)[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 withIP_NOT_ALLOWED.[native]— surface admin-managed OIDC providers (issuer / client / scopes / allowed domains) instead of env-onlygenericOAuth.🌐 P3 — federation breadth
@better-auth/ssovs a custom plugin. Do not ship a SAML settings surface before an enforcing impl (ADR-0049).Suggested order
Out of scope (separate ADRs): SCIM/provisioning (done — ADR-0071 #2356), passkeys/WebAuthn, a dedicated security-events audit stream.