Blame · Line-by-line history
api-auth-2fa.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 81807e1 | 1 | /** |
| 2 | * 2FA must hold on the API surface too. | |
| 3 | * | |
| 4 | * `sessions.requires_2fa` is set when the password step succeeds and cleared | |
| 5 | * only once the second factor is accepted (routes/auth.tsx:565, 674, 716). | |
| 6 | * middleware/auth.ts honours it in both its softAuth and requireAuth paths | |
| 7 | * (lines ~200 and ~273). | |
| 8 | * | |
| 9 | * middleware/api-auth.ts's session-cookie fallback checked only expiry. A | |
| 10 | * session that had passed the password step but not 2FA therefore | |
| 11 | * authenticated fully against every route using apiAuth — and was handed | |
| 12 | * ["repo","user","admin"] scopes — bypassing 2FA for the whole API surface. | |
| 13 | */ | |
| 14 | ||
| 15 | import { describe, expect, it } from "bun:test"; | |
| 16 | import { readFileSync } from "fs"; | |
| 17 | ||
| 18 | const API_AUTH = readFileSync("src/middleware/api-auth.ts", "utf8"); | |
| 19 | const AUTH = readFileSync("src/middleware/auth.ts", "utf8"); | |
| 20 | ||
| 21 | describe("api-auth session fallback honours requires2fa", () => { | |
| 22 | it("rejects a session still pending its second factor", () => { | |
| 23 | expect(API_AUTH).toContain("!session.requires2fa"); | |
| 24 | }); | |
| 25 | ||
| 26 | it("checks it alongside expiry, in the same guard", () => { | |
| 27 | // Guards against the check being added somewhere that a later `return` | |
| 28 | // can skip. | |
| 29 | const guard = API_AUTH.slice( | |
| 30 | API_AUTH.indexOf("// Fall back to session cookie"), | |
| 31 | API_AUTH.indexOf('c.set("authMethod", "session")') | |
| 32 | ); | |
| 33 | expect(guard).toContain("expiresAt"); | |
| 34 | expect(guard).toContain("!session.requires2fa"); | |
| 35 | }); | |
| 36 | ||
| 37 | it("does not grant scopes before the 2FA check", () => { | |
| 38 | const scopeIdx = API_AUTH.indexOf('c.set("tokenScopes", ["repo", "user", "admin"])'); | |
| 39 | const checkIdx = API_AUTH.indexOf("!session.requires2fa"); | |
| 40 | expect(checkIdx).toBeGreaterThan(-1); | |
| 41 | expect(scopeIdx).toBeGreaterThan(checkIdx); | |
| 42 | }); | |
| 43 | }); | |
| 44 | ||
| 45 | describe("the two middlewares agree", () => { | |
| 46 | it("both consult requires2fa", () => { | |
| 47 | // The bug was a divergence between them; keep them in step. | |
| 48 | expect(AUTH).toContain("session.requires2fa"); | |
| 49 | expect(API_AUTH).toContain("session.requires2fa"); | |
| 50 | }); | |
| 51 | }); |