Aller au contenu

Story AUTH-011: Challenge attempt-limit, 30-min lockout, and 3-cycle permanent revoke

Module: identity Slice: S5 (arch §11) — V0 envelope AUTH-006 (arch §0.1, attempt-limit applied at v0.1.0) Side: [BACKEND] Version target: V0 — v0.1.0 Priority: 11 Depends on: AUTH-007 (challenge), AUTH-005 (signed-link state) Can develop concurrently with: AUTH-010, AUTH-012 Merge order: After AUTH-007 Estimated complexity: M PRD User Stories: AUTH-CP-04 (customer lockout cycle) Wireframe: N/A — backend only (the lockout / permanently-revoked customer screens are served by the CP module; AUTH provides the states + French copy)


Objective

Through v0.0.2–v0.0.5 the customer challenge counts attempts but never locks (deliberate, arch §0.2). v0.1.0 turns the limit on: after 5 wrong attempts the link locks for 30 minutes; after 3 such lockout cycles the link is permanently revoked, the file flips to an anomaly, and the operator is notified. This is the brute-force protection for the customer auth path, working together with the Redis rate-limit (AUTH-012).


Backend Scope

Entities

Updates signed_links (attempt_counter, cycle_counter, lockout_until, state, version). Appends to customer_auth_attempts.

Service Layer

  • Enforce the attempt limit in ChallengeVerificationService.submitChallenge (R-CP-010/011/012/013):
  • On the 5th consecutive wrong attempt: set lockout_until = now() + 30 min, cycle_counter++, state LOCKED; append a LOCKOUT_TRIGGERED attempt row; audit customer_auth_lockout_started.
  • During the lockout window: a click/submit consumes no attempt; return "réessayez dans X minutes" (R-CP-011).
  • On the first submit after the lockout window expires: if cycle_counter < 3, reset attempt_counter to 0/5 and re-present the challenge (audit customer_auth_lockout_resumed); else proceed to permanent revoke.
  • On cycle_counter == 3 (15 cumulative failures): set state PERMANENTLY_REVOKED, flip the file to anomaly AUTH_PERMANENTLY_LOCKED, notify the operator (file timeline + anomaly view), audit AUTH_LINK_PERMANENTLY_REVOKED(reason=MAX_LOCKOUT_CYCLES).
  • Optimistic concurrency on counter updates (arch §5.1, ADR-AUTH-03): UPDATE ... SET attempt_counter = attempt_counter + 1, version = version + 1 WHERE link_id = :id AND version = :expected. On 0 rows updated → re-read and retry. This guarantees concurrent multi-device submits never double-increment (exactly one lockout is triggered). The Postgres row is the system of record; no Redis-fronted counters in V1.

API Endpoints

None new. Reached via the CP boundary (POST /api/portail/{token}/challenge).

Validation Rules

  • 5 attempts per cycle; 30-min lockout; max 3 cycles → permanent revoke (AUTH-NFR-03).
  • Counters are authoritative server-side; a page reload during lockout shows a refreshed countdown (R-CP-002).

Multi-Tenant Considerations

All counters live on the tenant-scoped signed_links row on the platform datasource. The operator anomaly notification is routed to the link's own tenant/agency.

Audit & Logging

  • customer_auth_lockout_startedcampaign_id, file_id, cycle_index.
  • customer_auth_lockout_resumedcampaign_id, file_id, cycle_index.
  • AUTH_LINK_PERMANENTLY_REVOKEDcampaign_id, file_id, reason (MAX_LOCKOUT_CYCLES).

Frontend Scope

N/A — backend only. The lockout and permanently-revoked screens are CP stories; AUTH supplies the states and the French copy below.

French copy provided to CP (design §4.3 / §4.5 / §10.1)

  • Lockout titre: « Trop de tentatives »
  • Lockout corps: « Pour des raisons de sécurité, l'accès est mis en pause. Réessayez dans {dureeRestante}. »
  • Lockout sortie: « En cas de doute, appelez votre conseiller : 📞 {agencyPhone} »
  • Last-chance warning at 4/5 (design §10.1): « ⚠️ Encore une erreur et l'accès sera bloqué 30 minutes. »
  • Permanently revoked (design §4.5): « Pour des raisons de sécurité, ce lien a été désactivé. Contactez votre conseiller : 📞 {agencyPhone} »

UI States (ALL REQUIRED)

N/A — backend only.


Acceptance Criteria

AC-001: 5th wrong attempt triggers a 30-min lockout
Given a customer has failed 4 challenge attempts
When the 5th attempt is also wrong
Then the link enters a 30-min lockout (state LOCKED, lockout_until = now + 30 min, cycle_counter incremented)
And the customer sees "Trop de tentatives ... Réessayez dans {dureeRestante}."
And customer_auth_lockout_started is audit-logged
AC-002: After the lockout window the counter resets and the cycle increments
Given a link in lockout whose 30-min window has elapsed and cycle_counter < 3
When the customer returns
Then attempt_counter resets to 0/5, the challenge is re-presented
And customer_auth_lockout_resumed is audit-logged
AC-003: Three lockout cycles permanently revoke the link
Given the lockout-cycle counter reaches 3 (15 cumulative failures)
When the threshold is crossed
Then the link state becomes PERMANENTLY_REVOKED, the file flips to anomaly AUTH_PERMANENTLY_LOCKED, and the operator is notified
And the customer sees "Pour des raisons de sécurité, ce lien a été désactivé. Contactez votre conseiller ..."
And AUTH_LINK_PERMANENTLY_REVOKED(reason=MAX_LOCKOUT_CYCLES) is audit-logged
AC-004: Concurrent multi-device submissions do not double-increment
Given a link with attempt_counter = 4
When two devices submit a wrong value at the same time
Then optimistic concurrency ensures the counter ends at exactly 5 and exactly one lockout is triggered

Compliance Rules

  • CIMA 001-2024 art. 26 single-use link = VÉRIFIÉ; attempt-limit strengthens the security posture.
  • [LEGAL-REVIEW]: authentication mechanism, not proof of consent (red flag #5 unchanged).

Standards & Conventions

  • docs/standards/java-spring-guidelines.md (optimistic locking via version column).
  • docs/standards/critical-rules.md (tenant isolation; HMAC-only attempt values).
  • docs/standards/api-guidelines.md.

Testing Requirements

Unit Tests

  • 5th wrong → lockout; window-expiry reset (cycle<3); cycle==3 → permanent revoke.
  • During lockout, a submit consumes no attempt.

Integration Tests

  • Optimistic concurrency test (arch §13.4): two concurrent wrong submits on attempt_counter=4 → final counter exactly 5, state LOCKED, exactly one lockout event.
  • Permanent revoke flips the file to anomaly and notifies the operator.
  • Reload during lockout shows a refreshed (server-driven) countdown.

What QA Will Validate

  • The 5/30-min/3-cycle behaviour matches the rules; permanent revoke notifies the operator; concurrent devices cannot bypass the limit; all customer copy is exact French.

Out of Scope

  • Expiry / TTL — AUTH-010 (same sub-version, separate slice).
  • Redis rate-limiting — AUTH-012.
  • HMAC + anti-replay — V1.
  • The customer-facing React lockout / revoked screens — CP module.

Definition of Done

  • [ ] 5-attempt limit, 30-min lockout, and 3-cycle permanent revoke enforced
  • [ ] Lockout window consumes no attempt; post-window reset increments the cycle
  • [ ] Permanent revoke flips the file to anomaly + notifies the operator
  • [ ] Optimistic concurrency prevents double-increment on concurrent submits
  • [ ] Exact French lockout / revoked copy provided to CP
  • [ ] Audit entries: customer_auth_lockout_started, customer_auth_lockout_resumed, AUTH_LINK_PERMANENTLY_REVOKED
  • [ ] Unit + integration tests written and passing (incl. concurrency test)
  • [ ] Tenant isolation verified (platform DB; tenant_id scoping)
  • [ ] No silent failures