mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
fix(export): every whole-record export asks for a fresh proof
GET /api/export with type=all (the default), POST /api/export/health-record and GET /api/fhir/Patient/$everything hand over the whole record but took a live session alone, so a stolen browser session could still walk off with everything the full-backup gate was meant to protect. A browser session now needs a sign-in or re-proof within five minutes, with the second factor on an account that has one, and the doctor-report panel opens the re-proof dialog and retries. When a manager builds the report for a record they manage, their own session is the one asked. On a token the shipped app keeps working: health-record and $everything take the token as before, including a fhir:read token, and the recent-proof guard names both. GET /api/export?type=all, which no shipped client calls with a token, wants a step-up elevation there. Single-type exports are unchanged. The API reference now also describes the enrollment rule for accounts with a second factor and the passkey refusal it adds.
This commit is contained in:
1 parent
0692017f34
commit
a4d27c618e
13 files changed
+304
-76
No files matched your search
@@ -73,6 +73,8 @@ function callers(exportName: string): string[] {
|
||||
|
||||
/** Routes that resolve their own caller and take the recent-proof gate. */
|
||||
const RECENT_PROOF_ROUTES: Record<string, string> = {
|
||||
// `type=all` only; the single-type exports stay on `requireAuth`.
|
||||
"app/api/export/route.ts": "elevation",
|
||||
"app/api/export/full-backup/route.ts": "elevation",
|
||||
"app/api/export/encrypted/route.ts": "elevation-if-enrolled",
|
||||
// The shipped iOS app calls these two on its token without an elevation and
|
||||
@@ -82,6 +84,17 @@ const RECENT_PROOF_ROUTES: Record<string, string> = {
|
||||
"app/api/mcp/tokens/route.ts": "token",
|
||||
};
|
||||
|
||||
/**
|
||||
* Routes that resolve a cookie OR a token through their own resolver
|
||||
* (`requireAuth` with a scope, `requireRecordAuth`) and add the cookie arm
|
||||
* only. Their Bearer arm is the token, as with the `token` rule above: the
|
||||
* shipped app calls both with its token and no elevation.
|
||||
*/
|
||||
const TOKEN_ARM_COOKIE_PROOF_ROUTES = [
|
||||
"app/api/export/health-record/route.ts",
|
||||
"app/api/fhir/Patient/$everything/route.ts",
|
||||
].sort();
|
||||
|
||||
/** Cookie-only routes that add the gate after their own resolver. */
|
||||
const COOKIE_PROOF_ROUTES = [
|
||||
"app/api/admin/backups/[id]/download/route.ts",
|
||||
@@ -112,10 +125,20 @@ describe("the recent-proof gate", () => {
|
||||
expect(found).toEqual(RECENT_PROOF_ROUTES);
|
||||
});
|
||||
|
||||
it("the cookie arm is added by exactly the known cookie-only routes", () => {
|
||||
it("the cookie arm is added by exactly the known routes", () => {
|
||||
const found = callers("assertRecentCookieProof");
|
||||
expect(found.length).toBeGreaterThan(0);
|
||||
expect(found).toEqual(COOKIE_PROOF_ROUTES);
|
||||
expect(found).toEqual(
|
||||
[...COOKIE_PROOF_ROUTES, ...TOKEN_ARM_COOKIE_PROOF_ROUTES].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it("a route that resolves either transport asks only on the cookie", () => {
|
||||
for (const rel of TOKEN_ARM_COOKIE_PROOF_ROUTES) {
|
||||
expect(read(rel), rel).toMatch(
|
||||
/if \(auth\.authMethod === "cookie"\) \{\s*await assertRecentCookieProof\(/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("each cookie-only route resolves a cookie before it asks", () => {
|
||||
|
||||
Reference in new issue
Block a user