Aman-Mittal opened a new pull request, #212:
URL: https://github.com/apache/fineract-consumer-facing/pull/212

   ## What and why
   
   Two logic flaws in the BFF's own authentication design, found by reviewing 
the auth, OTP, ABAC,
   open-banking and audit paths end to end. Both are in scope under 
SECURITY.md: neither is a
   "prototype lacks production hardening" finding.
   
   **One-time codes were not bound to a purpose.** The pending-code slot was 
keyed on the user alone
   (`otp:pending:{publicId}`), so the six ceremonies that mint codes — 
registration, login 2FA,
   password change, password reset, transfer step-up, beneficiary step-up — 
shared one slot, and
   whichever code was minted last satisfied whichever challenge was presented. 
The weakest challenge
   in the system therefore issued a credential for the strongest one, and `POST 
/api/v1/user/password/reset`
   is public and takes nothing but an email address, a code and a new password. 
`OtpPurpose` now joins
   both the Valkey key and the hashed material, so neither a slot read nor a 
lifted hash crosses
   ceremonies, and the delivery email states what the code authorises instead 
of being purpose-free.
   
   **The registration submit endpoint answered two different ways.** It 
verified the applicant's
   identity document before checking whether the Fineract client was already 
registered, so a correct
   government ID returned 409 and a wrong one 403 — on a public endpoint, 
against a sequential client
   id, with nothing counting the attempts. The already-registered check now 
runs first, so both cases
   answer identically, and `IdentityVerificationAttemptCounter` caps failures 
at five per client per
   fifteen minutes and reports a lockout as the same generic mismatch, so the 
refusal itself tells an
   attacker nothing. Per-IP throttling stays delegated to the deployment 
environment, as SECURITY.md
   and the docs already say.
   
   Two smaller things in the same area:
   
   - `POST /api/v1/registration/otp/send` performed no status check, so any 
public id presented to it
     got a fresh code emailed — including one belonging to a fully onboarded 
account. Both OTP steps
     now require `PENDING_OTP`.
   - `PendingOtp` carried the plaintext code and the destination address under 
a bare `@ToString`,
     against the project's own rule for secret-bearing data classes. Nothing 
logs it today; one
     careless `log.debug` would have.
   
   The review covered more than it changed. The ABAC enforcement across all 
thirty feature request
   mappings, the open-banking authorization server (redirect-URI matching, 
PKCE, consent-to-TPP-and-user
   binding, revocation, scope handling, token separation), the refresh-token 
lifecycle, the JWT denylist,
   the audit submission path and the error envelope all held up; no change was 
needed there, and the
   docs now record what was verified.
   
   ## Verification
   
   - `./gradlew test` — 561 unit tests, 0 failures. Includes the 36 ArchUnit 
and Spring Modulith checks,
     which cover the module boundaries and naming of the new classes.
   - `./gradlew cucumber` against the Docker stack (real Fineract, PostgreSQL, 
Valkey, Mailpit) —
     83 scenarios / 367 steps, all passing.
   - New Cucumber coverage: purpose binding in both directions (a login 
two-factor code refused at the
     password reset, a password reset code refused as a login second factor — 
both would have been
     accepted before), an already-registered client refused identically for a 
matching and a
     non-matching identifier, the identity lockout, and the OTP-send status 
guard.
   - New unit coverage: `RegistrationCommandServiceImplTest` (the order of the 
three submit checks, the
     lockout short-circuit, the `PENDING_OTP` guard on both OTP steps),
     `IdentityVerificationAttemptCounterTest` (cap, window, fail-open when 
Valkey is unreachable), and
     additions to `OtpServiceImplTest` / `OtpRepositoryTest` (a hash minted 
under one purpose does not
     validate under another, the same code hashes differently per purpose, one 
slot per purpose, key
     segments unique and `:`-free).
   - `node scripts/check-i18n.mjs` — passes; the new error code is translated 
in `en` and `hi`.
   - One existing scenario changed meaning and was retargeted rather than 
worked around: with the
     `PENDING_OTP` guard, replaying a verified registration code is refused by 
the state guard (409)
     before the spent code is looked at (400). It now asserts that refusal, and 
says where the
     single-use property it used to stand for is pinned instead 
(`OtpServiceImplTest`, `login.feature`).
   
   ## Screenshots
   
   Not applicable — no UI change beyond one new translated error code.
   
   ## Checklist
   
   - [x] I did not hand-edit generated files under 
`frontend/src/openapi-client/`.
   - [x] Backend code follows the feature and CQRS layout in AGENTS.md; the new 
counter is an
         `infrastructure` component beside `RateLimitCounter` and 
`ClientStandingChecker`, the new
         exception lives on the registration command side, and the architecture 
tests pass unchanged.
   - [x] I did not add entries to a lint or architecture baseline.
   - [x] User-facing strings use translation keys 
(`error.msg.consumer.registration.not.pending`, en + hi).
   - [x] I added or updated tests appropriate to this change.
   - [x] UI workflow changes include suitable e2e coverage, including 
real-backend testing where
         relevant — the behaviour changes are all server-side and covered by 
Cucumber against real Fineract.
   - [x] Commits are signed.
   - [x] I followed the AI-assisted contributions guidance.
   - [x] This is a bug fix, so the dev-list-first rule for features does not 
apply.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to