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:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-02
|
||||
@@ -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.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Proposal: fingerprint-token-auth
|
||||
|
||||
## Why
|
||||
|
||||
The Fingerprint Agent wizard's two a2loft calls — staff lookup and fingerprint
|
||||
save — prove the operator with a phone number and an OTP forwarded through
|
||||
same-origin proxies. a2loft resolves that phone against the domain being
|
||||
addressed, so an operator who holds the OpenFGA `capture_biometric` relation
|
||||
but is not a super-user of that domain's uploader list is rejected. The cropper
|
||||
already saves with the session's PocketBase token; the wizard's a2loft calls
|
||||
should use the same token flow.
|
||||
|
||||
## What Changes
|
||||
|
||||
- The `/api/:domain/staff/:reference` and
|
||||
`/api/:domain/staff/:reference/fingerprints` proxies send the session's
|
||||
PocketBase token as an `Authorization: Bearer` header instead of forwarding
|
||||
`phone`+`otp` query parameters.
|
||||
- The OTP validation is removed from the proxies; the session token is the
|
||||
credential.
|
||||
- The wizard's lookup and save steps drop their OTP fields and OTP state.
|
||||
- The Fingerprint Agent and tools-app specs, the README, and the Help page are
|
||||
updated.
|
||||
- Requires the a2loft `fingerprint-token-auth` change deployed first.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
(none)
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `fingerprint-wizard`: the two-finger enrollment flow and the per-call OTP
|
||||
requirement become token-authenticated.
|
||||
- `tools-app`: the a2loft proxies requirement now injects the session token
|
||||
instead of forwarding `phone`+`otp`.
|
||||
|
||||
## Impact
|
||||
|
||||
- `tools/server.js`: the two a2loft proxy handlers and `validateProxy`.
|
||||
- `tools/public/assets/fp/index.html`, `tools/public/assets/fp/app.js`: the OTP
|
||||
fields, state and request parameters removed.
|
||||
- `tools/test/server.test.js`: proxy assertions.
|
||||
- Documentation: README and the Help page.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Spec Delta: fingerprint-wizard
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Two-finger enrollment flow
|
||||
The wizard SHALL enroll a profile by: selecting a permitted domain, looking
|
||||
up staff by reference (token-validated through the a2loft proxy), displaying
|
||||
the staff details, capturing the right thumb and right index finger, and
|
||||
saving both fingerprints through the a2loft proxy authenticated with the
|
||||
session token. The wizard SHALL offer per-finger recapture until save, SHALL
|
||||
require both fingers before save, and SHALL reset domain-independent state
|
||||
(reference, staff details, captured artifacts) when starting a new
|
||||
enrollment.
|
||||
|
||||
#### Scenario: Complete enrollment
|
||||
- **WHEN** the operator looks up staff, captures both fingers, and saves
|
||||
- **THEN** both fingerprints are saved through the a2loft proxy and the
|
||||
wizard reports success
|
||||
|
||||
#### Scenario: Save requires both fingers
|
||||
- **WHEN** the operator attempts to save with only one finger captured
|
||||
- **THEN** the wizard rejects the save and explains both fingers are
|
||||
required
|
||||
|
||||
#### Scenario: New enrollment resets state
|
||||
- **WHEN** the operator starts a new enrollment after a save
|
||||
- **THEN** the reference, staff details, and captured finger artifacts are
|
||||
cleared while the permitted domain list persists
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: OTP validated on every a2loft call
|
||||
**Reason**: a2loft's fingerprint staff-lookup and save endpoints now accept the
|
||||
session's PocketBase token, so the operator proves identity with the session
|
||||
instead of a per-call OTP. The OTP scoped operators to the domain's uploader
|
||||
list and rejected capture agents who hold only `capture_biometric`.
|
||||
**Migration**: the wizard sends the session token through the same-origin
|
||||
proxy and collects no OTP; a2loft keeps OTP mode for other clients.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Token authenticated on every a2loft call
|
||||
The wizard SHALL authenticate each a2loft call — staff lookup and fingerprint
|
||||
save — with the signed-in operator's PocketBase session token, sent by the
|
||||
same-origin proxy as an `Authorization: Bearer` credential. The wizard SHALL
|
||||
NOT collect an OTP.
|
||||
|
||||
#### Scenario: Lookup uses the session token
|
||||
- **WHEN** the operator submits a reference
|
||||
- **THEN** the wizard calls the staff-lookup proxy and a2loft authenticates the
|
||||
session token
|
||||
|
||||
#### Scenario: Save uses the session token
|
||||
- **WHEN** the operator saves the captured fingerprints
|
||||
- **THEN** the wizard calls the fingerprint-save proxy and a2loft authenticates
|
||||
the session token
|
||||
|
||||
#### Scenario: No OTP field
|
||||
- **WHEN** the wizard page is served
|
||||
- **THEN** it offers no OTP input for lookup or save
|
||||
@@ -0,0 +1,26 @@
|
||||
# Spec Delta: tools-app
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: a2loft proxies inject operator identity
|
||||
The app SHALL proxy staff lookup and fingerprint-save calls to
|
||||
`https://apis.a2loft.net` under same-origin API routes. The proxy SHALL
|
||||
authenticate the operator with the signed-in session's PocketBase token, sent
|
||||
as an `Authorization: Bearer` credential, preserving the a2loft request and
|
||||
response shapes. The proxies SHALL require a session; a request without one
|
||||
SHALL be rejected without contacting a2loft. The proxies SHALL NOT forward or
|
||||
require an OTP.
|
||||
|
||||
#### Scenario: Staff lookup via proxy
|
||||
- **WHEN** the page requests staff details for a domain and reference
|
||||
- **THEN** the app calls a2loft's identity endpoint with the session token as a
|
||||
bearer credential and returns a2loft's response unchanged
|
||||
|
||||
#### Scenario: Fingerprint save via proxy
|
||||
- **WHEN** the page saves fingerprints for a domain and reference
|
||||
- **THEN** the app posts them to a2loft's fingerprint endpoint with the session
|
||||
token as a bearer credential
|
||||
|
||||
#### Scenario: Unauthenticated proxy call rejected
|
||||
- **WHEN** a proxy route is called without a valid session
|
||||
- **THEN** the app rejects the call without contacting a2loft
|
||||
@@ -0,0 +1,29 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Proxies
|
||||
|
||||
- [x] 1.1 `tools/server.js`: `a2loftUrl`/`a2loftSaveUrl` drop `phone`/`otp`;
|
||||
both handlers send `Authorization: Bearer ${req.session.token}`;
|
||||
`validateProxy` drops the OTP/phone checks and keeps domain + reference.
|
||||
Verified: the proxy tests assert the Bearer header and no query auth.
|
||||
- [x] 1.2 Update the proxy tests: assert no `?otp`, assert the Bearer header,
|
||||
and drop the no-OTP rejection case while the auth/domain rejection remains
|
||||
(no a2loft call). Verified.
|
||||
|
||||
## 2. Wizard UI
|
||||
|
||||
- [x] 2.1 `tools/public/assets/fp/index.html`: remove the lookup and save OTP
|
||||
inputs.
|
||||
- [x] 2.2 `tools/public/assets/fp/app.js`: remove `otpLookup`/`otpSave`, the
|
||||
`otp` request params, and the OTP resets; tighten `canLookup`/`canSave`.
|
||||
Verified: the served page has no OTP field (server test) and both gates pass
|
||||
without a credential (wizard test).
|
||||
- [x] 2.3 Update the `fingerprint-wizard` and `tools-app` spec deltas, README
|
||||
steps 1/3, the a2loft proxy paragraph, and the Help page.
|
||||
|
||||
## 3. Verify
|
||||
|
||||
- [x] 3.1 Run the full tools test suite (`node --test`) and confirm it passes.
|
||||
(66/66 pass.)
|
||||
- [ ] 3.2 Field pass against a real domain (a2loft deployed): look up a
|
||||
reference and save both fingerprints with no OTP.
|
||||
Reference in New Issue
Block a user