effect-auth

Security Policies

Configure secure defaults and operation-specific overrides.

AuthRateLimit is the HTTP-operation security seam for effect-auth. Its standard implementation applies operation-specific rate limits before authentication work begins. It is intentionally narrower than application authorization: it limits attempts using trusted request or session context, but it does not decide whether an actor may access your tenant, billing account, admin screen, or other application resource.

HTTP request
  -> origin/CSRF and application boundary guards
  -> secured *HttpOperation
       -> AuthRateLimit.require({ operation, ip/email/user }) exactly once
       -> authentication domain service
  -> error mapping / cookie commitment

Standard policy

Provide AuthRateLimitStandardLive() to the HTTP Operations layer. The operation implementation captures that service and calls its own policy; the preset API and a Custom API merely bind the same operation. Do not call AuthRateLimit.require again around a standard operation: doing so consumes rate-limit budget twice. Calls made directly to primitives do not pass through this seam, so the application owns equivalent boundary controls. See Architecture for the layer boundaries.

const OperationsLive = PasswordHttpOperationsLive.pipe(
  Layer.provide(AuthRateLimitStandardLive()),
  Layer.provide(AppAuthServicesLive)
);

Defaults are fixed-window rules. When an operation has two rows, both rules are combined into one RateLimitPolicy and required in one AuthRateLimit call.

OperationsKeyLimit / window
Password sign-inIP; email20 / 10m; 5 / 10m
Password sign-upIP; email10 / 1h; 3 / 1h
Password reset startIP; email10 / 10m; 3 / 10m
Password reset verifyIP20 / 10m
Email verification startIP; email10 / 10m; 3 / 10m
Email OTP startIP; email10 / 10m; 5 / 10m
Email OTP verifyIP20 / 10m
Email auth start, magic-link startIP; email10 / 10m; 5 / 10m
Magic-link verifyIP20 / 10m
Passkey registration start / finishuser10 / 1h; 30 / 10m
Passkey authentication start / finishIP20 / 10m; 30 / 10m
Passkey credential list / revokeuser60 / 1m; 20 / 10m
TOTP enrollment start / confirm, verifyuser10 / 1h; 20 / 10m; 20 / 10m
TOTP factor list / revokeuser60 / 1m; 20 / 10m
Recovery-code generate / regenerateuser10 / 1h each
Recovery-code verify / list / revokeuser20 / 10m; 60 / 1m; 20 / 10m
MFA optionsIP30 / 10m
MFA TOTP and recovery-code verify, including flow variantsIP20 / 10m each
MFA passkey start / verifyIP30 / 10m each
Step-up optionsuser60 / 1m
Step-up password, TOTP, recovery-code verifyuser20 / 10m each
Step-up passkey start / verifyuser30 / 10m each

The operation names and rule IDs are exported as AuthRateLimitStandardRules; use the exact operation keys shown there when configuring overrides.

Overrides and disabling

Configuration is per operation. An array replaces that operation's defaults rather than stacking with them. null disables policy for that operation. An omitted key (undefined) retains the standard rules. Use null only when another control deliberately owns the risk; AuthRateLimitNoopLive disables the entire service and is primarily useful for explicit replacement or tests.

AuthRateLimitStandardLive({
  "auth.password.sign_in": [
    {
      id: "app.password.sign_in.ip",
      key: "ip",
      limit: 8,
      window: Duration.minutes(15),
    },
  ],
  "auth.magic_link.verify": null,
});

Rules may select fixed-window (the default) or token-bucket, with tokens where appropriate. Email- and user-keyed rules require the operation to supply that subject; a missing subject fails closed with RateLimitStoreError. A missing IP uses the stable ip:missing bucket so requests without trusted IP metadata are still constrained. Rate-limit-store failures, exceeded limits, and privacy hashing failures propagate as typed errors and are mapped by HTTP Operations; the implementation does not silently fail open.

Privacy and keys

Raw email addresses and IP addresses are not used as limiter storage keys. PrivacyLive normalizes them and computes namespaced HMAC-SHA-256 hashes using the application privacy secret; RateLimitKey.emailHash and RateLimitKey.ipHash then identify the key kind. User rules use the internal UserId, not browser-supplied input. Configure proxy-header trust carefully so an attacker cannot choose the IP metadata used by policy.

RateLimiter owns policy composition and consumption. Its live implementation delegates persistence to Effect's persistence rate limiter, while RateLimiterMemoryLive is process-local. Choose durable storage when counters must survive process or isolate replacement.

Origin, CSRF, and CORS

AuthOriginCheckMiddleware is a request-side CSRF defense for unsafe methods (POST, PUT, PATCH, and DELETE). It accepts same-origin requests or normalized origins in allowedOrigins, using Origin and then Referer. Missing headers are allowed by default for non-browser clients; set allowMissingOrigin: false for strict browser-only boundaries. This check is separate from AuthRateLimit and separate from the double-submit AuthCsrfMiddleware primitive.

Origin/CSRF checks are not CORS. CORS controls which cross-origin responses browsers expose and which preflights succeed; configure its response headers independently, especially when credentials are used.

Finally, keep application authorization outside AuthRateLimit. Session, role, tenant, ownership, recent-step-up, and business-resource decisions belong in App-owned Guards or domain invariants. If you build directly from primitives, you also own transport validation, origin/CSRF policy, rate limiting, error projection, auditing, and cookie/session commitment that the HTTP Operations layer would otherwise provide.

On this page