---
title: "Password and Email Risk"
url: "https://effect-auth.itsbroly.com/authentication/abuse-protection/password-email-risk/"
description: "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.

```mermaid title="Registration applies email and password policies before hashing and atomic storage"
flowchart LR
  I[Normalize identity] --> E[Email acceptance]
  E --> P[Password risk]
  P --> H[Hash password]
  H --> C[Atomic registration]
```

## Breached password policy [#breached-password-policy]

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

```ts title="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 [#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.

```ts title="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 [#wire-feature-layers]

```ts title="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.

| Operation            | Maintained order                                                                   |
| -------------------- | ---------------------------------------------------------------------------------- |
| Signup               | Size, normalize, email policy, password policy, hash/write                         |
| Reset                | Inspect challenge/account/credential, password policy, hash, atomic consume/update |
| Initial set          | Size, ensure absent credential, policy, hash/write                                 |
| Change               | Size, verify current credential, replacement policy, hash/write                    |
| Identity add/replace | Normalize, 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 policy                         | Behavior                             |
| --------------------------------------- | ------------------------------------ |
| `PasswordRiskPolicy.noopLayer`          | Accepts passwords without a provider |
| `EmailAcceptancePolicy.noopLayer`       | Accepts 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.

