# ADR 0004 — Account recovery and rate limiting

- Status: Accepted
- Date: 2026-08-19

## Context

Phase 6 completes the auth surface: forgot/reset password, activation resend,
and rate limiting on `/auth/**`.

## Decisions

### 1. Password reset kills all sessions

`POST /auth/password/forgot` always returns `202` (no enumeration): if the email
exists, prior `PASSWORD_RESET` tokens are invalidated and a fresh, short-lived
(1h) token is emailed; if not, nothing happens. `POST /auth/password/reset`
consumes the single-use token, sets the new Argon2id hash, and **revokes every
refresh token for the user** so all existing sessions die. The new password is
persisted with an explicit `saveAndFlush` *before* the bulk token/refresh revokes
run — those use `clearAutomatically`, which would otherwise detach the entity and
drop the change.

### 2. Activation resend mirrors registration's non-enumeration

`POST /auth/activation/resend` returns `202` regardless of state; it only acts
for an existing, still-unactivated account, invalidating prior activation tokens
and emailing a fresh one.

### 3. Rate limiting: in-memory token bucket, single-instance

`/auth/**` is rate-limited by a self-contained per-client-IP token-bucket filter
registered ahead of the security chain — no Bucket4j/Redis dependency. It is
therefore **single-instance only**: behind multiple replicas each instance limits
independently; a shared store would be needed for a global limit. This is
documented on the filter and in the runbook. Over-limit requests get a `429`
RFC 7807 problem (`RATE_LIMITED`). Capacity/refill are configurable
(`app.rate-limit.*`); the filter is disabled by default in the test suite (shared
MockMvc client IP) and exercised by a dedicated `RateLimitingIT`.

## Consequences

- `auth.service` coverage 100% line / 96% branch; PIT 80% across service packages.
- The auth API from §5 is now complete; Phase 7 moves to TLS.
- Follow-up worth noting: the in-memory bucket map is unbounded in the number of
  distinct client IPs; acceptable for this single-instance service, but a
  production multi-tenant deployment would add eviction or move to a shared store.
