Skip to content

fix(j0di3): Capture and forward X-Troop-Token on troop-scoped calls#1136

Merged
jeromehardaway merged 1 commit into
masterfrom
fix/j0di3-troop-access-token
May 28, 2026
Merged

fix(j0di3): Capture and forward X-Troop-Token on troop-scoped calls#1136
jeromehardaway merged 1 commit into
masterfrom
fix/j0di3-troop-access-token

Conversation

@jeromehardaway

Copy link
Copy Markdown
Contributor

Summary

Production is returning 403 TROOP_ACCESS_DENIED on every J0dI3 call because the backend added a per-troop session header — X-Troop-Token — that our proxy doesn't send. This PR captures the token at troop registration, backfills it for existing users, and forwards it on every troop-scoped request.

Root cause

The J0dI3 backend's _check_troop_access middleware now requires two headers on troop-scoped resources:

  • X-API-Key (unchanged) — service identity, already sent.
  • X-Troop-Token (new) — per-user troop session, missing.

Admin-scoped clients skip the check, which is why admin endpoints aren't affected.

The token is minted once on POST /api/v1/troops/ (returned as access_token) and can be re-minted via POST /api/v1/troops/{troop_id}/access-token/rotate (admin scope).

Fix

  • Schema (prisma/schema.prisma + migration) — adds User.troopAccessToken String?.
  • ensureTroop (src/lib/ensure-troop.ts) — captures access_token on new registration. For existing users with troopId but no token, calls the admin rotate endpoint once and persists. Reads access_token/troop_access_token/token defensively in case backend naming differs.
  • requireAuth (src/lib/rbac.ts) — loads troopAccessToken from DB and exposes as req.user.troopToken.
  • j0di3Proxy dispatch (src/lib/j0di3-proxy.ts) — injects X-Troop-Token on troop-scoped calls. Returns 400 with a re-auth hint if the token is missing. j0di3AdminProxy skips the header.

Backfill plan

ensureTroop is fired from the NextAuth session callback (src/pages/api/auth/options.ts:125) on every session refresh, so existing users self-heal transparently — no manual migration script, no forced sign-out. Worst case: a user's first J0dI3 call after deploy still 400s; the next session refresh rotates a token and subsequent calls succeed.

Verification

  • npm test — 382/382 passing (added 2 new tests: rotate-on-backfill, missing-token 400 path)
  • npm run lint — clean on touched files
  • npm run typecheck — only the same pre-existing errors as master (swagger / 4 test files)

Test plan

  • Apply migration to a staging Postgres: npx prisma migrate deploy adds the troopAccessToken column
  • Sign in as a user with an existing troopId but no troopAccessToken → next request to /api/j0di3/troops/dashboard succeeds (after session refresh fires ensureTroop → rotate)
  • Sign in as a brand-new user → ensureTroop registers and stores token in the same Prisma update
  • Verify /api/j0di3/troops/dashboard request has X-Troop-Token header in upstream call (check backend logs or proxy with a probe)
  • Verify /api/j0di3/admin/* calls do NOT send X-Troop-Token (admin scope)
  • If a user's token gets revoked, the response is 403 from J0dI3 (forwarded by dispatch) — user can sign out and back in to re-rotate

Notes

  • This PR does not change the API-key header (X-API-Key stays). The 403 was purely about the missing troop token.
  • The ensureTroop field-name fallback (access_token || troop_access_token || token) is defensive — drop to just access_token once the backend response shape is confirmed.

J0dI3 now requires X-Troop-Token (per-troop session token) on every
troop-scoped resource alongside the existing X-API-Key. Without it,
every /api/j0di3/* call returns 403 with TROOP_ACCESS_DENIED.

- Schema: add User.troopAccessToken with migration.
- ensureTroop: capture access_token from POST /troops/ registration.
  For existing users with troopId but no token, rotate one via
  POST /troops/{id}/access-token/rotate (admin-scoped, uses our API
  key) on next session callback — transparent self-heal.
- requireAuth: load troopAccessToken from DB, expose as
  req.user.troopToken.
- j0di3Proxy dispatch: inject X-Troop-Token header on troop-scoped
  calls; return 400 with a re-auth hint if the token is missing.
  j0di3AdminProxy skips the header — admin scope bypasses the check.
@vercel

vercel Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vets-who-code-app Ready Ready Preview, Comment May 28, 2026 12:13pm

Request Review

@jeromehardaway jeromehardaway merged commit 7e6b737 into master May 28, 2026
6 checks passed
@jeromehardaway jeromehardaway deleted the fix/j0di3-troop-access-token branch May 28, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant