effect-auth

Password and Email Risk

Reject breached passwords and unwanted email domains through explicit policy.

Password and email risk run at credential/identity mutation points, not only HTTP routes, so alternate transports cannot bypass them.

Registration applies email and password policies before hashing and atomic storage

Breached password policy

PasswordRiskPolicy receives a redacted password and operation: sign-up, reset, set, or change. The HIBP adapter implements BreachedPasswordProvider.

src/server/password-risk.live.ts
import * as HibpPwnedPasswords from "@effect-auth/core/HibpPwnedPasswords";
import {
  BreachedPasswordProvider,
  PasswordRiskPolicy,
} from "@effect-auth/core/PasswordRisk";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

const HibpLive = HibpPwnedPasswords.layer({
  applicationId: "acme-auth-worker",
  cacheFailure: "fail",
  timeoutMs: 2_000,
  staleOnError: true,
});

export const PasswordRiskLive = Layer.effect(
  PasswordRiskPolicy,
  Effect.flatMap(BreachedPasswordProvider, (provider) =>
    PasswordRiskPolicy.make(provider, {
      occurrenceThreshold: 1,
      providerFailure: "fail-closed",
    })
  )
).pipe(Layer.provide(HibpLive));

The adapter computes SHA-1 locally for the HIBP range protocol and sends only the first five hexadecimal characters with response padding. SHA-1 is not used to store passwords. HIBP still observes the auth Worker's network endpoint, request timing, and prefix.

An optional HibpRangeCache is keyed only by prefixes and stores the returned range entries required for local suffix matching. The bundled memory cache is bounded; same-prefix requests are coalesced. For distributed Workers, provide an app-owned bounded cache and never persist the queried password, full hash, or its specific suffix separately.

Email acceptance policy

EmailAcceptancePolicy covers password signup and identity add/replace. The disposable-domain adapter uses an application-pinned dataset and performs no request-time download.

src/server/email-risk.live.ts
import { layer as disposableEmailDomainsLayer } from "@effect-auth/core/DisposableEmailDomains";
import { layerNoDeps as emailAcceptancePolicyLayerNoDeps } from "@effect-auth/core/EmailRisk";
import { Layer } from "effect";

const ReputationLayer = disposableEmailDomainsLayer({
  domains: pinnedDisposableDomains,
  allowlist: ["approved.example"],
  denylist: ["blocked.example"],
});

export const EmailRiskLayer = emailAcceptancePolicyLayerNoDeps({
  disposable: "deny",
  blocked: "deny",
  suspicious: "deny",
  outage: { mode: "fail" },
  operations: ["password-sign-up", "identity-add", "identity-replace"],
}).pipe(Layer.provide(ReputationLayer));

Allowlist wins over denylist/dataset. Exact parent matches cover subdomains. Record dataset source, license, revision, digest, and review process. Existing login is never blocked by a changed email policy.

Wire feature Layers

src/server/password.live.ts
import {
  PasswordDefaultLive,
  PasswordResetDefaultLive,
} from "@effect-auth/core/Password";

export const AppPasswordLive = PasswordDefaultLive(
  undefined,
  PasswordRiskLive,
  EmailRiskLayer
);

export const AppPasswordResetLive = PasswordResetDefaultLive(
  passwordResetOptions,
  PasswordRiskLive
);

Use IdentityManagementWithEmailAcceptanceLive for identity email mutations. Bare IdentityManagementLive does not apply email acceptance.

OperationMaintained order
SignupSize, normalize, email policy, password policy, hash/write
ResetInspect challenge/account/credential, password policy, hash, atomic consume/update
Initial setSize, ensure absent credential, policy, hash/write
ChangeSize, verify current credential, replacement policy, hash/write
Identity add/replaceNormalize, email policy for email only, mutation

Passwords over 1,024 UTF-8 bytes fail before provider lookup. A reset password rejected by policy leaves a valid unexpired challenge unconsumed, so the user may retry with another password. Password change verifies the current credential before calling the risk provider.

Explicit policyBehavior
PasswordRiskPolicy.noopLayerAccepts passwords without a provider
EmailAcceptancePolicy.noopLayerAccepts email without a provider
emailAcceptancePolicyLayerNoDeps(...)Uses the required EmailReputation

Construct explicit production policy when these defaults do not match requirements. Test all operation kinds, provider failures, HIBP prefix-only transport/cache bounds, domain parent/IDNA matches, allowlist precedence, reset retry, and absence of sensitive provider data from telemetry.

On this page