Skip to content

Commit b4bee71

Browse files
authored
fix(credentials): match JSON 403s case-insensitively and refuse a malformed ZoomInfo access token (#9914)
- isVendorJsonForbidden lowercases Content-Type before matching, since media types are case-insensitive; a vendor 403 sent as Application/JSON no longer rejects a live key that only lacks a scope. - requestZoomInfoToken requires a non-blank string access_token, so the saved-credential minter and the legacy inline path never cache or send a malformed token.
1 parent 27c25c0 commit b4bee71

4 files changed

Lines changed: 32 additions & 7 deletions

File tree

‎apps/sim/lib/credentials/token-service-accounts/errors.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ export interface ProviderResponse {
222222
export function isVendorJsonForbidden(response: ProviderResponse): boolean {
223223
return (
224224
response.status === 403 &&
225-
(response.headers.get('content-type') ?? '').includes('application/json')
225+
(response.headers.get('content-type') ?? '').toLowerCase().includes('application/json')
226226
)
227227
}
228228

‎apps/sim/lib/credentials/token-service-accounts/validators/hubspot.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ describe('validateHubspotServiceAccount', () => {
156156
'lacks the contacts scope',
157157
() => jsonResponse({ status: 'error', category: 'MISSING_SCOPES' }, 403),
158158
],
159+
[
160+
'lacks the contacts scope, answered with a mixed-case JSON media type',
161+
() =>
162+
new Response(JSON.stringify({ status: 'error', category: 'MISSING_SCOPES' }), {
163+
status: 403,
164+
headers: { 'Content-Type': 'Application/JSON; charset=UTF-8' },
165+
}),
166+
],
159167
])(
160168
'accepts a service key that account-info refuses but that %s',
161169
async (_case, contactsAnswer) => {

‎apps/sim/lib/internal/zoominfo/client.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,26 @@ describe('ZoomInfo requests', () => {
3737
expect(mockSecureFetchWithValidation).not.toHaveBeenCalled()
3838
})
3939

40+
it.each([
41+
['a blank', ' ', 'client-blank-token'],
42+
['a non-string', 123, 'client-numeric-token'],
43+
])(
44+
'refuses %s access token in the token response before calling the API',
45+
async (_case, token, clientId) => {
46+
mockSecureFetchWithValidation.mockResolvedValue(
47+
Response.json({ access_token: token, expires_in: 3600 })
48+
)
49+
50+
await expect(
51+
requestZoomInfo({ ...REQUEST, clientId, clientSecret: 'secret' }, 'request-4')
52+
).rejects.toMatchObject({
53+
message: 'ZoomInfo token response missing access_token',
54+
status: 500,
55+
})
56+
expect(mockSecureFetchWithValidation).toHaveBeenCalledTimes(1)
57+
}
58+
)
59+
4060
it('keeps reporting an inline token refusal exactly as before', async () => {
4161
mockSecureFetchWithValidation.mockResolvedValue(
4262
Response.json({ error: 'invalid_client' }, { status: 401 })

‎apps/sim/lib/internal/zoominfo/client.ts‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createHash } from 'node:crypto'
22
import { createLogger } from '@sim/logger'
3+
import { isRecordLike } from '@sim/utils/object'
34
import {
45
MAX_JSON_API_RESPONSE_BYTES,
56
secureFetchWithValidation,
@@ -130,12 +131,8 @@ export async function requestZoomInfoToken(
130131
logger.warn('ZoomInfo token fetch failed', { requestId, status: response.status, error: text })
131132
throw new ZoomInfoTokenError(response.status)
132133
}
133-
const data = (await response.json()) as {
134-
access_token?: string
135-
expires_in?: number
136-
scope?: string
137-
}
138-
if (!data.access_token) {
134+
const data: unknown = await response.json()
135+
if (!isRecordLike(data) || typeof data.access_token !== 'string' || !data.access_token.trim()) {
139136
throw new ZoomInfoOperationError('ZoomInfo token response missing access_token', 500)
140137
}
141138
return {

0 commit comments

Comments
 (0)