Aman-Mittal opened a new pull request, #372:
URL: https://github.com/apache/fineract-backoffice-ui/pull/372

   Closes #369
   
   ## Summary
   
   Enabling `fineract.security.2fa.enabled` made this application **unusable**. 
Sign-in reported
   success, the user landed on the dashboard, and every request from that 
moment answered 403 —
   including the settings screen that would have turned it back off. The only 
way out was a backend
   restart with an environment variable.
   
   The platform was asking a question nothing here read. `POST 
/v1/authentication` answers:
   
   ```jsonc
   {
     "authenticated": true,                      // ← what the client branched 
on
     "base64EncodedAuthenticationKey": "…",
     "permissions": ["ALL_FUNCTIONS"],
     "isTwoFactorAuthenticationRequired": true   // ← never inspected
   }
   ```
   
   `authenticated` was never the right field. Until a one-time token is 
validated the Basic credential
   opens `/v1/twofactor/**` and nothing else. Reproduced before and after, as 
`mifos` (who holds
   `ALL_FUNCTIONS`):
   
   | Request | 2FA off | 2FA on, before validating | after validating |
   |---|---|---|---|
   | `GET /v1/offices` | 200 | **403** | **200** |
   | `GET /v1/clients` | 200 | **403** | **200** |
   | `GET /v1/twofactor` | 404 | 200 | 200 |
   
   ## Changes
   
   **The session knows it is half-finished.** `isAuthenticated` stays false and 
a new
   `twoFactorPending` is true for exactly the window between the password being 
accepted and the
   one-time token being validated. `authGuard` therefore keeps the user on the 
sign-in page, and
   typing a URL does not get them past it — there is an e2e case for precisely 
that.
   
   **A second sign-in step** (`features/login/two-factor/`): choose a channel, 
receive a code, enter
   it. The choice collapses when the platform offers only one, which is the 
usual case. A refused code
   keeps the user there, clears the field, and shows Fineract's own reason — 
that arrives as a 403
   carrying `validation.msg.domain.rule.violation`, so it reads *"The provided 
one time token is
   invalid"* rather than a generic permissions message.
   
   **The token travels on every later request** as 
`Fineract-Platform-TFA-Token`, alongside
   `Authorization`; the platform wants both. Discovered empirically rather than 
assumed — the other
   two spellings I tried answered 403.
   
   **Signing out invalidates it at the platform** (`POST 
/v1/twofactor/invalidate`) rather than only
   forgetting it locally, where it would stay live for its full 24 hours.
   
   **Nothing changes where the platform asks for no second factor.** The flag 
is absent entirely, and
   that case is the regression guard in every layer of the tests.
   
   ## Contract, verified against a running Fineract
   
   | Step | Call | Answer |
   |---|---|---|
   | 1 | `POST /v1/authentication` | `isTwoFactorAuthenticationRequired: true` |
   | 2 | `GET /v1/twofactor` | `[{ "name": "email", "target": 
"[email protected]" }]` |
   | 3 | `POST /v1/twofactor?deliveryMethod=email` | `{ "tokenLiveTimeInSec": 
300, … }` |
   | 4 | `POST /v1/twofactor/validate?token=<OTP>` | `{ "token": "…", 
"validFrom": …, "validTo": … }` |
   | 5 | everything after | header `Fineract-Platform-TFA-Token` |
   | 6 | `POST /v1/twofactor/invalidate` | body `{ "token": "…" }` |
   
   The one-time code is **alphanumeric** (`NMKH4`), not digits — worth knowing 
before writing a `\d+`
   pattern against it.
   
   ## Testing
   
   | Command | Result |
   |---|---|
   | `npm run test` | **1057 SUCCESS**, 0 failed (grepped for `✘`/`FAILED`) |
   | `npx playwright test --project=mocked two-factor-authentication.spec.ts` | 
**10 passed** |
   | `npm run test:e2e:2fa` (real Fineract) | **3 passed** |
   | `npm run lint:prune` | pass — no new suppressions |
   | `npm run format:check` | pass |
   | `npm run i18n:check` | pass — 1527 keys, en/hi/ko |
   | `npm run typecheck:e2e` | pass |
   | `npm run check:route-permissions` | pass |
   | `npm run api:surface` / `ga:check` | pass — 8/9, **0 blocking** |
   | `./scripts/check-license.sh` | pass |
   | `zizmor` v1.28.0 over all workflows | **no findings** |
   | `npm run build` | pass |
   
   ### Mocked matrix — `e2e/two-factor-authentication.spec.ts`
   
   Asks for a code and admits on success · sends the header on every later 
request · keeps the user on
   the step when the code is wrong, with the platform's reason · accepts a 
correct code after a wrong
   one · offers the choice when there are several channels · says so when the 
account has none ·
   returns to the choice when the code could not be sent · **refuses a 
half-finished session by URL** ·
   backing out returns to the password form · **a deployment with no second 
factor signs in exactly as
   before**.
   
   ### Real backend — `e2e/two-factor-backend.spec.ts`
   
   Three cases against a Fineract that is genuinely demanding a factor, with a 
genuinely emailed code:
   the application stops for it; a real token completes sign-in **and a screen 
whose data comes from an
   endpoint the platform was refusing a moment ago then loads**; a wrong code 
is refused with the
   platform's reason.
   
   ## Infrastructure — a dedicated stack and CI job
   
   `fineract.security.2fa.enabled` is **process-wide**: with it on, every 
endpoint except
   `/v1/twofactor` answers 403 until a token is validated. So this cannot share 
an instance with the
   ordinary suite, and gets its own everything:
   
   - `deploy/docker-compose-e2e-2fa.yml` — the flag, plus a mail catcher
   - `scripts/e2e-stack-2fa.sh` — brings it up and points Fineract's SMTP at 
the catcher
   - a `two-factor` Playwright project, excluded from the default run
   - **a dedicated `E2E (two-factor, real Fineract)` CI job**
   
   The mail catcher is not a convenience. Fineract sends the code *before* 
persisting anything, so a
   deployment without a reachable SMTP server issues no token at all — `POST 
/v1/twofactor` answers
   500 and `twofactor_access_token` stays empty. Reading the mailbox stands in 
for the user's inbox;
   there is no database shortcut.
   
   ## Two things this turned up in existing code
   
   - **`landsOn`** waited for `#username`, which is *replaced* rather than 
hidden during the second
     step. It now waits on the login card. Would have broken any future test 
navigating mid-flow.
   - **The code input had to become a signal.** A plain field assignment does 
not mark an OnPush view
     dirty, so clearing it after a refusal left the rejected code on screen. My 
own spec caught it.
   
   ## Documentation
   
   `DOCS/TWO-FACTOR.md` — how the flow fits together, where the code lives, how 
to enable it locally,
   and how to run both test layers. Pointer added from `security.md`.
   
   ## Known limitations
   
   - **Email is the only channel the seeded platform offers.** The step renders 
whatever
     `GET /v1/twofactor` returns, so another channel is a Fineract 
configuration change rather than a
     change here — but only email has been exercised end to end.
   - **No "remember this device".** The platform supports it through 
`extendedToken`; the service
     takes the parameter and the UI never sets it, so every sign-in asks.
   - **The real-backend job costs a second Fineract boot** in CI, because of 
the process-wide switch.
     Three tests is a deliberate floor: the matrix lives in the mocked suite, 
which is free.
   
   ## Breaking changes
   
   None. A deployment that does not enable two-factor authentication sees no 
change — the platform
   never sets the flag, and the sign-in page never shows the extra step. That 
is asserted at the unit,
   mocked-e2e and real-backend layers.
   
   Deployments that *had* already enabled it were unusable and now work.
   


-- 
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