Files
twenty/packages
Sonarly Claude Code c457a0266e Renewed workspace-agnostic refresh tokens become unverifiable due to workspaceId: null in JWT
https://sonarly.com/issue/9509?type=bug

Workspace-agnostic refresh tokens that have been renewed once embed `workspaceId: null` in their JWT payload, making all subsequent renewal attempts fail with "Invalid token type". Combined with a frontend race condition on OAuth redirects, this locks returning users out of the app.

Fix: ## Two-layer fix for workspace-agnostic refresh token renewal failure

### Problem
After one token renewal cycle, workspace-agnostic refresh tokens embed `workspaceId: null` in their JWT payload. On the next renewal attempt, `verifyJwtToken` in `jwt-wrapper.service.ts` uses the `in` operator (`'workspaceId' in payload`) which returns `true` even when the value is `null`, selecting `null` as `appSecretBody`. Since `isDefined(null)` is `false`, the token is rejected with "Invalid token type" — permanently locking the user out after their first renewal.

### Fix 1: Prevent new bad tokens (`renew-token.service.ts`)
Changed `workspaceId` to `workspaceId ?? undefined` when calling `generateRefreshToken`. Since `undefined` values are omitted by `JSON.stringify` during JWT signing, the renewed refresh token will not contain a `workspaceId` key at all — matching the structure of the original first-generation token.

### Fix 2: Handle existing bad tokens (`jwt-wrapper.service.ts`)
Added `isDefined(payload.workspaceId)` to the `'workspaceId' in payload` check, so tokens already in the wild with `workspaceId: null` will correctly fall through to `userId` as the secret body. This is backward-compatible: tokens with a valid `workspaceId` string are unaffected.

### Test
Added a spec covering the workspace-agnostic renewal case with `workspaceId: null` from the DB entity, asserting that `generateRefreshToken` receives `workspaceId: undefined`.
2026-03-11 12:50:07 +00:00
..
2026-03-11 13:44:13 +01:00