OAuth credential sync and app integration enhancements
calcom/cal.com#11059Golden 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's full review
As posted on the PR — the complete output from FriendlyReviewer.
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)
UnhandledJSON.parseon 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 inJSON.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 accessesres?.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-levelhubspotClientsingleton is mutated on each request — race condition in serverless environment. - 🔴 office365calendar/CalendarService.ts (L490)
Impossible conditionstatus < 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 forprisma— 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 usesz.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 asuserIdinstead ofcredential.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 thatcodeis 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-levelletvariables 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.
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.