feat(fpagent): authenticate the wizard's a2loft calls with the session token

The fingerprint wizard's staff-lookup and fingerprint-save proxies forwarded the page's OTP with the session phone. a2loft resolves that phone against the domain being addressed, so an operator who holds only capture_biometric was rejected unless they were a domain super-user. The cropper already saves with the session token.

- tools/server.js: both a2loft proxies send Authorization: Bearer <session token>, no phone/otp; validateProxy drops the OTP and phone checks.
- tools/public/assets/fp: both OTP fields, otpLookup/otpSave state, request params and resets removed; canLookup/canSave no longer gate on OTP.
- Tests assert the Bearer header, no query auth, no OTP field in the served page, and both gates without a credential (66/66 pass).
- README and Help page updated.

Requires the a2loft fingerprint-token-auth change deployed first. OpenSpec change: fingerprint-token-auth.
This commit is contained in:
2026-10-02 20:41:43 +01:00
parent 548e33390c
commit 14f6fd6a8e
13 changed files with 262 additions and 67 deletions
@@ -0,0 +1,49 @@
# Design
## Context
See proposal.md for motivation. The wizard already holds the PocketBase token
in the page (`window.__SESSION__.token`) for fpagent enrollment, and the
cropper's save already sends `Authorization: Bearer ${req.session.token}` to
a2loft through its own proxy (`tools/cropper.js`). The two remaining OTP calls
are the wizard's staff-lookup and fingerprint-save proxies in `tools/server.js`;
their handlers receive the a2loft URL from `a2loftUrl`/`a2loftSaveUrl`, which
currently append `phone`+`otp`, and `validateProxy` rejects a missing `otp`.
## Goals / Non-Goals
**Goals:** both wizard a2loft calls use the session token; no OTP in the
wizard; proxy validation keeps rejecting unauthenticated calls; deployable
after the a2loft change with a clean rollback.
**Non-Goals:** fpagent capture (already carries the token); the cropper; a2loft's
own OTP compatibility; the legacy singular fingerprint endpoint.
## Decisions
1. **Bearer from the server session**, never the browser: the proxy attaches
`Authorization: Bearer ${req.session.token}`, mirroring `tools/cropper.js`.
The page's token copy stays for fpagent only.
2. **Drop the OTP check in `validateProxy`**, along with the now-unnecessary
phone check; keep the hardcoded-domain and reference checks. `requireApi`
already guarantees a session.
3. **Remove the OTP inputs and state** rather than hiding them;
`canLookup`/`canSave` no longer require an OTP.
## Risks / Trade-offs
- [a2loft not yet dual-mode] → deploy the a2loft change first; this proxy change
depends on it.
- [Operators outside a domain's uploader list can now enroll] → intended;
a2loft authorizes `capture_biometric` with the `domain:idid` fallback.
- [Rollback] → revert the proxy and re-add the OTP fields; a2loft stays
dual-mode.
## Migration Plan
Deploy a2loft dual-mode → deploy this app → field pass: look up a reference and
save both fingerprints with no OTP; a bogus-token call fails cleanly.
## Open Questions
None.