POST /auth/refresh drops the role claim, breaking role-gated endpoints like POST /post after token refresh

Open
#960 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
74/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
node.js, typescript

Research direction

Start in src/server/routes/auth.ts and src/server/routes/post.routes.ts, then inspect src/server/plugins/jwt.ts and grep for other request.user.role reads. Trace the refresh-token flow and the role checks on POST /post and POST /post/:id/reply. Done means role-gated requests continue to work after token refresh and all equivalent role reads are covered.

Written by the indexing model from the issue text.

Description

Bug

Coordinators (and agents) are hitting 403 Only agents and coordinators can create posts. on POST /post in production, even though their account role is correct in the DB.

Repro (from user report, prod):

  • POST https://app.need4deed.org/api/post403 Forbidden
  • Response body: Only agents and coordinators can create posts.
  • Confirmed via browser network tab + console on app.need4deed.org/de/dashboard/posts.

Root cause

src/server/routes/auth.ts:

  • Login (line 85) signs the access token with the full payload including role:
    const userPayload = { id: user.id, email: user.email, role: user.role };
    
  • POST /auth/refresh (line 196) mints a new access token with role omitted entirely:
    const userPayload = { id: user.id, email: user.email };
    

Access tokens are short-lived (15 min per CLAUDE.md), so POST /auth/refresh fires automatically throughout any normal session. Once it fires once, every subsequent access token for that session has no role claim.

src/server/routes/post.routes.ts's POST /post and POST /post/:id/reply handlers read the role straight off the raw JWT payload:

const { role } = request.user;
if (role !== UserRole.AGENT && role !== UserRole.COORDINATOR) {
  throw new UnauthorizedError("Only agents and coordinators can create posts.");
}

With role now undefined post-refresh, this check fails for every role, not just coordinators — it just happens to be most visible for the roles that are supposed to be allowed to post.

Note the same handler pulls personId from request.authUser?.personId (the DB-authoritative user object the JWT plugin loads fresh on every request — see src/server/plugins/jwt.ts's own comment: "Expose the already-loaded user (carries personId + DB-authoritative role) for downstream hooks") rather than from request.user. So the fix belongs at the call site, not necessarily in the token itself: use request.authUser.role (already loaded, already correct) instead of request.user.role.

Suggested fix

Either (or both, in order of increasing scope):

  1. In post.routes.ts (and grep for other request.user.role reads gated the same way), switch to request.authUser.role — it's already fetched from the DB on every authenticated request and is never stale.
  2. Fix POST /auth/refresh to include role in the reissued access token's payload, so request.user.role isn't silently wrong for any other code that may rely on it.

Scope check

Worth a quick grep across the codebase for other handlers reading request.user.role (as opposed to request.authUser.role) — those would have the identical latent bug, just less visible than posting.

Dominant language
TypeScript
Stars
3
Forks
12
Avg merge
15h 57m
Merged PRs (30d)
58

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from need4deed-org/be

All issues in need4deed-org/be

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.