OAuth credential sync and app integration enhancements - #5
Conversation
…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>
There was a problem hiding this comment.
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.tspackages/app-store/_utils/decodeOAuthState.tspackages/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.tspackages/app-store/hubspot/api/add.tspackages/app-store/larkcalendar/api/add.tspackages/app-store/office365calendar/api/add.tspackages/app-store/office365video/api/add.tspackages/app-store/salesforce/api/add.tspackages/app-store/zoho-bigin/api/add.tspackages/app-store/zohocrm/api/_getAdd.tspackages/app-store/zoomvideo/api/add.ts |
Updated import path for encodeOAuthState to reflect new oauth subdirectory structure. |
packages/app-store/googlecalendar/api/callback.tspackages/app-store/larkcalendar/api/callback.tspackages/app-store/office365calendar/api/callback.ts |
Updated import path for decodeOAuthState to reflect new oauth subdirectory structure. |
packages/app-store/hubspot/api/callback.tspackages/app-store/office365video/api/callback.tspackages/app-store/salesforce/api/callback.tspackages/app-store/zoho-bigin/api/callback.tspackages/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.tspackages/app-store/tandemvideo/api/callback.tspackages/app-store/webex/api/callback.tspackages/app-store/zoomvideo/api/callback.ts |
Updated import path for createOAuthAppCredential to reflect new oauth subdirectory structure. |
packages/app-store/googlecalendar/lib/CalendarService.tspackages/app-store/hubspot/lib/CalendarService.tspackages/app-store/larkcalendar/lib/CalendarService.tspackages/app-store/zoho-bigin/lib/CalendarService.tspackages/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.tspackages/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.tspackages/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
- Visit Settings → Code Management
- Configure repository dependencies
- 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
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
config- shows the current configretrigger_review- retriggers the review
More commands to be added soon.
| if ( | ||
| req.headers[process.env.CALCOM_WEBHOOK_HEADER_NAME || "calcom-webhook-secret"] !== | ||
| process.env.CALCOM_WEBHOOK_SECRET | ||
| ) { |
There was a problem hiding this comment.
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.
| 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(), |
There was a problem hiding this comment.
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.
| const response = await fetch(process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT, { | ||
| method: "POST", | ||
| body: new URLSearchParams({ | ||
| calcomUserId: userId.toString(), | ||
| appSlug, | ||
| }), | ||
| }); | ||
| return response; |
There was a problem hiding this comment.
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.
| } | ||
| 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( |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
Test 8nn
Summary by CodeRabbit
New Features
Chores
✏️ 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.
/api/webhook/app-credential) with AES256 encryption and custom header authentication for external credential management_utils/oauth/subdirectory across all app integrationsrefreshOAuthTokensutility for conditional token refresh via external sync endpoint when feature flag is enabledparseRefreshTokenResponseutility for standardized token validation with flexible schema supportAPP_CREDENTIAL_SHARING_ENABLEDfeature flag requiring both webhook secret and encryption key