Aller au contenu

Story AUTH-010: Signed-link expiration (72h TTL) becomes effective

Module: identity Slice: arch §0.1 AUTH-006 (expiration wired) + arch §5.1 (TTL) Side: [BACKEND] Version target: V0 — v0.1.0 Priority: 10 Depends on: AUTH-005 Can develop concurrently with: AUTH-011, AUTH-012 Merge order: After AUTH-005 Estimated complexity: S PRD User Stories: AUTH-CP-05 (customer link expiry) Wireframe: N/A — backend only (the customer "expired" screen is served by the CP module; AUTH provides the state + French copy)


Objective

Through v0.0.0–v0.0.5 signed links never expire (a deliberate V0 simplification, arch §0.2). v0.1.0 is the production-hardening gate: the 72h TTL becomes effective. A link clicked after its TTL is rejected as EXPIRED, and the customer is told to ask their adviser for a new one. This aligns V0 with the V1 PRD §19 end-state for link lifetime.


Backend Scope

Entities

Uses signed_links.expires_at (already stored at issuance since AUTH-005) and signed_links.state (→ EXPIRED).

Service Layer

  • Enforce TTL on verifyLink: if now() > expires_at, return a terminal EXPIRED result, set state EXPIRED (from any non-terminal prior state), and audit. TTL is 72h (AUTH-NFR-03; the 48–72h band per arch §5.1).
  • The verify SQL filter now includes AND expires_at > NOW() (arch §7.1).
  • An expired click does NOT promote the file to an anomaly (TTL expiry is normal — R-CP-005).
  • Reactivation after a suspension does NOT extend an existing link's TTL (the original 72h stands — AUTH-ADM-06 rule); operator re-issue (V1) is the way to get a fresh link.
  • In-flight payment when the TTL expires: the running payment completes via PAY idempotency; no further interaction on the link (R-CP-007c).

API Endpoints

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

Validation Rules

  • TTL stored at issuance; enforced at verify time.
  • Expired state is terminal; an expired link cannot be re-activated by clicking.

Multi-Tenant Considerations

TTL enforcement is per signed_links row, tenant-scoped, on the platform datasource. No cross-tenant effect.

Audit & Logging

  • AUTH_LINK_REJECTED_EXPIREDcampaign_id, file_id, age_seconds_past_ttl.

Frontend Scope

N/A — backend only. The customer "expired link" screen is a CP story; AUTH supplies the state and the French copy below.

French copy provided to CP (design §4.4 / §10.1)

  • Titre: « Ce lien a expiré »
  • Corps: « Pour des raisons de sécurité, ce lien n'est plus valide. »
  • Sortie: « Contactez votre conseiller pour en recevoir un nouveau : 📞 {agencyPhone} »

UI States (ALL REQUIRED)

N/A — backend only.


Acceptance Criteria

AC-001: A link clicked past its TTL is rejected as EXPIRED
Given a signed link whose expires_at is in the past
When the customer clicks it
Then verifyLink returns the terminal EXPIRED result (copy "Ce lien a expiré ...")
And the state becomes EXPIRED
And AUTH_LINK_REJECTED_EXPIRED is audit-logged with age_seconds_past_ttl
And the file is NOT promoted to an anomaly
AC-002: A link clicked within its TTL still verifies
Given a signed link whose expires_at is in the future
When the customer clicks it
Then verifyLink returns VALID with the branding context (unchanged from AUTH-005)
AC-003: Reactivation does not extend an existing link's TTL
Given a link within its 72h TTL when the tenant was suspended
When the tenant is reactivated 6 hours later
Then the link's expires_at is unchanged (the original 72h stands)

Compliance Rules

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

Standards & Conventions

  • docs/standards/java-spring-guidelines.md, docs/standards/api-guidelines.md.
  • docs/standards/critical-rules.md (tenant isolation).

Testing Requirements

Unit Tests

  • now() > expires_at → EXPIRED; now() <= expires_at → VALID.
  • Expired state is terminal (re-click stays EXPIRED).

Integration Tests

  • Clock-advanced test: link past TTL → EXPIRED + AUTH_LINK_REJECTED_EXPIRED + no anomaly promotion.
  • Reactivation does not extend expires_at.

What QA Will Validate

  • Links older than 72h are rejected with the exact French copy; fresh links still work; expiry never creates an anomaly; reactivation does not extend TTL.

Out of Scope

  • Attempt-limit / lockout / permanent revoke — AUTH-011 (same sub-version, separate slice).
  • Redis rate-limiting — AUTH-012.
  • HMAC + anti-replay — V1 (arch §0.4).
  • Operator re-issue endpoint — V1 (AUTH-CP-08).

Definition of Done

  • [ ] TTL (72h) enforced at verify time; expired links return EXPIRED
  • [ ] Expired click does not promote the file to an anomaly
  • [ ] Reactivation does not extend an existing link's TTL
  • [ ] Exact French "expired" copy provided to CP
  • [ ] Audit entry AUTH_LINK_REJECTED_EXPIRED with age_seconds_past_ttl
  • [ ] Unit + integration tests written and passing
  • [ ] Tenant isolation verified (platform DB; tenant_id scoping)
  • [ ] No silent failures on verify