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: ifnow() > expires_at, return a terminalEXPIREDresult, set stateEXPIRED(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_EXPIRED—campaign_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