fix(worker): refresh OAuth tokens in backend permission sync flow#1000
Conversation
Token refresh was previously only triggered from the Next.js jwt callback, meaning tokens could expire between user visits and cause account-driven permission sync jobs to fail silently. Move refresh logic to packages/backend/src/ee/tokenRefresh.ts and call it from accountPermissionSyncer before using an account's access token. On refresh failure, tokenRefreshErrorMessage is set on the Account record and surfaced in the linked accounts UI so users know to re-authenticate. Also adds a DB migration for the tokenRefreshErrorMessage field and wires the signIn event to clear it on successful re-authentication. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WalkthroughCentralizes OAuth token refresh into a backend function ( Changes
Sequence DiagramsequenceDiagram
participant Syncer as AccountPermissionSyncer
participant Refresh as ensureFreshAccountToken
participant Provider as OAuth Provider
participant DB as Database
participant UI as Frontend UI
Syncer->>Refresh: ensureFreshAccountToken(account, db)
Refresh->>Refresh: Validate access_token presence
Refresh->>Refresh: Check expiry (with buffer)
alt Token valid
Refresh->>Refresh: Decrypt & return access_token
Refresh-->>Syncer: access_token
else Token expired/near expiry
Refresh->>Refresh: Ensure refresh_token exists
Refresh->>Provider: refreshOAuthToken(request with provider creds)
alt Refresh successful
Provider-->>Refresh: token response
Refresh->>DB: Update Account (access_token, refresh_token?, expires_at), clear tokenRefreshErrorMessage
DB-->>Refresh: OK
Refresh-->>Syncer: new access_token
else Refresh failed
Provider-->>Refresh: Error
Refresh->>DB: set tokenRefreshErrorMessage on Account
DB-->>Refresh: OK
Refresh-->>Syncer: throw/error
end
end
Syncer->>Syncer: Continue permission sync using token
UI->>DB: Fetch Account (includes tokenRefreshErrorMessage)
DB-->>UI: Account with tokenRefreshErrorMessage
UI->>UI: Surface message / prompt re-authentication
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/web/src/ee/features/sso/actions.ts (1)
54-60:⚠️ Potential issue | 🟡 MinorDon't send the raw refresh failure text to the client.
linkedAccountProviderCard.tsxonly checkslinkedAccount.errorfor truthiness, so returningaccount.tokenRefreshErrorMessagehere just leaks backend diagnostics to the browser. Collapse it to a boolean or sentinel string instead.Possible minimal change
- error: account.tokenRefreshErrorMessage ?? undefined, + error: account.tokenRefreshErrorMessage ? "token_refresh_failed" : undefined,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/web/src/ee/features/sso/actions.ts` around lines 54 - 60, The code currently exposes backend refresh failure text by assigning account.tokenRefreshErrorMessage to the result.error field in the object pushed by the result.push in actions.ts; change this to emit a non-sensitive sentinel or boolean (e.g. error: !!account.tokenRefreshErrorMessage or error: "REFRESH_FAILED") instead of the raw string so linkedAccountProviderCard.tsx can still check truthiness without leaking diagnostics; update the object property where provider/isLinked/accountId/providerAccountId/isAccountLinking are set to use the boolean/sentinel and ensure undefined is used when there was no error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/backend/src/ee/tokenRefresh.ts`:
- Around line 123-136: The account writes in db.account.update (used when
writing encryptOAuthToken(refreshResponse.access_token) / refresh_token /
expires_at) and in setTokenRefreshError are vulnerable to races because they
unconditionally overwrite from the job-start snapshot; to fix, serialize or use
compare-and-set: either acquire a per-account mutex (e.g., Redis lock keyed by
account.id) around the token refresh flow to ensure only one refresh runs for an
account, or change the updates to conditional updates that include the snapshot
you read (e.g., use updateMany/update with a where that includes the account.id
plus a snapshot column like account.updatedAt or the previous
access_token/refresh_token value read earlier) so the write only applies if the
stored values match the snapshot; apply the same pattern to the
setTokenRefreshError path.
---
Outside diff comments:
In `@packages/web/src/ee/features/sso/actions.ts`:
- Around line 54-60: The code currently exposes backend refresh failure text by
assigning account.tokenRefreshErrorMessage to the result.error field in the
object pushed by the result.push in actions.ts; change this to emit a
non-sensitive sentinel or boolean (e.g. error:
!!account.tokenRefreshErrorMessage or error: "REFRESH_FAILED") instead of the
raw string so linkedAccountProviderCard.tsx can still check truthiness without
leaking diagnostics; update the object property where
provider/isLinked/accountId/providerAccountId/isAccountLinking are set to use
the boolean/sentinel and ensure undefined is used when there was no error.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 29444165-81cc-4596-86a9-2b25d4674472
📒 Files selected for processing (9)
CHANGELOG.mdpackages/backend/src/ee/accountPermissionSyncer.tspackages/backend/src/ee/tokenRefresh.tspackages/db/prisma/migrations/20260313002214_add_account_token_refresh_error_message/migration.sqlpackages/db/prisma/schema.prismapackages/web/src/auth.tspackages/web/src/ee/features/sso/actions.tspackages/web/src/ee/features/sso/components/linkedAccountProviderCard.tsxpackages/web/src/lib/encryptedPrismaAdapter.ts
Summary
jwtcallback, meaning tokens could expire between user visits and cause account-driven permission sync jobs to fail with auth errors.packages/backend/src/ee/tokenRefresh.tsand callsensureFreshAccountTokenfromaccountPermissionSyncerbefore using an account's access token.tokenRefreshErrorMessageto theAccountDB model — set on refresh failure, cleared on successful re-authentication — and surfaces it in the linked accounts UI so users know to re-authenticate.Test plan
tokenRefreshErrorMessageis set on the account and the "Token refresh failed — please reconnect" message appears in the linked accounts UItokenRefreshErrorMessageand removes the error from the UI🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes