OAuth credential sync and app integration enhancements

calcom/cal.com#11059
Golden recall: 100% Extra findings: 23

Golden Comments (5/5 found)

# Comment Severity Status FriendlyReviewer Detail
#1 parseRefreshTokenResponse hardcodes 'refresh_token' when it's missing from the OAuth response — invalidates the token, breaks all subsequent refreshes HIGH ✓ Found FriendlyReviewer: "Hardcoded sentinel value 'refresh_token' silently substituted when OAuth response lacks refresh_token. This overwrites valid stored refresh tokens, permanently breaking all subsequent OAuth refreshes" (parseRefreshTokenResponse.ts:28). Match perfect.
#2 Invalid Zod computed property keys [z.string().toString()] — evaluates to literal 'String' at runtime, not a dynamic catch-all HIGH ✓ Found FriendlyReviewer: "Invalid Zod computed property keys [z.string().toString()] evaluate to literal string 'String' at runtime" (parseRefreshTokenResponse.ts:10). Match perfect.
#3 parseRefreshTokenResponse returns a safeParse result ({ success, data, error }) instead of the token payload — stores the wrapper, not the data HIGH ✓ Found FriendlyReviewer: "parseRefreshTokenResponse returns the entire Zod SafeParseReturnType object instead of just data. This corrupts stored credential keys" (CalendarService.ts:98). Match perfect.
#4 Type mismatch in sync endpoint: refreshOAuthTokens returns a fetch Response but callers expect a token object — runtime error HIGH ✓ Found FriendlyReviewer found the bug in GoogleCalendar: "Type mismatch when APP_CREDENTIAL_SHARING_ENABLED is true: refreshOAuthTokens returns a Response object, but code accesses res?.data (undefined)" (CalendarService.ts:86). Match perfect.
#5 res?.data undefined when sync endpoint is used — token.access_token throws at runtime HIGH ✓ Found Treated together with Golden #4 by FriendlyReviewer — both are the same underlying type mismatch issue at CalendarService.ts:86. Both callers (GoogleCalendar, HubSpot) affected.

Supplementary Findings (24 findings)

Additional issues found by FriendlyReviewer that are not part of the golden (reference) set.

Finding File Severity Legitimate?
Unhandled JSON.parse on state parameter — DoS vector decodeOAuthState.ts:8 HIGH ✓ Security
No HTTP method restriction on webhook — should only accept POST app-credential.ts:22 HIGH ✓ Security
Unhandled exception JSON.parse(symmetricDecrypt(...)) — crash 500 app-credential.ts:57 HIGH ✓ Error handling
No runtime validation on decrypted keys — record corruption possible app-credential.ts:57 HIGH ✓ Data integrity
Token validity check inverted in HubSpot — never refreshes hubspot/CalendarService.ts:115 HIGH ✓ Logic bug
Missing handling for OAuth authorization denial — crashes if user refuses hubspot/api/callback.ts:22 HIGH ✓ Error handling
Module-level hubspotClient singleton mutated — race condition in serverless hubspot/CalendarService.ts:262 HIGH ✓ Race condition
Impossible condition status < 200 && status >= 300 (should be ||) — silences 4xx/5xx office365calendar/CalendarService.ts:490 HIGH ✓ Logic bug
Missing parseRefreshTokenResponse validation for credential sync office365video/VideoApiAdapter.ts:65 HIGH ✓ Missing validation
Missing import for prisma — ReferenceError salesforce/CalendarService.ts:1 HIGH ✓ Broken import
Token refresh uses old credentials (stale token) — unnecessary refresh salesforce/CalendarService.ts:84 HIGH ✓ Logic bug
Token validity inverted in Webex — expired token treated as valid webex/VideoApiAdapter.ts:38 HIGH ✓ Logic bug
Schema uses z.date() for start/end but Webex API returns ISO strings webex/VideoApiAdapter.ts:27 HIGH ✓ Type mismatch
credential.id (PK) passed as userId instead of credential.userId — sync broken zoho-bigin/CalendarService.ts:89 HIGH ✓ Logic bug
Token validity inverted in Zoho CRM — skips refresh, uses expired tokens zohocrm/CalendarService.ts:131 HIGH ✓ Logic bug
Expiry date 3600 ms instead of 3600s — token expires after 3.6 seconds zohocrm/CalendarService.ts:168 HIGH ✓ Unit error
OAuth state parameter not validated in callback — no CSRF protection zoomvideo/api/callback.ts:10 HIGH ✓ Security
getAvailability sends ISO strings but Lark freebusy API expects Unix timestamps larkcalendar/CalendarService.ts:289 HIGH ✓ API mismatch
No validation of code presence — undefined crash in stripe.oauth.token() stripepayment/api/callback.ts:23 HIGH ✓ Error handling
No HTTP response when OAuth token exchange fails — client hangs tandemvideo/api/callback.ts:55 HIGH ✓ Error handling
Module-level let variables — race condition in serverless tandemvideo/api/callback.ts:9 HIGH ✓ Race condition
EncodeOAuthState is a no-op — no security validation encodeOAuthState.ts:5 MEDIUM ✓ Security
Fire-and-forget fetch without error handling — unhandled promise rejection larkcalendar/api/add.ts:39 MEDIUM ✓ Error handling
APP_CREDENTIAL_SHARING_ENABLED not coerced to boolean — returns string constants.ts:103 LOW ✓ Code quality
CALCOM_WEBHOOK_SECRET out of alphabetical order turbo.json:207 LOW ✓ Code quality

Honest Analysis

FriendlyReviewer achieved a perfect 100% golden recall on this PR — all 5 reference bugs found with exact matches. This is the best performance in the entire Cal.com benchmark. The three systemic OAuth bugs (hardcoded sentinel, Zod computed keys evaluation, and safeParse wrapper return) are non-trivial issues that require understanding both Zod's runtime behavior and OAuth token lifecycle. The type mismatch between the sync endpoint and its callers was also correctly traced across two separate integrations.

The supplementary findings tell a more complicated story. 21 HIGH-severity findings across every OAuth integration is simultaneously impressive and suspect. Several findings are genuinely critical: the inverted token validity check in 3 separate apps (HubSpot, Webex, Zoho CRM) suggests FriendlyReviewer is good at pattern matching — once it finds the bug in one app, it finds the same antipattern in others. The Zoho "3600ms instead of 3600s" expiry bug and the OAuth CSRF (missing state validation) are also legitimate security concerns.

But the volume of HIGH findings (21 out of 24 supplementary, plus all 5 golden = 26 HIGH out of 29 total) may be a problem. When 90% of all findings are the highest severity, the severity classification becomes meaningless as a triage signal. A human team looking at this review would immediately discount the severity labels and have to re-prioritize everything manually. This is a FriendlyReviewer's constraint not fully suitable for so many findings.

That said, the cross-app pattern detection is genuinely impressive. Finding the same "inverted token validity" bug in HubSpot, Webex, and Zoho CRM would take a human reviewer significant time to discover across 40 files and 15+ OAuth implementations. FR found all three in one pass.

FriendlyReviewer's full review

As posted on the PR — the complete output from FriendlyReviewer.

friendly-reviewer bot commented now

Hi there 👋

☁️ Overview
26 high, 2 medium, 1 low

This PR introduces OAuth credential syncing across multiple app integrations. The review uncovered systemic security and correctness issues affecting nearly every integration. Several OAuth handlers lack proper state validation, error handling, and input sanitization. Token refresh logic is inverted in multiple apps, causing either stale token usage or unnecessary refreshes. Serverless race conditions exist from module-level mutable state, and type mismatches between sync endpoints and their callers will cause runtime failures. These findings should be addressed before enabling credential sharing in production.


Must fix

  • 🔴 parseRefreshTokenResponse.ts (L10)
    Invalid Zod computed property keys: [z.string().toString()] evaluates to literal string 'String' at runtime, not a dynamic catch-all key.
  • 🔴 parseRefreshTokenResponse.ts (L28)
    Hardcoded sentinel value 'refresh_token' silently substituted when OAuth response lacks refresh_token. Overwrites valid stored refresh tokens, permanently breaking all subsequent OAuth refreshes.
  • 🔴 decodeOAuthState.ts (L8)
    Unhandled JSON.parse on state parameter — malformed state causes uncaught exception. Also a DoS vector via crafted state strings.
  • 🔴 app-credential.ts (L22)
    No HTTP method restriction — webhook endpoint accepts any method. Only POST should be accepted per REST conventions and CSRF prevention.
  • 🔴 app-credential.ts (L57)
    Unhandled exception in JSON.parse(symmetricDecrypt(...)) — corrupted data causes 500 with stack trace instead of proper error response.
  • 🔴 app-credential.ts (L57)
    No runtime validation on decrypted OAuth keys — malformed or tampered data silently corrupts stored records.
  • 🔴 googlecalendar/CalendarService.ts (L98)
    parseRefreshTokenResponse returns entire Zod SafeParseReturnType object instead of just data. This corrupts stored credential keys with {success, data, error} wrapper.
  • 🔴 googlecalendar/CalendarService.ts (L86)
    Type mismatch when APP_CREDENTIAL_SHARING_ENABLED is true: refreshOAuthTokens returns a Response object, but code accesses res?.data (undefined).
  • 🔴 hubspot/CalendarService.ts (L115)
    Token validity check inverted: condition treats expired tokens as valid and valid tokens as expired — refresh never triggers.
  • 🔴 hubspot/api/callback.ts (L22)
    No handling for OAuth authorization denial — if user refuses, the callback crashes with undefined access.
  • 🔴 hubspot/CalendarService.ts (L262)
    Module-level hubspotClient singleton is mutated on each request — race condition in serverless environment.
  • 🔴 office365calendar/CalendarService.ts (L490)
    Impossible condition status < 200 && status >= 300 (should be ||) — all error status codes silently accepted as success.
  • 🔴 office365video/VideoApiAdapter.ts (L65)
    Missing parseRefreshTokenResponse validation for credential sync — uses raw response without validation.
  • 🔴 salesforce/CalendarService.ts (L1)
    Missing import for prisma — ReferenceError at runtime when credential sync code path executes.
  • 🔴 salesforce/CalendarService.ts (L84)
    Token refresh reads old credentials (stale token) instead of using current stored credentials — unnecessary refreshes.
  • 🔴 webex/VideoApiAdapter.ts (L38)
    Token validity check inverted: expired tokens treated as valid, valid tokens treated as expired.
  • 🔴 webex/VideoApiAdapter.ts (L27)
    Schema uses z.date() for start/end times but Webex API returns ISO 8601 strings — Zod parse failure.
  • 🔴 zoho-bigin/CalendarService.ts (L89)
    credential.id (database primary key) passed as userId instead of credential.userId — sync broken.
  • 🔴 zohocrm/CalendarService.ts (L131)
    Token validity check inverted: refresh skipped when needed, expired tokens used.
  • 🔴 zohocrm/CalendarService.ts (L168)
    Expiry calculated as 3600 ms instead of 3600 seconds — token expires after 3.6 seconds instead of 1 hour.
  • 🔴 zoomvideo/api/callback.ts (L10)
    OAuth state parameter not validated in callback — no CSRF protection. State should be compared against stored value.
  • 🔴 larkcalendar/CalendarService.ts (L289)
    getAvailability sends ISO 8601 date strings but Lark freebusy API expects Unix timestamps in seconds.
  • 🔴 stripepayment/api/callback.ts (L23)
    No validation that code is present — if undefined, stripe.oauth.token() crashes with uncaught error.
  • 🔴 tandemvideo/api/callback.ts (L55)
    No HTTP response sent when OAuth token exchange fails — client connection hangs until timeout.
  • 🔴 tandemvideo/api/callback.ts (L9)
    Module-level let variables mutated on each request — race condition in serverless environment.

Should address

  • 🟡 encodeOAuthState.ts (L5)
    EncodeOAuthState is a no-op — returns input unchanged. Provides no actual security validation.
  • 🟡 larkcalendar/api/add.ts (L39)
    Fire-and-forget fetch without error handling or await — unhandled promise rejection on failure.

Minor

  • 🔵 constants.ts (L103)
    APP_CREDENTIAL_SHARING_ENABLED env var not coerced to boolean — returns string "true"/"false" instead of actual boolean.
  • 🔵 turbo.json (L207)
    CALCOM_WEBHOOK_SECRET listed out of alphabetical order in turbo.json configuration.