Skip to content

Conversation

@everettbu
Copy link

Test 8

…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 <[email protected]>
Co-authored-by: Omar López <[email protected]>
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Greptile Summary

This PR introduces a comprehensive OAuth credential sync and app integration enhancement system across the Cal.com codebase. The changes implement two main improvements:

1. OAuth Utility Reorganization: The PR systematically moves OAuth-related utilities (encodeOAuthState, decodeOAuthState, createOAuthAppCredential, etc.) from the general _utils directory into a dedicated _utils/oauth subdirectory across all app store integrations. This affects 20+ app integrations including Google Calendar, Office 365, HubSpot, Salesforce, Zoom, and others, creating better code organization and separation of concerns.

2. Centralized OAuth Token Management: The core enhancement introduces a new credential sharing system that allows OAuth token refresh requests to be optionally routed through a centralized endpoint when APP_CREDENTIAL_SHARING_ENABLED is configured. Key components include:

  • A new refreshOAuthTokens utility that wraps existing token refresh logic and conditionally routes to an external credential sync endpoint
  • A parseRefreshTokenResponse utility for standardized token response validation
  • A webhook handler (/api/webhook/app-credential) for receiving encrypted credential updates
  • Environment variables for configuring webhook authentication, encryption keys, and sync endpoints
  • A feature flag (APP_CREDENTIAL_SHARING_ENABLED) that requires both webhook secret and encryption key to be present

This system enables self-hosted Cal.com instances embedded within larger applications to sync OAuth credentials rather than requiring users to re-authenticate with services they've already connected to in the parent application. The implementation uses AES256 encryption for secure credential transmission and maintains backward compatibility by falling back to standard OAuth flows when credential sharing is disabled.

The changes integrate across 15+ calendar and app services (Google Calendar, Office 365, Salesforce, HubSpot, Zoom, Webex, Lark, Zoho, etc.), applying the centralized token refresh pattern consistently while preserving existing functionality.

Confidence score: 2/5

  • This PR introduces significant security-sensitive changes with multiple critical implementation issues that could cause authentication failures and security vulnerabilities
  • Several files contain logic errors, missing imports, unsafe JSON parsing, hardcoded fallback values, and improper error handling that will likely break OAuth flows
  • Files requiring immediate attention: packages/app-store/salesforce/lib/CalendarService.ts (missing prisma import), packages/app-store/_utils/oauth/parseRefreshTokenResponse.ts (invalid Zod schema syntax), apps/web/pages/api/webhook/app-credential.ts (security vulnerabilities), packages/app-store/_utils/oauth/createOAuthAppCredential.ts (incorrect team validation logic), and packages/app-store/googlecalendar/api/callback.ts (unsafe credential creation)

40 files reviewed, 11 comments

Edit Code Review Bot Settings | Greptile

Comment on lines +103 to +104
export const APP_CREDENTIAL_SHARING_ENABLED =
process.env.CALCOM_WEBHOOK_SECRET && process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY;
Copy link

Choose a reason for hiding this comment

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

style: Consider using explicit boolean conversion !!() for clarity, as this currently relies on JavaScript's truthy evaluation of strings

Comment on lines +177 to 189
const hubspotRefreshToken: HubspotToken = await refreshOAuthTokens(
async () =>
await hubspotClient.oauth.tokensApi.createToken(
"refresh_token",
undefined,
WEBAPP_URL + "/api/integrations/hubspot/callback",
this.client_id,
this.client_secret,
refreshToken
),
"hubspot",
credential.userId
);
Copy link

Choose a reason for hiding this comment

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

logic: The refreshOAuthTokens wrapper now handles credential sync routing, but the return type may not match the expected HubspotToken. The utility returns a raw fetch Response when using the sync endpoint, which would break the subsequent property access on line 192.

return Promise.reject(new Error("Invalid refreshed tokens were returned"));
}

await prisma.credential.update({
Copy link

Choose a reason for hiding this comment

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

syntax: Missing import for prisma. Add import prisma from "@calcom/prisma"; to the imports section.

Suggested change
await prisma.credential.update({
import type { TokenResponse } from "jsforce";
import jsforce from "jsforce";
import { RRule } from "rrule";
import { z } from "zod";
import { getLocation } from "@calcom/lib/CalEventParser";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type {
Calendar,
CalendarEvent,
IntegrationCalendar,
NewCalendarEventType,
Person,
} from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse";

},
}),
"zoho-bigin",
credentialId
Copy link

Choose a reason for hiding this comment

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

logic: Parameter mismatch: refreshOAuthTokens expects userId but receives credentialId. Check other integrations - Google Calendar passes credential.userId.

Comment on lines +8 to +10
[z.string().toString()]: z.number(),
// Allow other properties in the token response
[z.string().optional().toString()]: z.unknown().optional(),
Copy link

Choose a reason for hiding this comment

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

syntax: Invalid Zod schema syntax. Computed property keys like [z.string().toString()] are not valid in Zod object schemas and will cause runtime errors.

Suggested change
[z.string().toString()]: z.number(),
// Allow other properties in the token response
[z.string().optional().toString()]: z.unknown().optional(),
// Assume that any property with a number is the expiry
// Allow other properties in the token response

CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
# Key should match on Cal.com and your application
# must be 32 bytes for AES256 encryption algorithm
# You can use: `openssl rand -base64 24` to generate one
Copy link

Choose a reason for hiding this comment

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

logic: The encryption key generation command suggests 24 bytes (openssl rand -base64 24) but the comment states 32 bytes are required for AES256. Base64 encoding of 24 bytes produces 32 characters, but AES256 needs 32 actual bytes (256 bits). Should be openssl rand -base64 32 or clarify the requirement.

keys: z.string(),
});
/** */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
Copy link

Choose a reason for hiding this comment

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

logic: Missing HTTP method validation - any HTTP method is accepted including GET, PUT, DELETE which could expose this endpoint to CSRF attacks

Suggested change
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}

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

Choose a reason for hiding this comment

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

logic: Direct string comparison is vulnerable to timing attacks - use crypto.timingSafeEqual() for webhook secret validation

Comment on lines +57 to +59
const keys = JSON.parse(
symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
);
Copy link

Choose a reason for hiding this comment

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

logic: Missing error handling for JSON.parse() and symmetricDecrypt() - malformed data will cause runtime errors


// Decrypt the keys
const keys = JSON.parse(
symmetricDecrypt(reqBody.keys, process.env.CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY || "")
Copy link

Choose a reason for hiding this comment

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

logic: Using empty string fallback for encryption key defeats the security check on line 19 - should throw error if key is missing

@github-actions
Copy link
Contributor

Hey there and thank you for opening this pull request! 👋🏼

We require pull request titles to follow the Conventional Commits specification and it looks like your proposed title needs to be adjusted.

Details:

No release type found in pull request title "OAuth credential sync and app integration enhancements". Add a prefix to indicate what kind of release this pull request corresponds to. For reference, see https://www.conventionalcommits.org/

Available types:
 - feat: A new feature
 - fix: A bug fix
 - docs: Documentation only changes
 - style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
 - refactor: A code change that neither fixes a bug nor adds a feature
 - perf: A code change that improves performance
 - test: Adding missing tests or correcting existing tests
 - build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
 - ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
 - chore: Other changes that don't modify src or test files
 - revert: Reverts a previous commit

@github-actions
Copy link
Contributor

This PR is being marked as stale due to inactivity.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants