|
7 | 7 | createMockRequest, |
8 | 8 | hybridAuthMockFns, |
9 | 9 | } from '@sim/testing' |
| 10 | +import { createRouteContext } from '@sim/testing/helpers/http' |
10 | 11 | import { NextRequest } from 'next/server' |
11 | 12 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
12 | 13 |
|
@@ -36,11 +37,13 @@ vi.mock('@/lib/credentials/application/resolve-managed-oauth-token', () => ({ |
36 | 37 | resolveManagedOAuthCredentialToken: { execute: mockResolveManagedOAuthCredentialToken }, |
37 | 38 | })) |
38 | 39 |
|
| 40 | +import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid' |
39 | 41 | import { createQuickBooksAccountId } from '@/lib/oauth/quickbooks' |
40 | 42 | import { GET, POST } from '@/app/api/auth/oauth/token/route' |
41 | 43 | import { getToolMetadata } from '@/tools/metadata' |
42 | 44 |
|
43 | 45 | const mockGetToolMetadata = vi.mocked(getToolMetadata) |
| 46 | +const mockCheckSessionOrInternalAuth = vi.mocked(checkSessionOrInternalAuth) |
44 | 47 |
|
45 | 48 | describe('OAuth Token API Routes', () => { |
46 | 49 | beforeEach(() => { |
@@ -290,6 +293,116 @@ describe('OAuth Token API Routes', () => { |
290 | 293 | }) |
291 | 294 | }) |
292 | 295 |
|
| 296 | + describe('saved service-account credentials', () => { |
| 297 | + const authorized = { |
| 298 | + ok: true, |
| 299 | + requesterUserId: 'test-user-id', |
| 300 | + credentialOwnerUserId: 'test-user-id', |
| 301 | + workspaceId: 'workspace-id', |
| 302 | + } |
| 303 | + |
| 304 | + beforeEach(() => { |
| 305 | + authOAuthUtilsMockFns.mockResolveOAuthAccountId.mockResolvedValueOnce({ |
| 306 | + accountId: '', |
| 307 | + credentialId: 'saved-credential-id', |
| 308 | + credentialType: 'service_account', |
| 309 | + providerId: 'vanta-service-account', |
| 310 | + workspaceId: 'workspace-id', |
| 311 | + usedCredentialTable: true, |
| 312 | + }) |
| 313 | + authOAuthUtilsMockFns.mockResolveServiceAccountToken.mockResolvedValue({ |
| 314 | + accessToken: 'saved-secret', |
| 315 | + }) |
| 316 | + mockGetToolMetadata.mockReturnValue({ |
| 317 | + id: 'vanta_list_tests', |
| 318 | + params: {}, |
| 319 | + oauth: { required: true, provider: 'vanta', credentialKind: 'service-account' }, |
| 320 | + }) |
| 321 | + }) |
| 322 | + |
| 323 | + it('refuses to release the secret to a session caller', async () => { |
| 324 | + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ |
| 325 | + success: true, |
| 326 | + authType: AuthType.SESSION, |
| 327 | + userId: 'test-user-id', |
| 328 | + }) |
| 329 | + mockAuthorizeCredentialUse.mockResolvedValueOnce(authorized) |
| 330 | + |
| 331 | + const response = await POST( |
| 332 | + createMockRequest('POST', { |
| 333 | + credentialId: 'saved-credential-id', |
| 334 | + toolId: 'vanta_list_tests', |
| 335 | + }), |
| 336 | + createRouteContext({}) |
| 337 | + ) |
| 338 | + |
| 339 | + expect(response.status).toBe(403) |
| 340 | + const body = await response.json() |
| 341 | + expect(body).toMatchObject({ code: 'SERVICE_ACCOUNT_SESSION_FORBIDDEN' }) |
| 342 | + expect(JSON.stringify(body)).not.toContain('saved-secret') |
| 343 | + expect(authOAuthUtilsMockFns.mockResolveServiceAccountToken).not.toHaveBeenCalled() |
| 344 | + }) |
| 345 | + |
| 346 | + it('resolves the secret for an internal-JWT execution caller', async () => { |
| 347 | + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ |
| 348 | + success: true, |
| 349 | + authType: AuthType.INTERNAL_JWT, |
| 350 | + userId: 'test-user-id', |
| 351 | + }) |
| 352 | + mockAuthorizeCredentialUse.mockResolvedValueOnce(authorized) |
| 353 | + |
| 354 | + const response = await POST( |
| 355 | + createMockRequest('POST', { |
| 356 | + credentialId: 'saved-credential-id', |
| 357 | + toolId: 'vanta_list_tests', |
| 358 | + workflowId: 'workflow-id', |
| 359 | + }), |
| 360 | + createRouteContext({}) |
| 361 | + ) |
| 362 | + |
| 363 | + expect(response.status).toBe(200) |
| 364 | + await expect(response.json()).resolves.toMatchObject({ |
| 365 | + accessToken: 'saved-secret', |
| 366 | + credentialType: 'service_account', |
| 367 | + }) |
| 368 | + }) |
| 369 | + }) |
| 370 | + |
| 371 | + it('still returns an OAuth token to a session caller', async () => { |
| 372 | + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ |
| 373 | + success: true, |
| 374 | + authType: AuthType.SESSION, |
| 375 | + userId: 'test-user-id', |
| 376 | + }) |
| 377 | + mockAuthorizeCredentialUse.mockResolvedValueOnce({ |
| 378 | + ok: true, |
| 379 | + requesterUserId: 'test-user-id', |
| 380 | + credentialOwnerUserId: 'test-user-id', |
| 381 | + }) |
| 382 | + authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({ |
| 383 | + id: 'credential-id', |
| 384 | + accessToken: 'test-token', |
| 385 | + refreshToken: 'refresh-token', |
| 386 | + accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000), |
| 387 | + providerId: 'google', |
| 388 | + }) |
| 389 | + authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockResolvedValueOnce({ |
| 390 | + accessToken: 'session-oauth-token', |
| 391 | + refreshed: false, |
| 392 | + }) |
| 393 | + |
| 394 | + const response = await POST( |
| 395 | + createMockRequest('POST', { credentialId: 'credential-id' }), |
| 396 | + createRouteContext({}) |
| 397 | + ) |
| 398 | + |
| 399 | + expect(response.status).toBe(200) |
| 400 | + await expect(response.json()).resolves.toMatchObject({ |
| 401 | + accessToken: 'session-oauth-token', |
| 402 | + credentialType: 'oauth', |
| 403 | + }) |
| 404 | + }) |
| 405 | + |
293 | 406 | describe('credentialAccountUserId + providerId path', () => { |
294 | 407 | it('should reject internal JWT authentication', async () => { |
295 | 408 | hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ |
|
0 commit comments