Skip to content

fix(web): Fix token refresh in back-compat case#841

Merged
brendan-kellam merged 12 commits intomainfrom
bkellam/fix-SOU-316
Feb 7, 2026
Merged

fix(web): Fix token refresh in back-compat case#841
brendan-kellam merged 12 commits intomainfrom
bkellam/fix-SOU-316

Conversation

@brendan-kellam
Copy link
Contributor

@brendan-kellam brendan-kellam commented Feb 2, 2026

This PR fixes token refresh when using the deprecated (but still back-compat supported) env vars AUTH_EE_GITHUB_CLIENT_ID and AUTH_EE_GITHUB_CLIENT_SECRET. It additionally adds better error handling for the refresh path.

Summary by CodeRabbit

  • Bug Fixes

    • Serialized token refreshes per user and added a fallback to support deprecated SSO environment credentials, improving refresh reliability.
  • Documentation

    • Removed enterprise provider environment variables section; recommend configuring provider base URLs in identity provider config.
    • Updated permission-syncing docs and moved one enterprise variable into the main listing.
  • Chores

    • Marked legacy EE env vars as deprecated, removed commented dev env entries, and added an option to skip runtime env validation.
  • Changelog

    • Added Unreleased entry documenting an additional token refresh scenario.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 2, 2026

Caution

Review failed

The pull request is closed.

Walkthrough

Adds per-user refresh locks and refactors token refresh to a single-flight flow with zod validation and deprecated-env fallback; deprecates EE provider env vars in the env schema, updates docs and changelog, and removes commented EE vars from .env.development.

Changes

Cohort / File(s) Summary
Token refresh logic
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts
Implements in-memory per-user refresh locks and single-flight refresh (refreshLinkedAccountTokens → doRefreshLinkedAccountTokens); adds tryRefreshToken, zod OAuth response validation, ProviderCredentials type, deprecated-env fallback (getDeprecatedEnvCredentials), stricter expiry checks, and updated encryption/persistence for tokens.
Environment schema & exports
packages/shared/src/env.server.ts, packages/shared/src/index.server.ts
Removes EE OAuth keys from the primary env block and re-adds them as @deprecated entries with guidance to use identityProviders; adds SKIP_ENV_VALIDATION (skipValidation) option; separates env export into its own statement.
Docs
docs/docs/configuration/environment-variables.mdx, docs/docs/features/permission-syncing.mdx
Removes Enterprise env-var section, moves AUTH_EE_ALLOW_EMAIL_ACCOUNT_LINKING into main list, and changes guidance to prefer provider baseUrl in identity provider config instead of env vars.
Dev env
.env.development
Removes commented-out AUTH_EE_* client ID/secret lines.
Changelog
CHANGELOG.md
Adds Unreleased → Fixed entry documenting token refresh error when SSO is configured using deprecated env vars.
Manifest / package
manifest_file, package.json
Minor manifest/package metadata changes recorded (add/remove lines).

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Config as Provider Config
    participant Env as Deprecated Env Vars
    participant OAuth as OAuth Provider

    App->>Config: request provider config for provider
    Config-->>App: config found? (yes/no)

    alt config found
        App->>OAuth: POST token endpoint (client_id/client_secret from config)
        OAuth-->>App: access_token, refresh_token, expires_in
        App-->>App: normalize & persist refreshed tokens
    else config missing
        App->>Env: getDeprecatedEnvCredentials(provider)
        Env-->>App: client_id, client_secret, optional baseUrl
        App->>OAuth: tryRefreshToken(env creds, refreshToken[, redirect_uri])
        OAuth-->>App: access_token/refresh_token or error
        alt refresh successful
            App-->>App: normalize & persist refreshed tokens
        else refresh failed
            App-->>App: log error and record failure
        end
    end
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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(web): Fix token refresh in back-compat case' directly and accurately describes the main change: fixing token refresh when using deprecated environment variables for backward compatibility.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bkellam/fix-SOU-316

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.

@brendan-kellam brendan-kellam marked this pull request as ready for review February 6, 2026 23:31
@claude
Copy link

claude bot commented Feb 6, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

1 similar comment
@claude
Copy link

claude bot commented Feb 6, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

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: 2

🤖 Fix all issues with AI agents
In `@CHANGELOG.md`:
- Line 18: Update the changelog line that currently reads 'Fixed token refresh
error "Provider config not found or invalid for: x" when a sso is configured
using deprecated env vars.' to use the correct acronym casing and article:
replace "a sso" with "SSO" so the sentence reads e.g. '... when SSO is
configured using deprecated env vars.'; ensure only the phrase "a sso" is
changed to "SSO" in the affected line.

In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 164-171: Validate the OAuth response body before returning: ensure
data.access_token exists and throw or return a handled error (so
encryptOAuthToken never receives undefined), and avoid setting expiresAt to 0 by
using a safe fallback instead of 0 (e.g., expiresAt: Math.floor(Date.now()/1000)
+ (data.expires_in ?? 300) or explicitly set expiresAt to null and document that
consumers (e.g., refreshLinkedAccountTokens) must handle null), so tokens
without expires_in don’t create an immediate refresh loop; update the function
that returns { accessToken, refreshToken, expiresAt } in tokenRefresh.ts
accordingly.
🧹 Nitpick comments (1)
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts (1)

149-156: Consider adding a timeout to the fetch call.

This is an external OAuth call with no timeout. If the provider endpoint is slow or unresponsive, this will hang indefinitely, potentially blocking the token refresh for all accounts in the Promise.all at line 36.

Proposed fix
     const response = await fetch(url, {
         method: 'POST',
         headers: {
             'Content-Type': 'application/x-www-form-urlencoded',
             'Accept': 'application/json',
         },
         body: new URLSearchParams(bodyParams),
+        signal: AbortSignal.timeout(10_000), // 10 second timeout
     });

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

🤖 Fix all issues with AI agents
In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 156-168: The URL construction for token refresh can produce double
slashes when baseUrl has a trailing slash; update the logic in tokenRefresh.ts
(the block that sets the url variable based on baseUrl and provider) to build
the endpoint using URL resolution instead of string concatenation—e.g., use new
URL('/login/oauth/access_token', baseUrl) for GitHub and new URL('/oauth/token',
baseUrl) for GitLab when baseUrl is present; keep the existing hardcoded
fallback URLs when baseUrl is absent and preserve the unsupported-provider error
path.
🧹 Nitpick comments (1)
packages/shared/src/index.server.ts (1)

33-33: Nit: missing trailing semicolon.

Every other export statement in this file ends with a semicolon.

-export { env } from "./env.server.js"
+export { env } from "./env.server.js";

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: 2

🤖 Fix all issues with AI agents
In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 89-93: The DB update unconditionally overwrites refresh_token with
encryptOAuthToken(refreshTokenResponse.refresh_token), which will wipe the
stored token when refresh_token is undefined; change the update in the token
refresh flow (where data: { access_token: encryptOAuthToken(...), refresh_token:
..., expires_at }) to only set/update the refresh_token field when
refreshTokenResponse.refresh_token is present — e.g., build the update payload
for the DB (or use a conditional property) so encryptOAuthToken is only called
and persisted if refreshTokenResponse.refresh_token !== undefined, keeping the
existing DB refresh token otherwise; reference encryptOAuthToken and
refreshTokenResponse to locate the change.
- Around line 241-243: The current error log in tokenRefresh.ts prints the full
OAuth response body (json) which may contain sensitive tokens; update the
failure branch (the block using result.success, logger.error, provider and json)
to avoid dumping the raw response and instead log only the Zod validation error
details (use result.error.message or result.error.format()) and contextual info
such as provider and request id if available; remove or redact
JSON.stringify(json, null, 2) from the logger.error call so no raw
tokens/secrets are logged.

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

🤖 Fix all issues with AI agents
In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 78-81: The code sets expires_at = null when
refreshTokenResponse.expires_in is missing, which causes the account to be
excluded by the existing DB query (expires_at: { not: null }) and prevents
future refreshes; change the logic where refreshTokenResponse is processed (look
for refreshTokenResponse and the expires_at assignment in tokenRefresh.ts) to
compute expires_at as Math.floor(Date.now()/1000) +
(refreshTokenResponse.expires_in ?? 3600) (i.e., default to 3600s / 1 hour)
instead of null so the record remains eligible for refreshes; ensure any
save/update that writes expires_at uses this computed non-null value.
🧹 Nitpick comments (1)
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts (1)

232-236: Consider redacting the error response body as well.

errorText from a failed token refresh could also contain sensitive information (e.g., partial tokens in error descriptions from some providers). Consider truncating or sanitizing it before logging.

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

🤖 Fix all issues with AI agents
In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 127-138: The current logic logs "Provider config not found or
invalid" even when deprecated env credentials were found but
tryRefreshToken(provider, refreshToken, envCredentials) returned null; update
the branch handling providerConfigs.length === 0 to distinguish these cases:
call getDeprecatedEnvCredentials(provider) as before, and if envCredentials
exists but tryRefreshToken returns null, call logger.error with a message
indicating that token refresh using deprecated env credentials failed for that
provider (include provider and that deprecated creds were used), otherwise keep
the existing "Provider config not found or invalid" message when no
envCredentials exist; adjust logging in the block around providerConfigs,
getDeprecatedEnvCredentials, tryRefreshToken, and logger.error to reflect these
distinct outcomes.
🧹 Nitpick comments (2)
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts (2)

229-236: Consider adding a timeout to the fetch call.

If the OAuth provider is slow or unresponsive, this fetch will hang indefinitely since there's no AbortSignal with a timeout. Since this runs in a background refresh flow, a hung request would hold the per-user lock and block subsequent refresh attempts.

Proposed fix
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 30_000);
+
     const response = await fetch(url, {
         method: 'POST',
         headers: {
             'Content-Type': 'application/x-www-form-urlencoded',
             'Accept': 'application/json',
         },
         body: new URLSearchParams(bodyParams),
+        signal: controller.signal,
     });
+
+    clearTimeout(timeout);

238-241: Error response body may contain sensitive data.

errorText from the provider's error response could contain tokens, partial credentials, or internal details. Consider truncating or omitting it from logs, similar to how the success-path validation was tightened.

@brendan-kellam brendan-kellam merged commit f14c52f into main Feb 7, 2026
9 of 10 checks passed
@brendan-kellam brendan-kellam deleted the bkellam/fix-SOU-316 branch February 7, 2026 01:56
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