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]