#906·midday

[Security] Removed team members retain JWT, OAuth, and API-key access

Author: frank-mendezCreated Aug 26, 2026Updated Aug 26, 2026

Summary

A team member removed through the admin team.deleteMember flow can continue using previously issued JWT, OAuth access-token, and API-key credentials against team-scoped REST endpoints.

Severity

High — authorization failure. A removed member may retain read/write access to the team until the relevant credential expires or is manually revoked.

Affected code

  • apps/api/src/rest/middleware/auth.ts:35-57 — JWT authentication loads the user and sets teamId without checking current team membership.
  • apps/api/src/rest/middleware/auth.ts:63-103 — OAuth authentication validates the token but does not verify that the token owner is still a member of the token's team.
  • apps/api/src/rest/middleware/auth.ts:109-160 — API-key authentication validates the key and user but does not verify current team membership.
  • packages/db/src/queries/teams.ts:425-445deleteTeamMember removes the usersOnTeam row but does not clear users.teamId or revoke team-scoped credentials.
  • packages/db/src/queries/oauth-flow.ts:227-268 — access-token validation checks token state and application state, but not team membership.
  • packages/db/src/queries/api-keys.ts:16-29 — API-key lookup validates the key hash, but not team membership.

Root cause

The credential middleware treats a valid JWT, OAuth token, or API key as sufficient authentication and derives the request team from the credential/user record. It does not perform a current usersOnTeam membership check.

The admin removal path only deletes the membership row. Unlike leaveTeam in packages/db/src/queries/teams.ts:356-380, it leaves the removed user's users.teamId unchanged and does not invalidate API keys or OAuth access/refresh tokens for that team.

Invalidating teamCache in apps/api/src/trpc/routers/team.ts:252-260 does not address this: the REST authentication middleware does not use that cache for its membership decision.

Reproduction

  1. Create team T with Alice as owner and Bob as a regular member.
  2. Have Bob obtain a valid session JWT, OAuth access token, and API key for team T.
  3. Alice removes Bob through team.deleteMember.
  4. Confirm Bob's usersOnTeam row is deleted.
  5. Reuse each credential against an authenticated, team-scoped REST endpoint.

Static verification on current main at commit 51587319f shows that the three middleware branches continue to accept the credentials and attach team context without a membership check. The admin removal query does not modify the credential records or clear users.teamId.

Impact

A removed employee, contractor, or compromised account can continue accessing or modifying team data through REST endpoints using credentials issued before removal. OAuth refresh tokens and API keys can also remain usable independently of the deleted membership row.

Recommended fix

  • Add a centralized current-membership check to REST authentication after validating each credential, using usersOnTeam and the credential's team.
  • Clear users.teamId when an administrator removes the user's only team membership.
  • Revoke or delete team-scoped API keys and OAuth access/refresh tokens when membership is removed, and invalidate related caches.
  • Add integration tests covering JWT, OAuth access/refresh tokens, and API keys before and after team.deleteMember.

Related code path

The leaveTeam flow already clears users.teamId; the admin removal flow should enforce equivalent authorization invalidation.