Skip to content

OAuth credential sync and app integration enhancements - #5

Open
ShashankFC wants to merge 1 commit into
oauth-security-basefrom
oauth-security-enhanced
Open

OAuth credential sync and app integration enhancements#5
ShashankFC wants to merge 1 commit into
oauth-security-basefrom
oauth-security-enhanced

Conversation

@ShashankFC

@ShashankFC ShashankFC commented Feb 4, 2026

Copy link
Copy Markdown

Test 8nn

Summary by CodeRabbit

  • New Features

    • Added app credential sharing and synchronization to enable seamless credential management and real-time updates across connected applications
    • Improved OAuth token refresh mechanism for enhanced authentication reliability and consistency across all supported integrations
  • Chores

    • Reorganized internal OAuth utility infrastructure

✏️ Tip: You can customize this high-level summary in your review settings.

nn---n*Replicated from [ai-code-review-evaluation/cal.com-coderabbit#8](https://github.com/ai-code-review-evaluation/cal.com-coderabbit/pull/8)*

EntelligenceAI PR Summary

This PR implements App Credential Sync functionality enabling self-hosters to centrally manage OAuth credentials for Cal.com integrations.

  • Added webhook endpoint (/api/webhook/app-credential) with AES256 encryption and custom header authentication for external credential management
  • Refactored OAuth utilities into dedicated _utils/oauth/ subdirectory across all app integrations
  • Introduced refreshOAuthTokens utility for conditional token refresh via external sync endpoint when feature flag is enabled
  • Added parseRefreshTokenResponse utility for standardized token validation with flexible schema support
  • Updated 15+ OAuth integrations (Google Calendar, Office 365, HubSpot, Zoom, Webex, Salesforce, Zoho, Lark) to use centralized refresh logic
  • Added APP_CREDENTIAL_SHARING_ENABLED feature flag requiring both webhook secret and encryption key
  • Configured 5 new environment variables for webhook authentication, encryption, and sync endpoint URL
  • Implemented comprehensive validation for user existence, app existence, and proper error handling with appropriate HTTP status codes

…11059)

* Add credential sync .env variables

* Add webhook to send app credentials

* Upsert credentials when webhook called

* Refresh oauth token from a specific endpoint

* Pass appSlug

* Add credential encryption

* Move oauth helps into a folder

* Create parse token response wrapper

* Add OAuth helpers to apps

* Clean up

* Refactor `appDirName` to `appSlug`

* Address feedback

* Change to safe parse

* Remove console.log

---------

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>

@entelligence-ai-pr-reviews entelligence-ai-pr-reviews Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Walkthrough

This PR introduces an App Credential Sync feature for self-hosters to integrate Cal.com into existing applications. It adds a webhook endpoint for external credential management with AES256 encryption and custom header authentication. The implementation refactors OAuth utilities into a dedicated subdirectory and centralizes token refresh logic across all OAuth integrations (Google Calendar, Office 365, HubSpot, Zoom, Webex, Salesforce, Zoho, and Lark). New utility functions enable conditional credential syncing with external endpoints when the feature flag is enabled. The changes include comprehensive validation, error handling, and support for both centralized and traditional OAuth flows.

Changes

File(s) Summary
.env.example Added configuration section for App Credential Sync with four new environment variables: CALCOM_WEBHOOK_SECRET, CALCOM_WEBHOOK_HEADER_NAME, CALCOM_CREDENTIAL_SYNC_ENDPOINT, and CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY with documentation and OpenSSL generation commands.
apps/web/pages/api/webhook/app-credential.ts Created new webhook API endpoint for managing app credentials via external requests with custom header validation, AES256 decryption, and credential upsert operations with comprehensive error handling.
packages/app-store/_utils/createOAuthAppCredential.ts
packages/app-store/_utils/decodeOAuthState.ts
packages/app-store/_utils/encodeOAuthState.ts
Relocated OAuth utility files from _utils/ to _utils/oauth/ subdirectory and updated import paths accordingly.
packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts Introduced utility function for validating OAuth token refresh responses with conditional schema validation based on credential sharing configuration.
packages/app-store/_utils/oauth/refreshOAuthTokens.ts Created utility function implementing conditional OAuth token refresh logic that calls external sync endpoint when credential sharing is enabled, otherwise executes provided callback.
packages/app-store/googlecalendar/api/add.ts
packages/app-store/hubspot/api/add.ts
packages/app-store/larkcalendar/api/add.ts
packages/app-store/office365calendar/api/add.ts
packages/app-store/office365video/api/add.ts
packages/app-store/salesforce/api/add.ts
packages/app-store/zoho-bigin/api/add.ts
packages/app-store/zohocrm/api/_getAdd.ts
packages/app-store/zoomvideo/api/add.ts
Updated import path for encodeOAuthState to reflect new oauth subdirectory structure.
packages/app-store/googlecalendar/api/callback.ts
packages/app-store/larkcalendar/api/callback.ts
packages/app-store/office365calendar/api/callback.ts
Updated import path for decodeOAuthState to reflect new oauth subdirectory structure.
packages/app-store/hubspot/api/callback.ts
packages/app-store/office365video/api/callback.ts
packages/app-store/salesforce/api/callback.ts
packages/app-store/zoho-bigin/api/callback.ts
packages/app-store/zohocrm/api/callback.ts
Updated import paths for createOAuthAppCredential and decodeOAuthState to reflect new oauth subdirectory structure.
packages/app-store/stripepayment/api/callback.ts
packages/app-store/tandemvideo/api/callback.ts
packages/app-store/webex/api/callback.ts
packages/app-store/zoomvideo/api/callback.ts
Updated import path for createOAuthAppCredential to reflect new oauth subdirectory structure.
packages/app-store/googlecalendar/lib/CalendarService.ts
packages/app-store/hubspot/lib/CalendarService.ts
packages/app-store/larkcalendar/lib/CalendarService.ts
packages/app-store/zoho-bigin/lib/CalendarService.ts
packages/app-store/zohocrm/lib/CalendarService.ts
Refactored OAuth token refresh logic to use centralized refreshOAuthTokens utility function, wrapping existing refresh calls as callbacks.
packages/app-store/office365calendar/lib/CalendarService.ts
packages/app-store/zoomvideo/lib/VideoApiAdapter.ts
Refactored OAuth token refresh to use refreshOAuthTokens utility and replaced inline schema parsing with parseRefreshTokenResponse utility function.
packages/app-store/office365video/lib/VideoApiAdapter.ts
packages/app-store/webex/lib/VideoApiAdapter.ts
Refactored OAuth token refresh logic to use centralized refreshOAuthTokens utility function for video API adapters.
packages/app-store/salesforce/lib/CalendarService.ts Implemented proactive token refresh before jsforce connection with Zod schema validation and parseRefreshTokenResponse utility.
packages/app-store/zoho-bigin/api/add.ts Updated import path for encodeOAuthState and replaced dynamic slug-based redirect URI with hardcoded 'zoho-bigin' string.
packages/lib/constants.ts Added APP_CREDENTIAL_SHARING_ENABLED feature flag constant that enables when both CALCOM_WEBHOOK_SECRET and CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY are present.
turbo.json Added five new Cal.com environment variables to Turbo configuration allowlist for credential encryption, webhook authentication, and sync endpoint configuration.

🔗 Cross-Repository Impact Analysis

Enable automatic detection of breaking changes across your dependent repositories. → Set up now

Learn more about Cross-Repository Analysis

What It Does

  • Automatically identifies repositories that depend on this code
  • Analyzes potential breaking changes across your entire codebase
  • Provides risk assessment before merging to prevent cross-repo issues

How to Enable

  1. Visit Settings → Code Management
  2. Configure repository dependencies
  3. Future PRs will automatically include cross-repo impact analysis!

Benefits

  • 🛡️ Prevent breaking changes across repositories
  • 🔍 Catch integration issues before they reach production
  • 📊 Better visibility into your multi-repo architecture

▶️AI Code Reviews for VS Code, Cursor, Windsurf
Install the extension

Note for Windsurf Please change the default marketplace provider to the following in the windsurf settings:

Marketplace Extension Gallery Service URL: https://marketplace.visualstudio.com/_apis/public/gallery

Marketplace Gallery Item URL: https://marketplace.visualstudio.com/items

Entelligence.ai can learn from your feedback. Simply add 👍 / 👎 emojis to teach it your preferences. More shortcuts below

Emoji Descriptions:

  • ⚠️ Potential Issue - May require further investigation.
  • 🔒 Security Vulnerability - Fix to ensure system safety.
  • 💻 Code Improvement - Suggestions to enhance code quality.
  • 🔨 Refactor Suggestion - Recommendations for restructuring code.
  • ℹ️ Others - General comments and information.

Interact with the Bot:

  • Send a message or request using the format:
    @entelligenceai + *your message*
Example: @entelligenceai Can you suggest improvements for this code?
  • Help the Bot learn by providing feedback on its responses.
    @entelligenceai + *feedback*
Example: @entelligenceai Do not comment on `save_auth` function !

Also you can trigger various commands with the bot by doing
@entelligenceai command

The current supported commands are

  1. config - shows the current config
  2. retrigger_review - retriggers the review

More commands to be added soon.

Comment on lines +24 to +27
if (
req.headers[process.env.CALCOM_WEBHOOK_HEADER_NAME || "calcom-webhook-secret"] !==
process.env.CALCOM_WEBHOOK_SECRET
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correctness: The authentication check on line 25 is bypassed if CALCOM_WEBHOOK_SECRET is not defined. If the environment variable is missing, both the header lookup and the secret evaluate to undefined, causing the inequality check to fail and allowing unauthenticated access. The comparison must only proceed if process.env.CALCOM_WEBHOOK_SECRET is truthy.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In `apps/web/pages/api/webhook/app-credential.ts`, lines 24-27, prevent auth bypass when `CALCOM_WEBHOOK_SECRET` is unset. Add a guard that returns 500 if the secret is missing, then compare the header against the non-null secret. Use the provided diff as the exact fix.

Comment on lines +5 to +10
const minimumTokenResponseSchema = z.object({
access_token: z.string(),
// Assume that any property with a number is the expiry
[z.string().toString()]: z.number(),
// Allow other properties in the token response
[z.string().optional().toString()]: z.unknown().optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correctness: The minimumTokenResponseSchema is broken and will cause runtime failures. Computed keys like [z.string().toString()] evaluate to literal strings (e.g., 'ZodString') instead of defining dynamic keys, causing validation to fail on real responses. Additionally, z.object strips unknown keys by default, so refresh_token will be lost from the input. Use z.object({...}).passthrough() or z.record() to correctly handle dynamic token response fields.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

File: packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts (lines 5-10). The current schema uses `z.string().toString()` as keys, which becomes a literal "[object Object]" key and does not act as a catch‑all. Zod also strips unknown keys by default, so `refresh_token` is dropped and later replaced with a placeholder. Update `minimumTokenResponseSchema` to include `refresh_token` and use `.passthrough()` or a proper `z.record(...)` to preserve arbitrary fields.

Comment on lines +8 to +15
const response = await fetch(process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT, {
method: "POST",
body: new URLSearchParams({
calcomUserId: userId.toString(),
appSlug,
}),
});
return response;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correctness: In refreshOAuthTokens, the if branch returns a raw Response object, while the else branch returns the result of refreshFunction(). This will cause runtime errors when callers attempt to access token properties on the Response object. Parse the response body (e.g., return await response.json()) before returning.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

File: packages/app-store/_utils/oauth/refreshOAuthTokens.ts (lines 8-15). The sync endpoint branch returns a raw Response while the else-branch returns the refreshFunction result, creating an inconsistent return type for callers. Update the fetch branch to validate response.ok and return the parsed payload (response.json()) so both branches return the same token shape.

Comment on lines 64 to 85
}
try {
const appAccessToken = await getAppAccessToken();
const resp = await fetch(`${this.url}/authen/v1/refresh_access_token`, {
method: "POST",
headers: {
Authorization: `Bearer ${appAccessToken}`,
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: refreshToken,
}),
});
const resp = await refreshOAuthTokens(
async () =>
await fetch(`${this.url}/authen/v1/refresh_access_token`, {
method: "POST",
headers: {
Authorization: `Bearer ${appAccessToken}`,
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: refreshToken,
}),
}),
"lark-calendar",
credential.userId
);

const data = await handleLarkError<RefreshTokenResp>(resp, this.log);
this.log.debug(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correctness: The refreshToken used in the refreshOAuthTokens callback is captured from the credential object before the lock is acquired. This introduces a race condition where concurrent requests will use a stale token already invalidated by a previous refresh (Refresh Token Rotation). Additionally, handleLarkError will crash with a 'body already consumed' error if refreshOAuthTokens returns the same Response object to multiple concurrent callers.

Fix: Move the handleLarkError call inside the refreshOAuthTokens callback so it returns the parsed data, and ensure the latest refreshToken is fetched from the database inside the callback to avoid using stale credentials.


return new jsforce.Connection({
clientId: consumer_key,
clientSecret: consumer_secret,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correctness: The jsforce.Connection is initialized with stale credentials from credentialKey. Use accessTokenParsed.data.access_token and accessTokenParsed.data.instance_url to ensure the connection is authorized. Additionally, prisma is not defined in this file, which will cause a ReferenceError when updating the credential.

🤖 AI Agent Prompt for Cursor/Windsurf

📋 Copy this prompt to your AI coding assistant (Cursor, Windsurf, etc.) to get help fixing this issue

In `packages/app-store/salesforce/lib/CalendarService.ts` around the `new jsforce.Connection` call (diff chunk lines ~103), update the connection initialization to use the freshly parsed token values (`accessTokenParsed.data.instance_url` and `accessTokenParsed.data.access_token`) instead of the stale `credentialKey` values. Keep the refresh token as-is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants