Story AUTH-007: Customer identity challenge — last-4 phone digits (with first-access capture)¶
Module: identity Slice: S5 reduced (arch §11) — V0 envelope AUTH-003 (arch §0.1, v0.0.2) Side: [BACKEND] Version target: V0 — v0.0.2 Priority: 7 Depends on: AUTH-005 Can develop concurrently with: AUTH-006 Merge order: After AUTH-005 Estimated complexity: M PRD User Stories: derived from V0 envelope line 26 (AUTH-003); related V1 challenge stories AUTH-CP-02 / AUTH-CP-04 (DOB/RCCM variants are V1) Wireframe: N/A — backend only (the challenge form is served by the CP module; AUTH provides the verification service + French copy)
Objective¶
v0.0.2 adds a light identity check before a customer can act on their contracts: they enter the last 4 digits of their declared phone number. If those digits were not present in the ingestion file, the customer captures them once on first access. This is the only V0 challenge factor — there is no OTP and no multi-channel (arch §0.1). The full DOB / RCCM challenge factors are V1.
Note: the design doc specifies exact French copy for the DOB and RCCM challenge variants but NOT for a dedicated "4 derniers chiffres" prompt (design §10 screen 10). The French strings below are derived for this V0.0.2 variant and must be confirmed by the DESIGNER / PRODUCT_OWNER (see index Open Questions).
Backend Scope¶
Entities¶
Reads signed_links (state, challenge_hmac). Appends to customer_auth_attempts (the append-only attempt log). May persist a first-access capture of the last-4 value on the relevant record (challenge_hmac comparison only — AUTH never stores the raw phone).
Service Layer¶
ChallengeVerificationService.submitChallenge(token, submittedValue, ip)(arch §10.5):- Normalize the submitted value (strip spaces; keep the 4 digits).
- Compare against the stored
challenge_hmacfor the link using a constant-time comparison; AUTH never holds the raw last-4 value, only the HMAC supplied at issuance (arch §2.2 note). - On success: open a server-side customer session bound to the link token (
CustomerSessionService), set stateAUTHENTICATED, audit success. - On failure: increment the attempt counter, append a FAILURE row to
customer_auth_attempts, audit the failure. v0.0.2: the counter is shown ("Tentative X/5") but the 5-attempt lockout / permanent-revoke is NOT enforced yet — that lands in v0.1.0 (AUTH-011), per arch §0.2 ("pas d'attempt-limit appliqué entre v0.0.0 et v0.0.5"). - First-access capture: if the file had no last-4 value at ingestion, the first access asks the customer to set it; the captured value is stored as the
challenge_hmac(HMAC, not raw) and used for subsequent checks. CustomerSessionService.getStatus(token, sessionToken)returns the session status (bearer = the link token).
API Endpoints¶
Reached via the CP module boundary (arch §3.4):
| Method | Path | Maps to | Auth |
|--------|------|---------|------|
| POST | /api/portail/{token}/challenge | ChallengeVerificationService.submitChallenge(token, value) | signed-link token |
| GET | /api/portail/{token}/session | CustomerSessionService.getStatus(token, sessionToken) | signed-link token |
Validation Rules¶
- Submitted value normalized to exactly 4 digits before comparison.
- Comparison is constant-time (R-CP-007).
- The submitted value is logged only as
HMAC-SHA256(challengeSecret, submitted || linkId)— never cleartext (arch §7.7).
Multi-Tenant Considerations¶
The challenge runs against a tenant-scoped signed_links row on the platform datasource. No cross-tenant exposure. The HMAC secret is platform-level config.
Audit & Logging¶
customer_auth_success—campaign_id,file_id.customer_auth_failure—campaign_id,file_id,attempt_index,attempt_value_hash(HMAC, NEVER the value).
Frontend Scope¶
N/A — backend only. The challenge form + first-access capture screen are CP stories; AUTH supplies the verification service and the (derived) French copy below.
French copy provided to CP (derived for the last-4 variant — to confirm with DESIGNER)¶
- Prompt: « Pour accéder à vos contrats, entrez les 4 derniers chiffres de votre numéro de téléphone. »
- Label: « 4 derniers chiffres du téléphone »
- First-access capture prompt: « Première connexion : confirmez les 4 derniers chiffres de votre numéro de téléphone. »
- Button: « Voir mes contrats »
- Wrong attempt (from design §10.1, adapted): « Les chiffres saisis ne correspondent pas. Vérifiez et réessayez. Tentative {n}/5. »
UI States (ALL REQUIRED)¶
N/A — backend only (the five customer states are implemented by the CP story; AUTH returns SUCCESS / FAILURE + attempt index + copy).
Acceptance Criteria¶
AC-001: Correct last-4 digits authenticate the customer
Given a valid signed link whose challenge_hmac matches the customer's last-4 phone digits
When the customer submits the correct 4 digits
Then a server-side customer session is opened (bound to the link token) and the link state becomes AUTHENTICATED
And customer_auth_success is audit-logged
AC-002: Wrong digits increment the counter and show "Tentative X/5"
Given a valid signed link
When the customer submits incorrect digits
Then the attempt counter increments and the failure is shown as "Tentative {n}/5"
And a FAILURE row is appended to customer_auth_attempts with the HMAC of the submitted value (never cleartext)
And customer_auth_failure is audit-logged
And in v0.0.2 the link is NOT locked out (lockout enforcement is v0.1.0)
AC-003: First-access capture when last-4 is missing from ingestion
Given a file that arrived without the customer's last-4 phone digits
When the customer accesses the link for the first time
Then they are asked to set their last-4 digits, which is stored as challenge_hmac (HMAC, not raw)
And subsequent challenge checks compare against the captured value
Compliance Rules¶
- CIMA Reg. 01-24 acceptability of the customer auth scheme for renewal consent + payment = open (AUTH-OQ-01, AUTH-TV-01) — working position only; lawyer to confirm before production.
- [LEGAL-REVIEW]: the challenge authenticates the customer; it is NOT used as proof of consent (red flag #5,
INCERTAIN). Keep that boundary. - Data minimisation: only the HMAC of phone digits / submitted value is stored, never the raw value (§C.2).
Standards & Conventions¶
docs/standards/java-spring-guidelines.md,docs/standards/api-guidelines.md.docs/standards/critical-rules.md(HMAC-only at rest; tenant isolation).docs/standards/connectivity-low-bandwidth.md(challenge round-trip budget).docs/standards/compliance-discipline.md(AUTH-OQ-01 working position).
Testing Requirements¶
Unit Tests¶
- Normalization to 4 digits; constant-time comparison.
- Submitted value persisted only as HMAC (no cleartext).
- v0.0.2: counter increments but no lockout state is set on the 5th failure.
Integration Tests¶
- Correct digits → AUTHENTICATED + session + customer_auth_success.
- Wrong digits → counter++ + customer_auth_failure; 5 wrong attempts do NOT lock the link in v0.0.2.
- First-access capture path stores challenge_hmac and authenticates on the next correct submission.
- Tenant isolation: challenge against a link from another tenant fails to resolve.
What QA Will Validate¶
- The last-4 challenge gates access; wrong attempts show "Tentative X/5"; first access can capture the digits; no raw phone value is stored anywhere; no lockout in v0.0.2.
Out of Scope¶
- 5-attempt lockout, 30-min lockout window, 3-cycle permanent revoke — v0.1.0 (AUTH-011).
- DOB / RCCM challenge factors — V1 (AUTH-CP-02 / AUTH-CP-03).
- OTP or multi-channel challenge — not in V0.
- The customer-facing React challenge + capture screens — CP module.
Definition of Done¶
- [ ] Correct last-4 digits authenticate and open a customer session (state AUTHENTICATED)
- [ ] Wrong digits increment the counter and surface "Tentative X/5" (no lockout in v0.0.2)
- [ ] First-access capture stores challenge_hmac (HMAC, never raw)
- [ ] Submitted values logged only as HMAC; no cleartext anywhere
- [ ] Audit entries customer_auth_success and customer_auth_failure (with attempt_value_hash)
- [ ] Unit + integration tests written and passing (incl. cross-tenant isolation)
- [ ] Tenant isolation verified (platform DB; tenant_id scoping)
- [ ] Derived French copy flagged for DESIGNER confirmation
- [ ] No silent failures on network errors