Skip to content

Commit 50d74c4

Browse files
committed
fix(zoho-desk): fall back to the credential Desk domain in webhook JWT verify
verifyAuth chose the JWKS host from providerConfig.apiDomain and otherwise defaulted to the US host (desk.zoho.com), so a non-US webhook row missing apiDomain would verify against the wrong JWKS and reject legitimate events. When apiDomain is absent, resolve it from the OAuth credential's __zoho_domain__ scope marker (mirroring deleteSubscription). The persisted-apiDomain fast path stays DB-free to respect the 5s delivery deadline. Adds tests for both paths.
1 parent beb4b06 commit 50d74c4

2 files changed

Lines changed: 46 additions & 3 deletions

File tree

apps/sim/lib/webhooks/providers/zoho-desk.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ describe('zohoDeskHandler', () => {
4141
})
4242

4343
describe('verifyAuth', () => {
44+
afterEach(() => {
45+
vi.mocked(getCredentialOwner).mockReset()
46+
})
47+
4448
it('rejects requests without the X-ZDesk-JWT header', async () => {
4549
const result = await zohoDeskHandler.verifyAuth?.(
4650
// biome-ignore lint/suspicious/noExplicitAny: minimal context for the header-only path
@@ -49,6 +53,33 @@ describe('zohoDeskHandler', () => {
4953
expect(result).not.toBeNull()
5054
expect(result?.status).toBe(401)
5155
})
56+
57+
it('falls back to the credential Desk domain when the webhook row has no apiDomain', async () => {
58+
vi.mocked(getCredentialOwner).mockResolvedValue({
59+
accountId: 'acct-1',
60+
userId: 'u1',
61+
// biome-ignore lint/suspicious/noExplicitAny: partial owner shape is enough for this path
62+
} as any)
63+
await zohoDeskHandler.verifyAuth?.(
64+
makeAuthContext(
65+
{ 'x-zdesk-jwt': 'not-a-real-jwt' },
66+
{ orgId: '1', externalId: '2', credentialId: 'cred-1' }
67+
// biome-ignore lint/suspicious/noExplicitAny: minimal context for the fallback path
68+
) as any
69+
)
70+
expect(getCredentialOwner).toHaveBeenCalledWith('cred-1', 'test')
71+
})
72+
73+
it('uses the persisted apiDomain without a credential lookup (fast path)', async () => {
74+
await zohoDeskHandler.verifyAuth?.(
75+
makeAuthContext(
76+
{ 'x-zdesk-jwt': 'not-a-real-jwt' },
77+
{ orgId: '1', externalId: '2', credentialId: 'cred-1', apiDomain: 'https://desk.zoho.eu' }
78+
// biome-ignore lint/suspicious/noExplicitAny: minimal context for the fast path
79+
) as any
80+
)
81+
expect(getCredentialOwner).not.toHaveBeenCalled()
82+
})
5283
})
5384

5485
describe('createSubscription', () => {

apps/sim/lib/webhooks/providers/zoho-desk.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,10 +313,22 @@ export const zohoDeskHandler: WebhookProviderHandler = {
313313
return new NextResponse('Unauthorized - webhook not fully provisioned', { status: 401 })
314314
}
315315

316-
const apiDomain =
317-
typeof providerConfig.apiDomain === 'string'
316+
// Prefer the apiDomain persisted on the webhook row (fast path, no DB hit on
317+
// the 5s-deadline verification path). Only when it is absent - older rows, or
318+
// a config that never captured it - fall back to the Desk base stored on the
319+
// OAuth credential (`__zoho_domain__` scope marker), mirroring
320+
// deleteSubscription, so a non-US org verifies against its own JWKS rather
321+
// than defaulting to the US host and rejecting legitimate events.
322+
let apiDomain =
323+
typeof providerConfig.apiDomain === 'string' && providerConfig.apiDomain
318324
? providerConfig.apiDomain
319-
: DEFAULT_ZOHO_DESK_BASE
325+
: ''
326+
if (!apiDomain) {
327+
const credentialId =
328+
typeof providerConfig.credentialId === 'string' ? providerConfig.credentialId : undefined
329+
const owner = credentialId ? await getCredentialOwner(credentialId, requestId) : null
330+
apiDomain = owner ? await resolveZohoDeskApiDomain(owner.accountId) : DEFAULT_ZOHO_DESK_BASE
331+
}
320332

321333
let deskHost: string
322334
try {

0 commit comments

Comments
 (0)