Skip to content

fix(worker): refresh OAuth tokens in backend permission sync flow#1000

Merged
brendan-kellam merged 2 commits intomainfrom
brendan/fix-token-refresh-in-permission-sync-SOU-664
Mar 13, 2026
Merged

fix(worker): refresh OAuth tokens in backend permission sync flow#1000
brendan-kellam merged 2 commits intomainfrom
brendan/fix-token-refresh-in-permission-sync-SOU-664

Conversation

@brendan-kellam
Copy link
Contributor

@brendan-kellam brendan-kellam commented Mar 13, 2026

Summary

  • OAuth 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 with auth errors.
  • Moves token refresh logic to packages/backend/src/ee/tokenRefresh.ts and calls ensureFreshAccountToken from accountPermissionSyncer before using an account's access token.
  • Adds tokenRefreshErrorMessage to the Account DB 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

  • Verify that an expired OAuth token is automatically refreshed when a permission sync job runs
  • Verify that when refresh fails, tokenRefreshErrorMessage is set on the account and the "Token refresh failed — please reconnect" message appears in the linked accounts UI
  • Verify that successfully re-authenticating clears tokenRefreshErrorMessage and removes the error from the UI
  • Verify existing permission sync flows (GitHub, GitLab, Bitbucket Cloud, Bitbucket Server) still work correctly when tokens are valid

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Re-authentication prompts appear when OAuth token refresh fails to help restore account access.
    • UI refreshes automatically after successful permission refresh to show up-to-date linked account status.
  • Bug Fixes

    • Token refresh handling consolidated to backend for more reliable expiration handling and clearer per-account error reporting.
    • Backend now records token refresh error messages on accounts to aid recovery.

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>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 13, 2026

Walkthrough

Centralizes OAuth token refresh into a backend function (ensureFreshAccountToken(account, db)), persists token refresh errors on the Account record, and updates frontend flows to read/clear per-account token refresh errors instead of session-based provider errors.

Changes

Cohort / File(s) Summary
Database Schema
packages/db/prisma/migrations/20260313002214_add_account_token_refresh_error_message/migration.sql, packages/db/prisma/schema.prisma
Adds optional tokenRefreshErrorMessage TEXT column/field to Account to persist OAuth refresh errors.
Backend Token Refresh & Syncer
packages/backend/src/ee/tokenRefresh.ts, packages/backend/src/ee/accountPermissionSyncer.ts
Introduces ensureFreshAccountToken(account, db) that validates, decrypts, refreshes tokens, updates DB, and records errors. accountPermissionSyncer now delegates token freshness to this function and removes local decrypt/validation logic.
Frontend Auth Changes
packages/web/src/auth.ts
Removes linkedAccountErrors session/JWT typings and runtime token-refresh logic; clears tokenRefreshErrorMessage on successful sign-in.
SSO UI / Actions
packages/web/src/ee/features/sso/actions.ts, packages/web/src/ee/features/sso/components/linkedAccountProviderCard.tsx
Switches linked-account error source to account.tokenRefreshErrorMessage; triggers router.refresh() after permission refresh to re-fetch UI data.
Adapter / Types
packages/web/src/lib/encryptedPrismaAdapter.ts
Extends encryptAccountData() input shape to accept optional tokenRefreshErrorMessage.
Changelog
CHANGELOG.md
Adds entry describing backend token refresh relocation and error surfacing changes.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and accurately describes the main change: moving OAuth token refresh logic into the backend permission sync flow, which is the core objective of the changeset.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch brendan/fix-token-refresh-in-permission-sync-SOU-664
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Don't send the raw refresh failure text to the client.

linkedAccountProviderCard.tsx only checks linkedAccount.error for truthiness, so returning account.tokenRefreshErrorMessage here 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

📥 Commits

Reviewing files that changed from the base of the PR and between a0d4658 and a9252bd.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • packages/backend/src/ee/accountPermissionSyncer.ts
  • packages/backend/src/ee/tokenRefresh.ts
  • packages/db/prisma/migrations/20260313002214_add_account_token_refresh_error_message/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/src/auth.ts
  • packages/web/src/ee/features/sso/actions.ts
  • packages/web/src/ee/features/sso/components/linkedAccountProviderCard.tsx
  • packages/web/src/lib/encryptedPrismaAdapter.ts

@brendan-kellam brendan-kellam enabled auto-merge (squash) March 13, 2026 01:28
@brendan-kellam brendan-kellam disabled auto-merge March 13, 2026 01:28
@brendan-kellam brendan-kellam merged commit c0b39e6 into main Mar 13, 2026
10 checks passed
@brendan-kellam brendan-kellam deleted the brendan/fix-token-refresh-in-permission-sync-SOU-664 branch March 13, 2026 01:28
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