deckhearth/.convoys/fix-auth-bypass/brief-2-remove-admin-bypass.md
Randall Stillwell f64a80ba2a docs: post-convoy cleanup for fix-auth-bypass
Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through
1629afb) on the docs side. Code already on main; this PR is docs only.

Updates:

AGENTS.md
  - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic
    admin, login/register rate limit)
  - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js
  - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place
    (NOT renumbered, to preserve cross-references)
  - new #12 documents the KV_REST_API_* env-var convention
  - §5 setup list adds the rate-limit env vars
  - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate)

.cursor/rules/auth-and-permissions.mdc
  - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js
  - token model now 24h (was 7d) with fail-loud explanation
  - server-side authorization patterns lead with null → 401 contract

.cursor/rules/api-routes.mdc
  - removes the "CRITICAL — known bug" callout (resolved by Brief 2)
  - adds a "Rate limiting" section with verbatim shape + env-var notes
  - "Dev/test endpoints" → "Removed" historical note so future agents
    searching for test-db understand why it's gone

.convoys/fix-auth-bypass.md (restored — was on convoy branch only)
  - frontmatter → status: shipped
  - new "Convoy outcome" section: briefs + commits + resolved gotchas,
    R1-R12 risk walk, env-var-rename deviation record, queued follow-up
    convoys, lessons learned

.convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch)
  - audit-trail completeness; convoy plan references them by name
  - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_*
    across init rules, smoke, pre-deploy checklist
  - brief 4 has a new "Post-merge addendum" explaining the rename

.convoys/ship-readiness.md
  - P0 #1, #2, #4 → RESOLVED with merge-commit citations
  - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers
    (cors-tighten and add-rate-limiting convoys)
  - each item gains an "As-shipped" line for self-containment

README.md
  - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep
  - auth + rate-limit + testing bullets updated
  - env-var template extended with KV_REST_API_*
  - deleted dev-endpoints note added to the API list
  - "Default Admin Account" section LEFT ALONE — drop-public-setup territory

Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass,
lint baseline unchanged (128/81/47).

Convoy: fix-auth-bypass / role-doc-writer (closeout)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 11:27:48 -05:00

6.9 KiB

convoy brief_number depends_on files cross_brief_commitments
fix-auth-bypass 2
1
lib/permission-middleware.js
pages/api/auth/verify.js
brief description
1 Brief 1 already replaced the `JWT_SECRET` literal in both files with imports from `lib/auth-secret.js`. This brief preserves those imports and only removes the synthetic-admin fallback shapes.
brief description
5 Brief 5 (vitest) writes the unit tests that prove `getUserFromRequest` returns `null` for the four shapes (missing header, malformed token, expired token, valid token-but-no-user-row). The behavior is implemented here; the harness lands in Brief 5.

Brief 2: Remove the synthetic-admin bypass

Goal (1 sentence)

Make lib/permission-middleware.js::getUserFromRequest return null for any unauthenticated request, and make pages/api/auth/verify.js return 401 instead of fetching admin@tcgvault.com when no Bearer token is present.

Files in scope (do not edit anything else)

  • lib/permission-middleware.js — modified
  • pages/api/auth/verify.js — modified

Conventions to follow

  • .cursor/rules/auth-and-permissions.mdc § "Server-side authorization patterns" — if (!user) return res.status(401) pattern. The 24 callers of getUserFromRequest already follow this; we just need to make the helper actually emit null.
  • .cursor/rules/api-routes.mdc § "Error handling" — keep the try/catch wrapper in place; do not throw out of the handler.
  • .cursor/rules/no-go-zones.mdc — do not touch any other file.

Acceptance criteria

lib/permission-middleware.js::getUserFromRequest

  • Delete lines 14-17 of the post-Brief-1 file (the console.warn and the synthetic admin return). Replace with a plain return null. Verbatim shape:
// before (post-Brief-1, with literal already gone):
if (!authHeader || !authHeader.startsWith('Bearer ')) {
  // For development, return user ID 1 if no token (should be removed in production)
  console.warn('⚠️  Development mode: Using fallback user authentication');
  return { userId: 1, email: 'admin@tcgvault.com', role: 'admin' };
}

// after:
if (!authHeader || !authHeader.startsWith('Bearer ')) {
  return null;
}
  • No console.warn. No comment-out. No env-gate (NODE_ENV === 'development'). The fallback is gone, period. If a developer needs an authenticated session locally, they log in.
  • The rest of getUserFromRequest (token verify, DB lookup, error catch) is unchanged.
  • The catch block at lines 38-41 stays:
} catch (error) {
  console.error('Error getting user from request:', error);
  return null;
}

This means JWT verification errors (expired, malformed, bad signature) AND DB errors all collapse to null. The 401 vs 500 distinction is left to callers (currently every caller treats null as 401, which is correct for an auth helper).

  • No change to checkCollectionPermission, withCollectionPermission, checkRolePermission, or logCollectionActivity.

pages/api/auth/verify.js

  • Delete lines 25-39 of the post-Brief-1 file (the // For development, return admin user if no token provided block and the SELECT … WHERE email = 'admin@tcgvault.com' query). Replace with an immediate 401:
// before:
if (!authHeader || !authHeader.startsWith('Bearer ')) {
  // For development, return admin user if no token provided
  // In production, this should return 401
  const result = await sql`
    SELECT id, email, role, created_at
    FROM users
    WHERE email = 'admin@tcgvault.com'
  `;

  if (result.rows.length > 0) {
    return res.status(200).json(result.rows[0]);
  } else {
    return res.status(401).json({ error: 'No admin user found' });
  }
}

// after:
if (!authHeader || !authHeader.startsWith('Bearer ')) {
  return res.status(401).json({ error: 'Authentication required' });
}
  • No env-gate. No comment-out.
  • The rest of verify.js (CORS headers, OPTIONS preflight, method gate, JWT verify, DB lookup) is unchanged. CORS tightening is Brief 4's scope and only covers login.js + register.js, NOT verify.js (out of scope per the convoy).
  • The error-message wording matches the existing convention: { error: 'Authentication required' }. Do not invent a new shape.

Caller spot-check (do this before opening the PR)

The convoy claims "30+ handlers depend on getUserFromRequest." Re-verify by running the following (results captured at architect time on 2026-05-23 — master revision ebd4fd1; if the count drifts, list the new files in the PR description):

  • rg "getUserFromRequest" pages/api --type js -l | wc -l → 24 files (one of which is permission-middleware.js's import-bookkeeping artifact, leave the count as-is).
  • rg "if \(!user\)" pages/api --type js -A 1 (with -A 1) — every match must be followed by return res.status(401).json({ error: 'Authentication required' }); or a similar 401. If any caller has a different shape (e.g. if (!user) return res.status(403), or if (user) ... inverted, or no null guard at all), stop and re-architect: that caller would need behavioral changes, and this convoy explicitly does not touch caller code.
  • One file is known to use optional-chaining instead of an early 401 — pages/api/collections/[identifier].js uses user?.userId because it allows anonymous access to public collections. This is intentional and stays correct under the fix (when user is null, user?.userId is undefined, the public-collection branch still works). Do not "fix" it.

Smoke (manual)

  • npm run dev; with no Authorization header, hit curl http://localhost:3000/api/user/profile → expect HTTP 401 with body {"error":"Authentication required"}. (Pre-fix: returns the admin user's profile.)
  • Same with curl http://localhost:3000/api/auth/verify → expect HTTP 401. (Pre-fix: returns admin user data.)
  • Log in via the UI; observe the dashboard loads (the helper still works for valid tokens).
  • Log out; observe the dashboard redirects to /login (the helper now correctly returns null).

Out of scope

  • No edits to any of the 24 callers — they already handle null correctly.
  • No CORS changes (Brief 4).
  • No rate-limit (Brief 4).
  • No tests — Brief 5 ships them.
  • No AGENTS.md / .cursor/rules/*.mdc updates — doc-writer pass.

Rationale (≤3 sentences)

This is the convoy's actual security fix — removing the synthetic admin makes 24 currently-broken handlers correct in one ~6-line change. Folding verify.js's parallel bug (the no-token branch fetches admin@tcgvault.com directly from the DB) into the same brief keeps "the auth helper returns null" and "the verify endpoint returns 401" coupled, since both have to land before any unauthenticated request can be safely served. Splitting them risks a deploy ordering where one is fixed and the other isn't — exactly the inconsistency that lets a P0 ship-blocker survive.