feat(auth): Implement encrypted storage for OAuth account tokens - #853

Merged
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling
Feb 5, 2026
Merged

feat(auth): Implement encrypted storage for OAuth account tokens#853
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling

Conversation

@harrison-xrb

@harrison-xrbharrison-xrb commented Feb 5, 2026

Copy link
Copy Markdown
Contributor
  • Add AES-256-GCM encryption for OAuth access and refresh tokens
  • Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
  • Implement automatic encryption on token storage and decryption on usage
  • Support graceful handling of existing plaintext tokens with automatic migration
  • Update accountPermissionSyncer to decrypt tokens before API calls
  • Update tokenRefresh to encrypt refreshed tokens before storage

This improves security by ensuring OAuth tokens are encrypted at rest in the database.

Additionally, this PR changes the refresh token path to source the provider tokens from the database rather than storing them in the JWT token.

Summary by CodeRabbit

  • Security
    • OAuth tokens (access, refresh, id tokens) are now encrypted at rest and transparently decrypted when needed.
    • Sign-in and token persistence automatically store encrypted token data.
  • New Tools
    • Added a CLI utility to decode/decrypt JWE session tokens for troubleshooting.
  • Behavior
    • No visible change to user workflows; authentication, token refresh, and permission syncing continue to operate seamlessly.

- Add AES-256-GCM encryption for OAuth access and refresh tokens
- Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
- Implement automatic encryption on token storage and decryption on usage
- Support graceful handling of existing plaintext tokens with automatic migration
- Update accountPermissionSyncer to decrypt tokens before API calls
- Update tokenRefresh to encrypt refreshed tokens before storage
This improves security by ensuring OAuth tokens are encrypted at rest in the database.
@coderabbitai

coderabbitaiBot commented Feb 5, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Decrypts stored OAuth tokens for runtime use and adds end-to-end encryption for OAuth tokens: shared encrypt/decrypt helpers, encrypted Prisma adapter and helpers, token encryption on sign-in/refresh, and decryption in backend permission sync flows.

Changes

Cohort / File(s)Summary
Shared crypto & exports
packages/shared/src/crypto.ts, packages/shared/src/index.server.ts
Add encryptOAuthToken / decryptOAuthToken (versioned AES-256-GCM with PBKDF2-derived per-token keys), schema validation, migration-safe handling, and re-export them from the server barrel.
NextAuth adapter & helpers
packages/web/src/lib/encryptedPrismaAdapter.ts, packages/web/src/auth.ts
Introduce EncryptedPrismaAdapter and encryptAccountData; switch NextAuth adapter to encrypt tokens on link/sign-in and change JWT/session shapes to surface linked account errors instead of token maps.
Token refresh persistence
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts
Load accounts from DB, decrypt stored refresh tokens before use, encrypt persisted refreshed tokens, and return per-account error map (LinkedAccountErrors).
Backend permission sync
packages/backend/src/ee/accountPermissionSyncer.ts
Decrypt account.access_token at start of sync and use decrypted token across provider-specific flows (GitHub/GitLab) for validation, client creation, and scope checks.
CLI tooling & manifests
packages/web/tools/decryptJWE.ts, packages/web/package.json, package.json
Add a CLI utility to decode JWE tokens (tools/decryptJWE.ts) and expose tool:decrypt-jwe npm scripts in package manifests.
Changelog
CHANGELOG.md
Documented storing OAuth tokens encrypted at rest and switching token sourcing from JWT to DB for refresh flows.

Sequence Diagram

sequenceDiagram
participant OAuthProvider as OAuth Provider
participant NextAuth as NextAuth/AuthService
participant Adapter as EncryptedPrismaAdapter
participant DB as Prisma/Database
participant Backend as Permission Syncer
participant API as GitHub/GitLab API
OAuthProvider->>NextAuth: callback with tokens
NextAuth->>Adapter: linkAccount(account with tokens)
Adapter->>Adapter: encryptOAuthToken(access/refresh/id)
Adapter->>DB: store account (encrypted tokens)
DB-->>Backend: fetch account (encrypted tokens)
Backend->>Backend: decryptOAuthToken(account.access_token)
Backend->>API: call provider API with decrypted token
API-->>Backend: scopes/repos response
Backend->>Backend: validate permissions
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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly and concisely describes the main objective: implementing encrypted storage for OAuth account tokens, which aligns with the primary changes across the codebase.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 5, 2026 02:37

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/shared/src/crypto.ts`:
- Around line 113-177: Add a fixed versioned encryption marker (e.g. "encv1:" or
magic bytes) to the output of encryptOAuthToken and make isOAuthTokenEncrypted
check for that marker rather than a length heuristic; update decryptOAuthToken
to expect that marker, strip it before base64-decoding, and treat any missing
marker as plaintext during migration but treat decoding/auth failures as a hard
failure (return null or throw depending on existing error policy) so we don't
silently accept bad ciphertext. Modify functions: isOAuthTokenEncrypted,
encryptOAuthToken, decryptOAuthToken, and keep deriveOAuthKey and constants but
add a constant for the marker/version to locate the logic easily.
In `@packages/web/src/lib/encryptedPrismaAdapter.ts`:
- Around line 9-46: encryptAccountData currently calls encryptOAuthToken on
possibly undefined values which returns null and causes Prisma update() to set
refresh_token=NULL; change encryptAccountData to only include
access_token/refresh_token/id_token keys when the incoming data has those keys
defined (i.e., check for data.access_token !== undefined, etc.) and only then
set the encrypted value (using encryptOAuthToken) so absent fields are left out
and do not overwrite existing DB values; locate and update the
encryptAccountData function (and any callers that expect its shape) to
conditionally add those properties rather than always spreading them as nulls.

Comment threadpackages/shared/src/crypto.ts
Comment threadpackages/web/src/lib/encryptedPrismaAdapter.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/tools/decryptJWE.ts`:
- Around line 20-27: The decryptJWE function currently logs the result of
decode(...) even when decode returns null for invalid/expired tokens; update
decryptJWE to check the result of decode (the variable decoded) and if it is
null, log an explicit error message including context (e.g., "Failed to decode
token: invalid or expired") and exit non-zero or throw an error so failures
aren't masked; keep references to decode, decryptJWE, token, secret and salt
when adding the null-check and error handling.

Comment threadpackages/web/tools/decryptJWE.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/permissionSyncing/tokenRefresh.ts (1)

160-164: ⚠️ Potential issue | 🟡 Minor

Potential refresh loop when expires_in is missing.

If the OAuth provider doesn't return expires_in, expiresAt is set to 0. Since the refresh logic triggers when now >= (expires_at - bufferTimeS) (line 44), a zero value would immediately qualify the token for refresh on the next request, potentially causing a refresh loop.

Consider using a reasonable default expiration (e.g., 1 hour) or logging a warning:

🛡️ Proposed fix
 const result = {
accessToken: data.access_token,
refreshToken: data.refresh_token ?? null,
- expiresAt: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : 0,+ expiresAt: data.expires_in + ? Math.floor(Date.now() / 1000) + data.expires_in + : Math.floor(Date.now() / 1000) + 3600, // Default to 1 hour if not provided
};
🤖 Fix all issues with AI agents
In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 64-69: The update currently writes refresh_token:
encryptOAuthToken(refreshedTokens.refreshToken) which yields undefined when
refreshedTokens.refreshToken is null and causes Prisma to skip updating the
column; add validation around refreshedTokens.refreshToken before the prisma
update: if it is non-null, encrypt and include it in the update; if it is null,
explicitly log a warning (including provider/user identifiers) that the
refresh_token was not rotated so this behavior is visible; optionally after the
prisma update compare the stored encrypted refresh_token (or the update
response) against the previous value to assert it changed when a new token was
provided and surface an error if providers that should rotate tokens did not.
🧹 Nitpick comments (2)
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts (1)

36-81: Consider race condition with concurrent token refresh.

Multiple simultaneous requests for the same user could trigger parallel refresh attempts for the same token. If the OAuth provider rotates refresh tokens (invalidating the old one), one refresh succeeds while others fail, causing spurious RefreshTokenError entries.

This is a known challenge with refresh token rotation. Consider:

  • Adding a short-lived cache/lock per account to prevent concurrent refreshes
  • Accepting this limitation and relying on the next request to succeed

Low priority if this edge case is acceptable for your use case.

packages/web/src/auth.ts (1)

220-224: Performance consideration: database query on every JWT callback.

refreshLinkedAccountTokens queries the database on every authenticated request to check token expiration. While the actual OAuth refresh only occurs when tokens are near expiry, the database roundtrip adds latency to every request.

Consider optimizing by:

  • Caching the next expiration time in the JWT itself
  • Only querying the database when approaching expiration
  • Using a background job for token refresh instead of inline

This may be acceptable for current usage patterns, but worth noting for scalability.

Sign up for freeto 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.

2 participants

@harrison-xrb@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(auth): Implement encrypted storage for OAuth account tokens - #853

Merged
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling
Feb 5, 2026
Merged

feat(auth): Implement encrypted storage for OAuth account tokens#853
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling

Conversation

@harrison-xrb

@harrison-xrbharrison-xrb commented Feb 5, 2026

Copy link
Copy Markdown
Contributor
  • Add AES-256-GCM encryption for OAuth access and refresh tokens
  • Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
  • Implement automatic encryption on token storage and decryption on usage
  • Support graceful handling of existing plaintext tokens with automatic migration
  • Update accountPermissionSyncer to decrypt tokens before API calls
  • Update tokenRefresh to encrypt refreshed tokens before storage

This improves security by ensuring OAuth tokens are encrypted at rest in the database.

Additionally, this PR changes the refresh token path to source the provider tokens from the database rather than storing them in the JWT token.

Summary by CodeRabbit

  • Security
    • OAuth tokens (access, refresh, id tokens) are now encrypted at rest and transparently decrypted when needed.
    • Sign-in and token persistence automatically store encrypted token data.
  • New Tools
    • Added a CLI utility to decode/decrypt JWE session tokens for troubleshooting.
  • Behavior
    • No visible change to user workflows; authentication, token refresh, and permission syncing continue to operate seamlessly.

- Add AES-256-GCM encryption for OAuth access and refresh tokens
- Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
- Implement automatic encryption on token storage and decryption on usage
- Support graceful handling of existing plaintext tokens with automatic migration
- Update accountPermissionSyncer to decrypt tokens before API calls
- Update tokenRefresh to encrypt refreshed tokens before storage
This improves security by ensuring OAuth tokens are encrypted at rest in the database.
@coderabbitai

coderabbitaiBot commented Feb 5, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Decrypts stored OAuth tokens for runtime use and adds end-to-end encryption for OAuth tokens: shared encrypt/decrypt helpers, encrypted Prisma adapter and helpers, token encryption on sign-in/refresh, and decryption in backend permission sync flows.

Changes

Cohort / File(s)Summary
Shared crypto & exports
packages/shared/src/crypto.ts, packages/shared/src/index.server.ts
Add encryptOAuthToken / decryptOAuthToken (versioned AES-256-GCM with PBKDF2-derived per-token keys), schema validation, migration-safe handling, and re-export them from the server barrel.
NextAuth adapter & helpers
packages/web/src/lib/encryptedPrismaAdapter.ts, packages/web/src/auth.ts
Introduce EncryptedPrismaAdapter and encryptAccountData; switch NextAuth adapter to encrypt tokens on link/sign-in and change JWT/session shapes to surface linked account errors instead of token maps.
Token refresh persistence
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts
Load accounts from DB, decrypt stored refresh tokens before use, encrypt persisted refreshed tokens, and return per-account error map (LinkedAccountErrors).
Backend permission sync
packages/backend/src/ee/accountPermissionSyncer.ts
Decrypt account.access_token at start of sync and use decrypted token across provider-specific flows (GitHub/GitLab) for validation, client creation, and scope checks.
CLI tooling & manifests
packages/web/tools/decryptJWE.ts, packages/web/package.json, package.json
Add a CLI utility to decode JWE tokens (tools/decryptJWE.ts) and expose tool:decrypt-jwe npm scripts in package manifests.
Changelog
CHANGELOG.md
Documented storing OAuth tokens encrypted at rest and switching token sourcing from JWT to DB for refresh flows.

Sequence Diagram

sequenceDiagram
participant OAuthProvider as OAuth Provider
participant NextAuth as NextAuth/AuthService
participant Adapter as EncryptedPrismaAdapter
participant DB as Prisma/Database
participant Backend as Permission Syncer
participant API as GitHub/GitLab API
OAuthProvider->>NextAuth: callback with tokens
NextAuth->>Adapter: linkAccount(account with tokens)
Adapter->>Adapter: encryptOAuthToken(access/refresh/id)
Adapter->>DB: store account (encrypted tokens)
DB-->>Backend: fetch account (encrypted tokens)
Backend->>Backend: decryptOAuthToken(account.access_token)
Backend->>API: call provider API with decrypted token
API-->>Backend: scopes/repos response
Backend->>Backend: validate permissions
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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly and concisely describes the main objective: implementing encrypted storage for OAuth account tokens, which aligns with the primary changes across the codebase.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 5, 2026 02:37

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/shared/src/crypto.ts`:
- Around line 113-177: Add a fixed versioned encryption marker (e.g. "encv1:" or
magic bytes) to the output of encryptOAuthToken and make isOAuthTokenEncrypted
check for that marker rather than a length heuristic; update decryptOAuthToken
to expect that marker, strip it before base64-decoding, and treat any missing
marker as plaintext during migration but treat decoding/auth failures as a hard
failure (return null or throw depending on existing error policy) so we don't
silently accept bad ciphertext. Modify functions: isOAuthTokenEncrypted,
encryptOAuthToken, decryptOAuthToken, and keep deriveOAuthKey and constants but
add a constant for the marker/version to locate the logic easily.
In `@packages/web/src/lib/encryptedPrismaAdapter.ts`:
- Around line 9-46: encryptAccountData currently calls encryptOAuthToken on
possibly undefined values which returns null and causes Prisma update() to set
refresh_token=NULL; change encryptAccountData to only include
access_token/refresh_token/id_token keys when the incoming data has those keys
defined (i.e., check for data.access_token !== undefined, etc.) and only then
set the encrypted value (using encryptOAuthToken) so absent fields are left out
and do not overwrite existing DB values; locate and update the
encryptAccountData function (and any callers that expect its shape) to
conditionally add those properties rather than always spreading them as nulls.

Comment threadpackages/shared/src/crypto.ts
Comment threadpackages/web/src/lib/encryptedPrismaAdapter.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/tools/decryptJWE.ts`:
- Around line 20-27: The decryptJWE function currently logs the result of
decode(...) even when decode returns null for invalid/expired tokens; update
decryptJWE to check the result of decode (the variable decoded) and if it is
null, log an explicit error message including context (e.g., "Failed to decode
token: invalid or expired") and exit non-zero or throw an error so failures
aren't masked; keep references to decode, decryptJWE, token, secret and salt
when adding the null-check and error handling.

Comment threadpackages/web/tools/decryptJWE.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/permissionSyncing/tokenRefresh.ts (1)

160-164: ⚠️ Potential issue | 🟡 Minor

Potential refresh loop when expires_in is missing.

If the OAuth provider doesn't return expires_in, expiresAt is set to 0. Since the refresh logic triggers when now >= (expires_at - bufferTimeS) (line 44), a zero value would immediately qualify the token for refresh on the next request, potentially causing a refresh loop.

Consider using a reasonable default expiration (e.g., 1 hour) or logging a warning:

🛡️ Proposed fix
 const result = {
accessToken: data.access_token,
refreshToken: data.refresh_token ?? null,
- expiresAt: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : 0,+ expiresAt: data.expires_in + ? Math.floor(Date.now() / 1000) + data.expires_in + : Math.floor(Date.now() / 1000) + 3600, // Default to 1 hour if not provided
};
🤖 Fix all issues with AI agents
In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 64-69: The update currently writes refresh_token:
encryptOAuthToken(refreshedTokens.refreshToken) which yields undefined when
refreshedTokens.refreshToken is null and causes Prisma to skip updating the
column; add validation around refreshedTokens.refreshToken before the prisma
update: if it is non-null, encrypt and include it in the update; if it is null,
explicitly log a warning (including provider/user identifiers) that the
refresh_token was not rotated so this behavior is visible; optionally after the
prisma update compare the stored encrypted refresh_token (or the update
response) against the previous value to assert it changed when a new token was
provided and surface an error if providers that should rotate tokens did not.
🧹 Nitpick comments (2)
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts (1)

36-81: Consider race condition with concurrent token refresh.

Multiple simultaneous requests for the same user could trigger parallel refresh attempts for the same token. If the OAuth provider rotates refresh tokens (invalidating the old one), one refresh succeeds while others fail, causing spurious RefreshTokenError entries.

This is a known challenge with refresh token rotation. Consider:

  • Adding a short-lived cache/lock per account to prevent concurrent refreshes
  • Accepting this limitation and relying on the next request to succeed

Low priority if this edge case is acceptable for your use case.

packages/web/src/auth.ts (1)

220-224: Performance consideration: database query on every JWT callback.

refreshLinkedAccountTokens queries the database on every authenticated request to check token expiration. While the actual OAuth refresh only occurs when tokens are near expiry, the database roundtrip adds latency to every request.

Consider optimizing by:

  • Caching the next expiration time in the JWT itself
  • Only querying the database when approaching expiration
  • Using a background job for token refresh instead of inline

This may be acceptable for current usage patterns, but worth noting for scalability.

Sign up for freeto 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.

2 participants

@harrison-xrb@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(auth): Implement encrypted storage for OAuth account tokens - #853

Merged
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling
Feb 5, 2026
Merged

feat(auth): Implement encrypted storage for OAuth account tokens#853
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling

Conversation

@harrison-xrb

@harrison-xrbharrison-xrb commented Feb 5, 2026

Copy link
Copy Markdown
Contributor
  • Add AES-256-GCM encryption for OAuth access and refresh tokens
  • Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
  • Implement automatic encryption on token storage and decryption on usage
  • Support graceful handling of existing plaintext tokens with automatic migration
  • Update accountPermissionSyncer to decrypt tokens before API calls
  • Update tokenRefresh to encrypt refreshed tokens before storage

This improves security by ensuring OAuth tokens are encrypted at rest in the database.

Additionally, this PR changes the refresh token path to source the provider tokens from the database rather than storing them in the JWT token.

Summary by CodeRabbit

  • Security
    • OAuth tokens (access, refresh, id tokens) are now encrypted at rest and transparently decrypted when needed.
    • Sign-in and token persistence automatically store encrypted token data.
  • New Tools
    • Added a CLI utility to decode/decrypt JWE session tokens for troubleshooting.
  • Behavior
    • No visible change to user workflows; authentication, token refresh, and permission syncing continue to operate seamlessly.

- Add AES-256-GCM encryption for OAuth access and refresh tokens
- Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
- Implement automatic encryption on token storage and decryption on usage
- Support graceful handling of existing plaintext tokens with automatic migration
- Update accountPermissionSyncer to decrypt tokens before API calls
- Update tokenRefresh to encrypt refreshed tokens before storage
This improves security by ensuring OAuth tokens are encrypted at rest in the database.
@coderabbitai

coderabbitaiBot commented Feb 5, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Decrypts stored OAuth tokens for runtime use and adds end-to-end encryption for OAuth tokens: shared encrypt/decrypt helpers, encrypted Prisma adapter and helpers, token encryption on sign-in/refresh, and decryption in backend permission sync flows.

Changes

Cohort / File(s)Summary
Shared crypto & exports
packages/shared/src/crypto.ts, packages/shared/src/index.server.ts
Add encryptOAuthToken / decryptOAuthToken (versioned AES-256-GCM with PBKDF2-derived per-token keys), schema validation, migration-safe handling, and re-export them from the server barrel.
NextAuth adapter & helpers
packages/web/src/lib/encryptedPrismaAdapter.ts, packages/web/src/auth.ts
Introduce EncryptedPrismaAdapter and encryptAccountData; switch NextAuth adapter to encrypt tokens on link/sign-in and change JWT/session shapes to surface linked account errors instead of token maps.
Token refresh persistence
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts
Load accounts from DB, decrypt stored refresh tokens before use, encrypt persisted refreshed tokens, and return per-account error map (LinkedAccountErrors).
Backend permission sync
packages/backend/src/ee/accountPermissionSyncer.ts
Decrypt account.access_token at start of sync and use decrypted token across provider-specific flows (GitHub/GitLab) for validation, client creation, and scope checks.
CLI tooling & manifests
packages/web/tools/decryptJWE.ts, packages/web/package.json, package.json
Add a CLI utility to decode JWE tokens (tools/decryptJWE.ts) and expose tool:decrypt-jwe npm scripts in package manifests.
Changelog
CHANGELOG.md
Documented storing OAuth tokens encrypted at rest and switching token sourcing from JWT to DB for refresh flows.

Sequence Diagram

sequenceDiagram
participant OAuthProvider as OAuth Provider
participant NextAuth as NextAuth/AuthService
participant Adapter as EncryptedPrismaAdapter
participant DB as Prisma/Database
participant Backend as Permission Syncer
participant API as GitHub/GitLab API
OAuthProvider->>NextAuth: callback with tokens
NextAuth->>Adapter: linkAccount(account with tokens)
Adapter->>Adapter: encryptOAuthToken(access/refresh/id)
Adapter->>DB: store account (encrypted tokens)
DB-->>Backend: fetch account (encrypted tokens)
Backend->>Backend: decryptOAuthToken(account.access_token)
Backend->>API: call provider API with decrypted token
API-->>Backend: scopes/repos response
Backend->>Backend: validate permissions
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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly and concisely describes the main objective: implementing encrypted storage for OAuth account tokens, which aligns with the primary changes across the codebase.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 5, 2026 02:37

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/shared/src/crypto.ts`:
- Around line 113-177: Add a fixed versioned encryption marker (e.g. "encv1:" or
magic bytes) to the output of encryptOAuthToken and make isOAuthTokenEncrypted
check for that marker rather than a length heuristic; update decryptOAuthToken
to expect that marker, strip it before base64-decoding, and treat any missing
marker as plaintext during migration but treat decoding/auth failures as a hard
failure (return null or throw depending on existing error policy) so we don't
silently accept bad ciphertext. Modify functions: isOAuthTokenEncrypted,
encryptOAuthToken, decryptOAuthToken, and keep deriveOAuthKey and constants but
add a constant for the marker/version to locate the logic easily.
In `@packages/web/src/lib/encryptedPrismaAdapter.ts`:
- Around line 9-46: encryptAccountData currently calls encryptOAuthToken on
possibly undefined values which returns null and causes Prisma update() to set
refresh_token=NULL; change encryptAccountData to only include
access_token/refresh_token/id_token keys when the incoming data has those keys
defined (i.e., check for data.access_token !== undefined, etc.) and only then
set the encrypted value (using encryptOAuthToken) so absent fields are left out
and do not overwrite existing DB values; locate and update the
encryptAccountData function (and any callers that expect its shape) to
conditionally add those properties rather than always spreading them as nulls.

Comment threadpackages/shared/src/crypto.ts
Comment threadpackages/web/src/lib/encryptedPrismaAdapter.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/tools/decryptJWE.ts`:
- Around line 20-27: The decryptJWE function currently logs the result of
decode(...) even when decode returns null for invalid/expired tokens; update
decryptJWE to check the result of decode (the variable decoded) and if it is
null, log an explicit error message including context (e.g., "Failed to decode
token: invalid or expired") and exit non-zero or throw an error so failures
aren't masked; keep references to decode, decryptJWE, token, secret and salt
when adding the null-check and error handling.

Comment threadpackages/web/tools/decryptJWE.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/permissionSyncing/tokenRefresh.ts (1)

160-164: ⚠️ Potential issue | 🟡 Minor

Potential refresh loop when expires_in is missing.

If the OAuth provider doesn't return expires_in, expiresAt is set to 0. Since the refresh logic triggers when now >= (expires_at - bufferTimeS) (line 44), a zero value would immediately qualify the token for refresh on the next request, potentially causing a refresh loop.

Consider using a reasonable default expiration (e.g., 1 hour) or logging a warning:

🛡️ Proposed fix
 const result = {
accessToken: data.access_token,
refreshToken: data.refresh_token ?? null,
- expiresAt: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : 0,+ expiresAt: data.expires_in + ? Math.floor(Date.now() / 1000) + data.expires_in + : Math.floor(Date.now() / 1000) + 3600, // Default to 1 hour if not provided
};
🤖 Fix all issues with AI agents
In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 64-69: The update currently writes refresh_token:
encryptOAuthToken(refreshedTokens.refreshToken) which yields undefined when
refreshedTokens.refreshToken is null and causes Prisma to skip updating the
column; add validation around refreshedTokens.refreshToken before the prisma
update: if it is non-null, encrypt and include it in the update; if it is null,
explicitly log a warning (including provider/user identifiers) that the
refresh_token was not rotated so this behavior is visible; optionally after the
prisma update compare the stored encrypted refresh_token (or the update
response) against the previous value to assert it changed when a new token was
provided and surface an error if providers that should rotate tokens did not.
🧹 Nitpick comments (2)
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts (1)

36-81: Consider race condition with concurrent token refresh.

Multiple simultaneous requests for the same user could trigger parallel refresh attempts for the same token. If the OAuth provider rotates refresh tokens (invalidating the old one), one refresh succeeds while others fail, causing spurious RefreshTokenError entries.

This is a known challenge with refresh token rotation. Consider:

  • Adding a short-lived cache/lock per account to prevent concurrent refreshes
  • Accepting this limitation and relying on the next request to succeed

Low priority if this edge case is acceptable for your use case.

packages/web/src/auth.ts (1)

220-224: Performance consideration: database query on every JWT callback.

refreshLinkedAccountTokens queries the database on every authenticated request to check token expiration. While the actual OAuth refresh only occurs when tokens are near expiry, the database roundtrip adds latency to every request.

Consider optimizing by:

  • Caching the next expiration time in the JWT itself
  • Only querying the database when approaching expiration
  • Using a background job for token refresh instead of inline

This may be acceptable for current usage patterns, but worth noting for scalability.

Sign up for freeto 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.

2 participants

@harrison-xrb@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(auth): Implement encrypted storage for OAuth account tokens - #853

Merged
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling
Feb 5, 2026
Merged

feat(auth): Implement encrypted storage for OAuth account tokens#853
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling

Conversation

@harrison-xrb

@harrison-xrbharrison-xrb commented Feb 5, 2026

Copy link
Copy Markdown
Contributor
  • Add AES-256-GCM encryption for OAuth access and refresh tokens
  • Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
  • Implement automatic encryption on token storage and decryption on usage
  • Support graceful handling of existing plaintext tokens with automatic migration
  • Update accountPermissionSyncer to decrypt tokens before API calls
  • Update tokenRefresh to encrypt refreshed tokens before storage

This improves security by ensuring OAuth tokens are encrypted at rest in the database.

Additionally, this PR changes the refresh token path to source the provider tokens from the database rather than storing them in the JWT token.

Summary by CodeRabbit

  • Security
    • OAuth tokens (access, refresh, id tokens) are now encrypted at rest and transparently decrypted when needed.
    • Sign-in and token persistence automatically store encrypted token data.
  • New Tools
    • Added a CLI utility to decode/decrypt JWE session tokens for troubleshooting.
  • Behavior
    • No visible change to user workflows; authentication, token refresh, and permission syncing continue to operate seamlessly.

- Add AES-256-GCM encryption for OAuth access and refresh tokens
- Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
- Implement automatic encryption on token storage and decryption on usage
- Support graceful handling of existing plaintext tokens with automatic migration
- Update accountPermissionSyncer to decrypt tokens before API calls
- Update tokenRefresh to encrypt refreshed tokens before storage
This improves security by ensuring OAuth tokens are encrypted at rest in the database.
@coderabbitai

coderabbitaiBot commented Feb 5, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Decrypts stored OAuth tokens for runtime use and adds end-to-end encryption for OAuth tokens: shared encrypt/decrypt helpers, encrypted Prisma adapter and helpers, token encryption on sign-in/refresh, and decryption in backend permission sync flows.

Changes

Cohort / File(s)Summary
Shared crypto & exports
packages/shared/src/crypto.ts, packages/shared/src/index.server.ts
Add encryptOAuthToken / decryptOAuthToken (versioned AES-256-GCM with PBKDF2-derived per-token keys), schema validation, migration-safe handling, and re-export them from the server barrel.
NextAuth adapter & helpers
packages/web/src/lib/encryptedPrismaAdapter.ts, packages/web/src/auth.ts
Introduce EncryptedPrismaAdapter and encryptAccountData; switch NextAuth adapter to encrypt tokens on link/sign-in and change JWT/session shapes to surface linked account errors instead of token maps.
Token refresh persistence
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts
Load accounts from DB, decrypt stored refresh tokens before use, encrypt persisted refreshed tokens, and return per-account error map (LinkedAccountErrors).
Backend permission sync
packages/backend/src/ee/accountPermissionSyncer.ts
Decrypt account.access_token at start of sync and use decrypted token across provider-specific flows (GitHub/GitLab) for validation, client creation, and scope checks.
CLI tooling & manifests
packages/web/tools/decryptJWE.ts, packages/web/package.json, package.json
Add a CLI utility to decode JWE tokens (tools/decryptJWE.ts) and expose tool:decrypt-jwe npm scripts in package manifests.
Changelog
CHANGELOG.md
Documented storing OAuth tokens encrypted at rest and switching token sourcing from JWT to DB for refresh flows.

Sequence Diagram

sequenceDiagram
participant OAuthProvider as OAuth Provider
participant NextAuth as NextAuth/AuthService
participant Adapter as EncryptedPrismaAdapter
participant DB as Prisma/Database
participant Backend as Permission Syncer
participant API as GitHub/GitLab API
OAuthProvider->>NextAuth: callback with tokens
NextAuth->>Adapter: linkAccount(account with tokens)
Adapter->>Adapter: encryptOAuthToken(access/refresh/id)
Adapter->>DB: store account (encrypted tokens)
DB-->>Backend: fetch account (encrypted tokens)
Backend->>Backend: decryptOAuthToken(account.access_token)
Backend->>API: call provider API with decrypted token
API-->>Backend: scopes/repos response
Backend->>Backend: validate permissions
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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly and concisely describes the main objective: implementing encrypted storage for OAuth account tokens, which aligns with the primary changes across the codebase.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 5, 2026 02:37

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/shared/src/crypto.ts`:
- Around line 113-177: Add a fixed versioned encryption marker (e.g. "encv1:" or
magic bytes) to the output of encryptOAuthToken and make isOAuthTokenEncrypted
check for that marker rather than a length heuristic; update decryptOAuthToken
to expect that marker, strip it before base64-decoding, and treat any missing
marker as plaintext during migration but treat decoding/auth failures as a hard
failure (return null or throw depending on existing error policy) so we don't
silently accept bad ciphertext. Modify functions: isOAuthTokenEncrypted,
encryptOAuthToken, decryptOAuthToken, and keep deriveOAuthKey and constants but
add a constant for the marker/version to locate the logic easily.
In `@packages/web/src/lib/encryptedPrismaAdapter.ts`:
- Around line 9-46: encryptAccountData currently calls encryptOAuthToken on
possibly undefined values which returns null and causes Prisma update() to set
refresh_token=NULL; change encryptAccountData to only include
access_token/refresh_token/id_token keys when the incoming data has those keys
defined (i.e., check for data.access_token !== undefined, etc.) and only then
set the encrypted value (using encryptOAuthToken) so absent fields are left out
and do not overwrite existing DB values; locate and update the
encryptAccountData function (and any callers that expect its shape) to
conditionally add those properties rather than always spreading them as nulls.

Comment threadpackages/shared/src/crypto.ts
Comment threadpackages/web/src/lib/encryptedPrismaAdapter.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/tools/decryptJWE.ts`:
- Around line 20-27: The decryptJWE function currently logs the result of
decode(...) even when decode returns null for invalid/expired tokens; update
decryptJWE to check the result of decode (the variable decoded) and if it is
null, log an explicit error message including context (e.g., "Failed to decode
token: invalid or expired") and exit non-zero or throw an error so failures
aren't masked; keep references to decode, decryptJWE, token, secret and salt
when adding the null-check and error handling.

Comment threadpackages/web/tools/decryptJWE.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/permissionSyncing/tokenRefresh.ts (1)

160-164: ⚠️ Potential issue | 🟡 Minor

Potential refresh loop when expires_in is missing.

If the OAuth provider doesn't return expires_in, expiresAt is set to 0. Since the refresh logic triggers when now >= (expires_at - bufferTimeS) (line 44), a zero value would immediately qualify the token for refresh on the next request, potentially causing a refresh loop.

Consider using a reasonable default expiration (e.g., 1 hour) or logging a warning:

🛡️ Proposed fix
 const result = {
accessToken: data.access_token,
refreshToken: data.refresh_token ?? null,
- expiresAt: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : 0,+ expiresAt: data.expires_in + ? Math.floor(Date.now() / 1000) + data.expires_in + : Math.floor(Date.now() / 1000) + 3600, // Default to 1 hour if not provided
};
🤖 Fix all issues with AI agents
In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 64-69: The update currently writes refresh_token:
encryptOAuthToken(refreshedTokens.refreshToken) which yields undefined when
refreshedTokens.refreshToken is null and causes Prisma to skip updating the
column; add validation around refreshedTokens.refreshToken before the prisma
update: if it is non-null, encrypt and include it in the update; if it is null,
explicitly log a warning (including provider/user identifiers) that the
refresh_token was not rotated so this behavior is visible; optionally after the
prisma update compare the stored encrypted refresh_token (or the update
response) against the previous value to assert it changed when a new token was
provided and surface an error if providers that should rotate tokens did not.
🧹 Nitpick comments (2)
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts (1)

36-81: Consider race condition with concurrent token refresh.

Multiple simultaneous requests for the same user could trigger parallel refresh attempts for the same token. If the OAuth provider rotates refresh tokens (invalidating the old one), one refresh succeeds while others fail, causing spurious RefreshTokenError entries.

This is a known challenge with refresh token rotation. Consider:

  • Adding a short-lived cache/lock per account to prevent concurrent refreshes
  • Accepting this limitation and relying on the next request to succeed

Low priority if this edge case is acceptable for your use case.

packages/web/src/auth.ts (1)

220-224: Performance consideration: database query on every JWT callback.

refreshLinkedAccountTokens queries the database on every authenticated request to check token expiration. While the actual OAuth refresh only occurs when tokens are near expiry, the database roundtrip adds latency to every request.

Consider optimizing by:

  • Caching the next expiration time in the JWT itself
  • Only querying the database when approaching expiration
  • Using a background job for token refresh instead of inline

This may be acceptable for current usage patterns, but worth noting for scalability.

Sign up for freeto 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.

2 participants

@harrison-xrb@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(auth): Implement encrypted storage for OAuth account tokens - #853

Merged
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling
Feb 5, 2026
Merged

feat(auth): Implement encrypted storage for OAuth account tokens#853
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling

Conversation

@harrison-xrb

@harrison-xrbharrison-xrb commented Feb 5, 2026

Copy link
Copy Markdown
Contributor
  • Add AES-256-GCM encryption for OAuth access and refresh tokens
  • Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
  • Implement automatic encryption on token storage and decryption on usage
  • Support graceful handling of existing plaintext tokens with automatic migration
  • Update accountPermissionSyncer to decrypt tokens before API calls
  • Update tokenRefresh to encrypt refreshed tokens before storage

This improves security by ensuring OAuth tokens are encrypted at rest in the database.

Additionally, this PR changes the refresh token path to source the provider tokens from the database rather than storing them in the JWT token.

Summary by CodeRabbit

  • Security
    • OAuth tokens (access, refresh, id tokens) are now encrypted at rest and transparently decrypted when needed.
    • Sign-in and token persistence automatically store encrypted token data.
  • New Tools
    • Added a CLI utility to decode/decrypt JWE session tokens for troubleshooting.
  • Behavior
    • No visible change to user workflows; authentication, token refresh, and permission syncing continue to operate seamlessly.

- Add AES-256-GCM encryption for OAuth access and refresh tokens
- Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
- Implement automatic encryption on token storage and decryption on usage
- Support graceful handling of existing plaintext tokens with automatic migration
- Update accountPermissionSyncer to decrypt tokens before API calls
- Update tokenRefresh to encrypt refreshed tokens before storage
This improves security by ensuring OAuth tokens are encrypted at rest in the database.
@coderabbitai

coderabbitaiBot commented Feb 5, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Decrypts stored OAuth tokens for runtime use and adds end-to-end encryption for OAuth tokens: shared encrypt/decrypt helpers, encrypted Prisma adapter and helpers, token encryption on sign-in/refresh, and decryption in backend permission sync flows.

Changes

Cohort / File(s)Summary
Shared crypto & exports
packages/shared/src/crypto.ts, packages/shared/src/index.server.ts
Add encryptOAuthToken / decryptOAuthToken (versioned AES-256-GCM with PBKDF2-derived per-token keys), schema validation, migration-safe handling, and re-export them from the server barrel.
NextAuth adapter & helpers
packages/web/src/lib/encryptedPrismaAdapter.ts, packages/web/src/auth.ts
Introduce EncryptedPrismaAdapter and encryptAccountData; switch NextAuth adapter to encrypt tokens on link/sign-in and change JWT/session shapes to surface linked account errors instead of token maps.
Token refresh persistence
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts
Load accounts from DB, decrypt stored refresh tokens before use, encrypt persisted refreshed tokens, and return per-account error map (LinkedAccountErrors).
Backend permission sync
packages/backend/src/ee/accountPermissionSyncer.ts
Decrypt account.access_token at start of sync and use decrypted token across provider-specific flows (GitHub/GitLab) for validation, client creation, and scope checks.
CLI tooling & manifests
packages/web/tools/decryptJWE.ts, packages/web/package.json, package.json
Add a CLI utility to decode JWE tokens (tools/decryptJWE.ts) and expose tool:decrypt-jwe npm scripts in package manifests.
Changelog
CHANGELOG.md
Documented storing OAuth tokens encrypted at rest and switching token sourcing from JWT to DB for refresh flows.

Sequence Diagram

sequenceDiagram
participant OAuthProvider as OAuth Provider
participant NextAuth as NextAuth/AuthService
participant Adapter as EncryptedPrismaAdapter
participant DB as Prisma/Database
participant Backend as Permission Syncer
participant API as GitHub/GitLab API
OAuthProvider->>NextAuth: callback with tokens
NextAuth->>Adapter: linkAccount(account with tokens)
Adapter->>Adapter: encryptOAuthToken(access/refresh/id)
Adapter->>DB: store account (encrypted tokens)
DB-->>Backend: fetch account (encrypted tokens)
Backend->>Backend: decryptOAuthToken(account.access_token)
Backend->>API: call provider API with decrypted token
API-->>Backend: scopes/repos response
Backend->>Backend: validate permissions
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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly and concisely describes the main objective: implementing encrypted storage for OAuth account tokens, which aligns with the primary changes across the codebase.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 5, 2026 02:37

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/shared/src/crypto.ts`:
- Around line 113-177: Add a fixed versioned encryption marker (e.g. "encv1:" or
magic bytes) to the output of encryptOAuthToken and make isOAuthTokenEncrypted
check for that marker rather than a length heuristic; update decryptOAuthToken
to expect that marker, strip it before base64-decoding, and treat any missing
marker as plaintext during migration but treat decoding/auth failures as a hard
failure (return null or throw depending on existing error policy) so we don't
silently accept bad ciphertext. Modify functions: isOAuthTokenEncrypted,
encryptOAuthToken, decryptOAuthToken, and keep deriveOAuthKey and constants but
add a constant for the marker/version to locate the logic easily.
In `@packages/web/src/lib/encryptedPrismaAdapter.ts`:
- Around line 9-46: encryptAccountData currently calls encryptOAuthToken on
possibly undefined values which returns null and causes Prisma update() to set
refresh_token=NULL; change encryptAccountData to only include
access_token/refresh_token/id_token keys when the incoming data has those keys
defined (i.e., check for data.access_token !== undefined, etc.) and only then
set the encrypted value (using encryptOAuthToken) so absent fields are left out
and do not overwrite existing DB values; locate and update the
encryptAccountData function (and any callers that expect its shape) to
conditionally add those properties rather than always spreading them as nulls.

Comment threadpackages/shared/src/crypto.ts
Comment threadpackages/web/src/lib/encryptedPrismaAdapter.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/tools/decryptJWE.ts`:
- Around line 20-27: The decryptJWE function currently logs the result of
decode(...) even when decode returns null for invalid/expired tokens; update
decryptJWE to check the result of decode (the variable decoded) and if it is
null, log an explicit error message including context (e.g., "Failed to decode
token: invalid or expired") and exit non-zero or throw an error so failures
aren't masked; keep references to decode, decryptJWE, token, secret and salt
when adding the null-check and error handling.

Comment threadpackages/web/tools/decryptJWE.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/permissionSyncing/tokenRefresh.ts (1)

160-164: ⚠️ Potential issue | 🟡 Minor

Potential refresh loop when expires_in is missing.

If the OAuth provider doesn't return expires_in, expiresAt is set to 0. Since the refresh logic triggers when now >= (expires_at - bufferTimeS) (line 44), a zero value would immediately qualify the token for refresh on the next request, potentially causing a refresh loop.

Consider using a reasonable default expiration (e.g., 1 hour) or logging a warning:

🛡️ Proposed fix
 const result = {
accessToken: data.access_token,
refreshToken: data.refresh_token ?? null,
- expiresAt: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : 0,+ expiresAt: data.expires_in + ? Math.floor(Date.now() / 1000) + data.expires_in + : Math.floor(Date.now() / 1000) + 3600, // Default to 1 hour if not provided
};
🤖 Fix all issues with AI agents
In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 64-69: The update currently writes refresh_token:
encryptOAuthToken(refreshedTokens.refreshToken) which yields undefined when
refreshedTokens.refreshToken is null and causes Prisma to skip updating the
column; add validation around refreshedTokens.refreshToken before the prisma
update: if it is non-null, encrypt and include it in the update; if it is null,
explicitly log a warning (including provider/user identifiers) that the
refresh_token was not rotated so this behavior is visible; optionally after the
prisma update compare the stored encrypted refresh_token (or the update
response) against the previous value to assert it changed when a new token was
provided and surface an error if providers that should rotate tokens did not.
🧹 Nitpick comments (2)
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts (1)

36-81: Consider race condition with concurrent token refresh.

Multiple simultaneous requests for the same user could trigger parallel refresh attempts for the same token. If the OAuth provider rotates refresh tokens (invalidating the old one), one refresh succeeds while others fail, causing spurious RefreshTokenError entries.

This is a known challenge with refresh token rotation. Consider:

  • Adding a short-lived cache/lock per account to prevent concurrent refreshes
  • Accepting this limitation and relying on the next request to succeed

Low priority if this edge case is acceptable for your use case.

packages/web/src/auth.ts (1)

220-224: Performance consideration: database query on every JWT callback.

refreshLinkedAccountTokens queries the database on every authenticated request to check token expiration. While the actual OAuth refresh only occurs when tokens are near expiry, the database roundtrip adds latency to every request.

Consider optimizing by:

  • Caching the next expiration time in the JWT itself
  • Only querying the database when approaching expiration
  • Using a background job for token refresh instead of inline

This may be acceptable for current usage patterns, but worth noting for scalability.

Sign up for freeto 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.

2 participants

@harrison-xrb@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(auth): Implement encrypted storage for OAuth account tokens - #853

Merged
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling
Feb 5, 2026
Merged

feat(auth): Implement encrypted storage for OAuth account tokens#853
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling

Conversation

@harrison-xrb

@harrison-xrbharrison-xrb commented Feb 5, 2026

Copy link
Copy Markdown
Contributor
  • Add AES-256-GCM encryption for OAuth access and refresh tokens
  • Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
  • Implement automatic encryption on token storage and decryption on usage
  • Support graceful handling of existing plaintext tokens with automatic migration
  • Update accountPermissionSyncer to decrypt tokens before API calls
  • Update tokenRefresh to encrypt refreshed tokens before storage

This improves security by ensuring OAuth tokens are encrypted at rest in the database.

Additionally, this PR changes the refresh token path to source the provider tokens from the database rather than storing them in the JWT token.

Summary by CodeRabbit

  • Security
    • OAuth tokens (access, refresh, id tokens) are now encrypted at rest and transparently decrypted when needed.
    • Sign-in and token persistence automatically store encrypted token data.
  • New Tools
    • Added a CLI utility to decode/decrypt JWE session tokens for troubleshooting.
  • Behavior
    • No visible change to user workflows; authentication, token refresh, and permission syncing continue to operate seamlessly.

- Add AES-256-GCM encryption for OAuth access and refresh tokens
- Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
- Implement automatic encryption on token storage and decryption on usage
- Support graceful handling of existing plaintext tokens with automatic migration
- Update accountPermissionSyncer to decrypt tokens before API calls
- Update tokenRefresh to encrypt refreshed tokens before storage
This improves security by ensuring OAuth tokens are encrypted at rest in the database.
@coderabbitai

coderabbitaiBot commented Feb 5, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Decrypts stored OAuth tokens for runtime use and adds end-to-end encryption for OAuth tokens: shared encrypt/decrypt helpers, encrypted Prisma adapter and helpers, token encryption on sign-in/refresh, and decryption in backend permission sync flows.

Changes

Cohort / File(s)Summary
Shared crypto & exports
packages/shared/src/crypto.ts, packages/shared/src/index.server.ts
Add encryptOAuthToken / decryptOAuthToken (versioned AES-256-GCM with PBKDF2-derived per-token keys), schema validation, migration-safe handling, and re-export them from the server barrel.
NextAuth adapter & helpers
packages/web/src/lib/encryptedPrismaAdapter.ts, packages/web/src/auth.ts
Introduce EncryptedPrismaAdapter and encryptAccountData; switch NextAuth adapter to encrypt tokens on link/sign-in and change JWT/session shapes to surface linked account errors instead of token maps.
Token refresh persistence
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts
Load accounts from DB, decrypt stored refresh tokens before use, encrypt persisted refreshed tokens, and return per-account error map (LinkedAccountErrors).
Backend permission sync
packages/backend/src/ee/accountPermissionSyncer.ts
Decrypt account.access_token at start of sync and use decrypted token across provider-specific flows (GitHub/GitLab) for validation, client creation, and scope checks.
CLI tooling & manifests
packages/web/tools/decryptJWE.ts, packages/web/package.json, package.json
Add a CLI utility to decode JWE tokens (tools/decryptJWE.ts) and expose tool:decrypt-jwe npm scripts in package manifests.
Changelog
CHANGELOG.md
Documented storing OAuth tokens encrypted at rest and switching token sourcing from JWT to DB for refresh flows.

Sequence Diagram

sequenceDiagram
participant OAuthProvider as OAuth Provider
participant NextAuth as NextAuth/AuthService
participant Adapter as EncryptedPrismaAdapter
participant DB as Prisma/Database
participant Backend as Permission Syncer
participant API as GitHub/GitLab API
OAuthProvider->>NextAuth: callback with tokens
NextAuth->>Adapter: linkAccount(account with tokens)
Adapter->>Adapter: encryptOAuthToken(access/refresh/id)
Adapter->>DB: store account (encrypted tokens)
DB-->>Backend: fetch account (encrypted tokens)
Backend->>Backend: decryptOAuthToken(account.access_token)
Backend->>API: call provider API with decrypted token
API-->>Backend: scopes/repos response
Backend->>Backend: validate permissions
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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly and concisely describes the main objective: implementing encrypted storage for OAuth account tokens, which aligns with the primary changes across the codebase.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 5, 2026 02:37

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/shared/src/crypto.ts`:
- Around line 113-177: Add a fixed versioned encryption marker (e.g. "encv1:" or
magic bytes) to the output of encryptOAuthToken and make isOAuthTokenEncrypted
check for that marker rather than a length heuristic; update decryptOAuthToken
to expect that marker, strip it before base64-decoding, and treat any missing
marker as plaintext during migration but treat decoding/auth failures as a hard
failure (return null or throw depending on existing error policy) so we don't
silently accept bad ciphertext. Modify functions: isOAuthTokenEncrypted,
encryptOAuthToken, decryptOAuthToken, and keep deriveOAuthKey and constants but
add a constant for the marker/version to locate the logic easily.
In `@packages/web/src/lib/encryptedPrismaAdapter.ts`:
- Around line 9-46: encryptAccountData currently calls encryptOAuthToken on
possibly undefined values which returns null and causes Prisma update() to set
refresh_token=NULL; change encryptAccountData to only include
access_token/refresh_token/id_token keys when the incoming data has those keys
defined (i.e., check for data.access_token !== undefined, etc.) and only then
set the encrypted value (using encryptOAuthToken) so absent fields are left out
and do not overwrite existing DB values; locate and update the
encryptAccountData function (and any callers that expect its shape) to
conditionally add those properties rather than always spreading them as nulls.

Comment threadpackages/shared/src/crypto.ts
Comment threadpackages/web/src/lib/encryptedPrismaAdapter.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/tools/decryptJWE.ts`:
- Around line 20-27: The decryptJWE function currently logs the result of
decode(...) even when decode returns null for invalid/expired tokens; update
decryptJWE to check the result of decode (the variable decoded) and if it is
null, log an explicit error message including context (e.g., "Failed to decode
token: invalid or expired") and exit non-zero or throw an error so failures
aren't masked; keep references to decode, decryptJWE, token, secret and salt
when adding the null-check and error handling.

Comment threadpackages/web/tools/decryptJWE.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/permissionSyncing/tokenRefresh.ts (1)

160-164: ⚠️ Potential issue | 🟡 Minor

Potential refresh loop when expires_in is missing.

If the OAuth provider doesn't return expires_in, expiresAt is set to 0. Since the refresh logic triggers when now >= (expires_at - bufferTimeS) (line 44), a zero value would immediately qualify the token for refresh on the next request, potentially causing a refresh loop.

Consider using a reasonable default expiration (e.g., 1 hour) or logging a warning:

🛡️ Proposed fix
 const result = {
accessToken: data.access_token,
refreshToken: data.refresh_token ?? null,
- expiresAt: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : 0,+ expiresAt: data.expires_in + ? Math.floor(Date.now() / 1000) + data.expires_in + : Math.floor(Date.now() / 1000) + 3600, // Default to 1 hour if not provided
};
🤖 Fix all issues with AI agents
In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 64-69: The update currently writes refresh_token:
encryptOAuthToken(refreshedTokens.refreshToken) which yields undefined when
refreshedTokens.refreshToken is null and causes Prisma to skip updating the
column; add validation around refreshedTokens.refreshToken before the prisma
update: if it is non-null, encrypt and include it in the update; if it is null,
explicitly log a warning (including provider/user identifiers) that the
refresh_token was not rotated so this behavior is visible; optionally after the
prisma update compare the stored encrypted refresh_token (or the update
response) against the previous value to assert it changed when a new token was
provided and surface an error if providers that should rotate tokens did not.
🧹 Nitpick comments (2)
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts (1)

36-81: Consider race condition with concurrent token refresh.

Multiple simultaneous requests for the same user could trigger parallel refresh attempts for the same token. If the OAuth provider rotates refresh tokens (invalidating the old one), one refresh succeeds while others fail, causing spurious RefreshTokenError entries.

This is a known challenge with refresh token rotation. Consider:

  • Adding a short-lived cache/lock per account to prevent concurrent refreshes
  • Accepting this limitation and relying on the next request to succeed

Low priority if this edge case is acceptable for your use case.

packages/web/src/auth.ts (1)

220-224: Performance consideration: database query on every JWT callback.

refreshLinkedAccountTokens queries the database on every authenticated request to check token expiration. While the actual OAuth refresh only occurs when tokens are near expiry, the database roundtrip adds latency to every request.

Consider optimizing by:

  • Caching the next expiration time in the JWT itself
  • Only querying the database when approaching expiration
  • Using a background job for token refresh instead of inline

This may be acceptable for current usage patterns, but worth noting for scalability.

Sign up for freeto 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.

2 participants

@harrison-xrb@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(auth): Implement encrypted storage for OAuth account tokens - #853

Merged
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling
Feb 5, 2026
Merged

feat(auth): Implement encrypted storage for OAuth account tokens#853
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling

Conversation

@harrison-xrb

@harrison-xrbharrison-xrb commented Feb 5, 2026

Copy link
Copy Markdown
Contributor
  • Add AES-256-GCM encryption for OAuth access and refresh tokens
  • Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
  • Implement automatic encryption on token storage and decryption on usage
  • Support graceful handling of existing plaintext tokens with automatic migration
  • Update accountPermissionSyncer to decrypt tokens before API calls
  • Update tokenRefresh to encrypt refreshed tokens before storage

This improves security by ensuring OAuth tokens are encrypted at rest in the database.

Additionally, this PR changes the refresh token path to source the provider tokens from the database rather than storing them in the JWT token.

Summary by CodeRabbit

  • Security
    • OAuth tokens (access, refresh, id tokens) are now encrypted at rest and transparently decrypted when needed.
    • Sign-in and token persistence automatically store encrypted token data.
  • New Tools
    • Added a CLI utility to decode/decrypt JWE session tokens for troubleshooting.
  • Behavior
    • No visible change to user workflows; authentication, token refresh, and permission syncing continue to operate seamlessly.

- Add AES-256-GCM encryption for OAuth access and refresh tokens
- Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
- Implement automatic encryption on token storage and decryption on usage
- Support graceful handling of existing plaintext tokens with automatic migration
- Update accountPermissionSyncer to decrypt tokens before API calls
- Update tokenRefresh to encrypt refreshed tokens before storage
This improves security by ensuring OAuth tokens are encrypted at rest in the database.
@coderabbitai

coderabbitaiBot commented Feb 5, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Decrypts stored OAuth tokens for runtime use and adds end-to-end encryption for OAuth tokens: shared encrypt/decrypt helpers, encrypted Prisma adapter and helpers, token encryption on sign-in/refresh, and decryption in backend permission sync flows.

Changes

Cohort / File(s)Summary
Shared crypto & exports
packages/shared/src/crypto.ts, packages/shared/src/index.server.ts
Add encryptOAuthToken / decryptOAuthToken (versioned AES-256-GCM with PBKDF2-derived per-token keys), schema validation, migration-safe handling, and re-export them from the server barrel.
NextAuth adapter & helpers
packages/web/src/lib/encryptedPrismaAdapter.ts, packages/web/src/auth.ts
Introduce EncryptedPrismaAdapter and encryptAccountData; switch NextAuth adapter to encrypt tokens on link/sign-in and change JWT/session shapes to surface linked account errors instead of token maps.
Token refresh persistence
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts
Load accounts from DB, decrypt stored refresh tokens before use, encrypt persisted refreshed tokens, and return per-account error map (LinkedAccountErrors).
Backend permission sync
packages/backend/src/ee/accountPermissionSyncer.ts
Decrypt account.access_token at start of sync and use decrypted token across provider-specific flows (GitHub/GitLab) for validation, client creation, and scope checks.
CLI tooling & manifests
packages/web/tools/decryptJWE.ts, packages/web/package.json, package.json
Add a CLI utility to decode JWE tokens (tools/decryptJWE.ts) and expose tool:decrypt-jwe npm scripts in package manifests.
Changelog
CHANGELOG.md
Documented storing OAuth tokens encrypted at rest and switching token sourcing from JWT to DB for refresh flows.

Sequence Diagram

sequenceDiagram
participant OAuthProvider as OAuth Provider
participant NextAuth as NextAuth/AuthService
participant Adapter as EncryptedPrismaAdapter
participant DB as Prisma/Database
participant Backend as Permission Syncer
participant API as GitHub/GitLab API
OAuthProvider->>NextAuth: callback with tokens
NextAuth->>Adapter: linkAccount(account with tokens)
Adapter->>Adapter: encryptOAuthToken(access/refresh/id)
Adapter->>DB: store account (encrypted tokens)
DB-->>Backend: fetch account (encrypted tokens)
Backend->>Backend: decryptOAuthToken(account.access_token)
Backend->>API: call provider API with decrypted token
API-->>Backend: scopes/repos response
Backend->>Backend: validate permissions
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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly and concisely describes the main objective: implementing encrypted storage for OAuth account tokens, which aligns with the primary changes across the codebase.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 5, 2026 02:37

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/shared/src/crypto.ts`:
- Around line 113-177: Add a fixed versioned encryption marker (e.g. "encv1:" or
magic bytes) to the output of encryptOAuthToken and make isOAuthTokenEncrypted
check for that marker rather than a length heuristic; update decryptOAuthToken
to expect that marker, strip it before base64-decoding, and treat any missing
marker as plaintext during migration but treat decoding/auth failures as a hard
failure (return null or throw depending on existing error policy) so we don't
silently accept bad ciphertext. Modify functions: isOAuthTokenEncrypted,
encryptOAuthToken, decryptOAuthToken, and keep deriveOAuthKey and constants but
add a constant for the marker/version to locate the logic easily.
In `@packages/web/src/lib/encryptedPrismaAdapter.ts`:
- Around line 9-46: encryptAccountData currently calls encryptOAuthToken on
possibly undefined values which returns null and causes Prisma update() to set
refresh_token=NULL; change encryptAccountData to only include
access_token/refresh_token/id_token keys when the incoming data has those keys
defined (i.e., check for data.access_token !== undefined, etc.) and only then
set the encrypted value (using encryptOAuthToken) so absent fields are left out
and do not overwrite existing DB values; locate and update the
encryptAccountData function (and any callers that expect its shape) to
conditionally add those properties rather than always spreading them as nulls.

Comment threadpackages/shared/src/crypto.ts
Comment threadpackages/web/src/lib/encryptedPrismaAdapter.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/tools/decryptJWE.ts`:
- Around line 20-27: The decryptJWE function currently logs the result of
decode(...) even when decode returns null for invalid/expired tokens; update
decryptJWE to check the result of decode (the variable decoded) and if it is
null, log an explicit error message including context (e.g., "Failed to decode
token: invalid or expired") and exit non-zero or throw an error so failures
aren't masked; keep references to decode, decryptJWE, token, secret and salt
when adding the null-check and error handling.

Comment threadpackages/web/tools/decryptJWE.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/permissionSyncing/tokenRefresh.ts (1)

160-164: ⚠️ Potential issue | 🟡 Minor

Potential refresh loop when expires_in is missing.

If the OAuth provider doesn't return expires_in, expiresAt is set to 0. Since the refresh logic triggers when now >= (expires_at - bufferTimeS) (line 44), a zero value would immediately qualify the token for refresh on the next request, potentially causing a refresh loop.

Consider using a reasonable default expiration (e.g., 1 hour) or logging a warning:

🛡️ Proposed fix
 const result = {
accessToken: data.access_token,
refreshToken: data.refresh_token ?? null,
- expiresAt: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : 0,+ expiresAt: data.expires_in + ? Math.floor(Date.now() / 1000) + data.expires_in + : Math.floor(Date.now() / 1000) + 3600, // Default to 1 hour if not provided
};
🤖 Fix all issues with AI agents
In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 64-69: The update currently writes refresh_token:
encryptOAuthToken(refreshedTokens.refreshToken) which yields undefined when
refreshedTokens.refreshToken is null and causes Prisma to skip updating the
column; add validation around refreshedTokens.refreshToken before the prisma
update: if it is non-null, encrypt and include it in the update; if it is null,
explicitly log a warning (including provider/user identifiers) that the
refresh_token was not rotated so this behavior is visible; optionally after the
prisma update compare the stored encrypted refresh_token (or the update
response) against the previous value to assert it changed when a new token was
provided and surface an error if providers that should rotate tokens did not.
🧹 Nitpick comments (2)
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts (1)

36-81: Consider race condition with concurrent token refresh.

Multiple simultaneous requests for the same user could trigger parallel refresh attempts for the same token. If the OAuth provider rotates refresh tokens (invalidating the old one), one refresh succeeds while others fail, causing spurious RefreshTokenError entries.

This is a known challenge with refresh token rotation. Consider:

  • Adding a short-lived cache/lock per account to prevent concurrent refreshes
  • Accepting this limitation and relying on the next request to succeed

Low priority if this edge case is acceptable for your use case.

packages/web/src/auth.ts (1)

220-224: Performance consideration: database query on every JWT callback.

refreshLinkedAccountTokens queries the database on every authenticated request to check token expiration. While the actual OAuth refresh only occurs when tokens are near expiry, the database roundtrip adds latency to every request.

Consider optimizing by:

  • Caching the next expiration time in the JWT itself
  • Only querying the database when approaching expiration
  • Using a background job for token refresh instead of inline

This may be acceptable for current usage patterns, but worth noting for scalability.

Sign up for freeto 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.

2 participants

@harrison-xrb@brendan-kellam
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(auth): Implement encrypted storage for OAuth account tokens - #853

Merged
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling
Feb 5, 2026
Merged

feat(auth): Implement encrypted storage for OAuth account tokens#853
brendan-kellam merged 6 commits into
sourcebot-dev:mainfrom
harrison-xrb:feat/improve-account-token-handling

Conversation

@harrison-xrb

@harrison-xrbharrison-xrb commented Feb 5, 2026

Copy link
Copy Markdown
Contributor
  • Add AES-256-GCM encryption for OAuth access and refresh tokens
  • Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
  • Implement automatic encryption on token storage and decryption on usage
  • Support graceful handling of existing plaintext tokens with automatic migration
  • Update accountPermissionSyncer to decrypt tokens before API calls
  • Update tokenRefresh to encrypt refreshed tokens before storage

This improves security by ensuring OAuth tokens are encrypted at rest in the database.

Additionally, this PR changes the refresh token path to source the provider tokens from the database rather than storing them in the JWT token.

Summary by CodeRabbit

  • Security
    • OAuth tokens (access, refresh, id tokens) are now encrypted at rest and transparently decrypted when needed.
    • Sign-in and token persistence automatically store encrypted token data.
  • New Tools
    • Added a CLI utility to decode/decrypt JWE session tokens for troubleshooting.
  • Behavior
    • No visible change to user workflows; authentication, token refresh, and permission syncing continue to operate seamlessly.

- Add AES-256-GCM encryption for OAuth access and refresh tokens
- Create EncryptedPrismaAdapter wrapping Auth.js PrismaAdapter
- Implement automatic encryption on token storage and decryption on usage
- Support graceful handling of existing plaintext tokens with automatic migration
- Update accountPermissionSyncer to decrypt tokens before API calls
- Update tokenRefresh to encrypt refreshed tokens before storage
This improves security by ensuring OAuth tokens are encrypted at rest in the database.
@coderabbitai

coderabbitaiBot commented Feb 5, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Decrypts stored OAuth tokens for runtime use and adds end-to-end encryption for OAuth tokens: shared encrypt/decrypt helpers, encrypted Prisma adapter and helpers, token encryption on sign-in/refresh, and decryption in backend permission sync flows.

Changes

Cohort / File(s)Summary
Shared crypto & exports
packages/shared/src/crypto.ts, packages/shared/src/index.server.ts
Add encryptOAuthToken / decryptOAuthToken (versioned AES-256-GCM with PBKDF2-derived per-token keys), schema validation, migration-safe handling, and re-export them from the server barrel.
NextAuth adapter & helpers
packages/web/src/lib/encryptedPrismaAdapter.ts, packages/web/src/auth.ts
Introduce EncryptedPrismaAdapter and encryptAccountData; switch NextAuth adapter to encrypt tokens on link/sign-in and change JWT/session shapes to surface linked account errors instead of token maps.
Token refresh persistence
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts
Load accounts from DB, decrypt stored refresh tokens before use, encrypt persisted refreshed tokens, and return per-account error map (LinkedAccountErrors).
Backend permission sync
packages/backend/src/ee/accountPermissionSyncer.ts
Decrypt account.access_token at start of sync and use decrypted token across provider-specific flows (GitHub/GitLab) for validation, client creation, and scope checks.
CLI tooling & manifests
packages/web/tools/decryptJWE.ts, packages/web/package.json, package.json
Add a CLI utility to decode JWE tokens (tools/decryptJWE.ts) and expose tool:decrypt-jwe npm scripts in package manifests.
Changelog
CHANGELOG.md
Documented storing OAuth tokens encrypted at rest and switching token sourcing from JWT to DB for refresh flows.

Sequence Diagram

sequenceDiagram
participant OAuthProvider as OAuth Provider
participant NextAuth as NextAuth/AuthService
participant Adapter as EncryptedPrismaAdapter
participant DB as Prisma/Database
participant Backend as Permission Syncer
participant API as GitHub/GitLab API
OAuthProvider->>NextAuth: callback with tokens
NextAuth->>Adapter: linkAccount(account with tokens)
Adapter->>Adapter: encryptOAuthToken(access/refresh/id)
Adapter->>DB: store account (encrypted tokens)
DB-->>Backend: fetch account (encrypted tokens)
Backend->>Backend: decryptOAuthToken(account.access_token)
Backend->>API: call provider API with decrypted token
API-->>Backend: scopes/repos response
Backend->>Backend: validate permissions
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 nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 60.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 nameStatusExplanation
Title check✅ PassedThe PR title clearly and concisely describes the main objective: implementing encrypted storage for OAuth account tokens, which aligns with the primary changes across the codebase.
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 5, 2026 02:37

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/shared/src/crypto.ts`:
- Around line 113-177: Add a fixed versioned encryption marker (e.g. "encv1:" or
magic bytes) to the output of encryptOAuthToken and make isOAuthTokenEncrypted
check for that marker rather than a length heuristic; update decryptOAuthToken
to expect that marker, strip it before base64-decoding, and treat any missing
marker as plaintext during migration but treat decoding/auth failures as a hard
failure (return null or throw depending on existing error policy) so we don't
silently accept bad ciphertext. Modify functions: isOAuthTokenEncrypted,
encryptOAuthToken, decryptOAuthToken, and keep deriveOAuthKey and constants but
add a constant for the marker/version to locate the logic easily.
In `@packages/web/src/lib/encryptedPrismaAdapter.ts`:
- Around line 9-46: encryptAccountData currently calls encryptOAuthToken on
possibly undefined values which returns null and causes Prisma update() to set
refresh_token=NULL; change encryptAccountData to only include
access_token/refresh_token/id_token keys when the incoming data has those keys
defined (i.e., check for data.access_token !== undefined, etc.) and only then
set the encrypted value (using encryptOAuthToken) so absent fields are left out
and do not overwrite existing DB values; locate and update the
encryptAccountData function (and any callers that expect its shape) to
conditionally add those properties rather than always spreading them as nulls.

Comment threadpackages/shared/src/crypto.ts
Comment threadpackages/web/src/lib/encryptedPrismaAdapter.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/tools/decryptJWE.ts`:
- Around line 20-27: The decryptJWE function currently logs the result of
decode(...) even when decode returns null for invalid/expired tokens; update
decryptJWE to check the result of decode (the variable decoded) and if it is
null, log an explicit error message including context (e.g., "Failed to decode
token: invalid or expired") and exit non-zero or throw an error so failures
aren't masked; keep references to decode, decryptJWE, token, secret and salt
when adding the null-check and error handling.

Comment threadpackages/web/tools/decryptJWE.ts

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

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/permissionSyncing/tokenRefresh.ts (1)

160-164: ⚠️ Potential issue | 🟡 Minor

Potential refresh loop when expires_in is missing.

If the OAuth provider doesn't return expires_in, expiresAt is set to 0. Since the refresh logic triggers when now >= (expires_at - bufferTimeS) (line 44), a zero value would immediately qualify the token for refresh on the next request, potentially causing a refresh loop.

Consider using a reasonable default expiration (e.g., 1 hour) or logging a warning:

🛡️ Proposed fix
 const result = {
accessToken: data.access_token,
refreshToken: data.refresh_token ?? null,
- expiresAt: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : 0,+ expiresAt: data.expires_in + ? Math.floor(Date.now() / 1000) + data.expires_in + : Math.floor(Date.now() / 1000) + 3600, // Default to 1 hour if not provided
};
🤖 Fix all issues with AI agents
In `@packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts`:
- Around line 64-69: The update currently writes refresh_token:
encryptOAuthToken(refreshedTokens.refreshToken) which yields undefined when
refreshedTokens.refreshToken is null and causes Prisma to skip updating the
column; add validation around refreshedTokens.refreshToken before the prisma
update: if it is non-null, encrypt and include it in the update; if it is null,
explicitly log a warning (including provider/user identifiers) that the
refresh_token was not rotated so this behavior is visible; optionally after the
prisma update compare the stored encrypted refresh_token (or the update
response) against the previous value to assert it changed when a new token was
provided and surface an error if providers that should rotate tokens did not.
🧹 Nitpick comments (2)
packages/web/src/ee/features/permissionSyncing/tokenRefresh.ts (1)

36-81: Consider race condition with concurrent token refresh.

Multiple simultaneous requests for the same user could trigger parallel refresh attempts for the same token. If the OAuth provider rotates refresh tokens (invalidating the old one), one refresh succeeds while others fail, causing spurious RefreshTokenError entries.

This is a known challenge with refresh token rotation. Consider:

  • Adding a short-lived cache/lock per account to prevent concurrent refreshes
  • Accepting this limitation and relying on the next request to succeed

Low priority if this edge case is acceptable for your use case.

packages/web/src/auth.ts (1)

220-224: Performance consideration: database query on every JWT callback.

refreshLinkedAccountTokens queries the database on every authenticated request to check token expiration. While the actual OAuth refresh only occurs when tokens are near expiry, the database roundtrip adds latency to every request.

Consider optimizing by:

  • Caching the next expiration time in the JWT itself
  • Only querying the database when approaching expiration
  • Using a background job for token refresh instead of inline

This may be acceptable for current usage patterns, but worth noting for scalability.

Sign up for freeto 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.

2 participants

@harrison-xrb@brendan-kellam