Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions apps/sim/app/api/auth/oauth/token/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('OAuth Token API Routes', () => {
const mockGetUserId = vi.fn()
const mockGetCredential = vi.fn()
const mockRefreshTokenIfNeeded = vi.fn()
const mockGetOAuthToken = vi.fn()
const mockAuthorizeCredentialUse = vi.fn()
const mockCheckHybridAuth = vi.fn()

Expand All @@ -29,6 +30,7 @@ describe('OAuth Token API Routes', () => {
getUserId: mockGetUserId,
getCredential: mockGetCredential,
refreshTokenIfNeeded: mockRefreshTokenIfNeeded,
getOAuthToken: mockGetOAuthToken,
}))

vi.doMock('@sim/logger', () => ({
Expand Down Expand Up @@ -230,6 +232,140 @@ describe('OAuth Token API Routes', () => {
expect(response.status).toBe(401)
expect(data).toHaveProperty('error', 'Failed to refresh access token')
})

describe('credentialAccountUserId + providerId path', () => {
it('should reject unauthenticated requests', async () => {
mockCheckHybridAuth.mockResolvedValueOnce({
success: false,
error: 'Authentication required',
})

const req = createMockRequest('POST', {
credentialAccountUserId: 'target-user-id',
providerId: 'google',
})

const { POST } = await import('@/app/api/auth/oauth/token/route')

const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(401)
expect(data).toHaveProperty('error', 'User not authenticated')
expect(mockGetOAuthToken).not.toHaveBeenCalled()
})

it('should reject API key authentication', async () => {
mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
authType: 'api_key',
userId: 'test-user-id',
})

const req = createMockRequest('POST', {
credentialAccountUserId: 'test-user-id',
providerId: 'google',
})

const { POST } = await import('@/app/api/auth/oauth/token/route')

const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(401)
expect(data).toHaveProperty('error', 'User not authenticated')
expect(mockGetOAuthToken).not.toHaveBeenCalled()
})

it('should reject internal JWT authentication', async () => {
mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
authType: 'internal_jwt',
userId: 'test-user-id',
})

const req = createMockRequest('POST', {
credentialAccountUserId: 'test-user-id',
providerId: 'google',
})

const { POST } = await import('@/app/api/auth/oauth/token/route')

const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(401)
expect(data).toHaveProperty('error', 'User not authenticated')
expect(mockGetOAuthToken).not.toHaveBeenCalled()
})

it('should reject requests for other users credentials', async () => {
mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
authType: 'session',
userId: 'attacker-user-id',
})

const req = createMockRequest('POST', {
credentialAccountUserId: 'victim-user-id',
providerId: 'google',
})

const { POST } = await import('@/app/api/auth/oauth/token/route')

const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(403)
expect(data).toHaveProperty('error', 'Unauthorized')
expect(mockGetOAuthToken).not.toHaveBeenCalled()
})

it('should allow session-authenticated users to access their own credentials', async () => {
mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
authType: 'session',
userId: 'test-user-id',
})
mockGetOAuthToken.mockResolvedValueOnce('valid-access-token')

const req = createMockRequest('POST', {
credentialAccountUserId: 'test-user-id',
providerId: 'google',
})

const { POST } = await import('@/app/api/auth/oauth/token/route')

const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(200)
expect(data).toHaveProperty('accessToken', 'valid-access-token')
expect(mockGetOAuthToken).toHaveBeenCalledWith('test-user-id', 'google')
})

it('should return 404 when credential not found for user', async () => {
mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
authType: 'session',
userId: 'test-user-id',
})
mockGetOAuthToken.mockResolvedValueOnce(null)

const req = createMockRequest('POST', {
credentialAccountUserId: 'test-user-id',
providerId: 'nonexistent-provider',
})

const { POST } = await import('@/app/api/auth/oauth/token/route')

const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(404)
expect(data.error).toContain('No credential found')
})
})
})

/**
Expand Down
16 changes: 16 additions & 0 deletions apps/sim/app/api/auth/oauth/token/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ export async function POST(request: NextRequest) {
providerId,
})

const auth = await checkHybridAuth(request, { requireWorkflowId: false })
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized request for credentialAccountUserId path`, {
success: auth.success,
authType: auth.authType,
})
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}

if (auth.userId !== credentialAccountUserId) {
logger.warn(
`[${requestId}] User ${auth.userId} attempted to access credentials for ${credentialAccountUserId}`
)
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 })
}

try {
const accessToken = await getOAuthToken(credentialAccountUserId, providerId)
if (!accessToken) {
Expand Down
10 changes: 8 additions & 2 deletions apps/sim/app/api/tools/asana/add-comment/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'

export const dynamic = 'force-dynamic'

const logger = createLogger('AsanaAddCommentAPI')

export async function POST(request: Request) {
export async function POST(request: NextRequest) {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const { accessToken, taskGid, text } = await request.json()

if (!accessToken) {
Expand Down
10 changes: 8 additions & 2 deletions apps/sim/app/api/tools/asana/create-task/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'

export const dynamic = 'force-dynamic'

const logger = createLogger('AsanaCreateTaskAPI')

export async function POST(request: Request) {
export async function POST(request: NextRequest) {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const { accessToken, workspace, name, notes, assignee, due_on } = await request.json()

if (!accessToken) {
Expand Down
10 changes: 8 additions & 2 deletions apps/sim/app/api/tools/asana/get-projects/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'

export const dynamic = 'force-dynamic'

const logger = createLogger('AsanaGetProjectsAPI')

export async function POST(request: Request) {
export async function POST(request: NextRequest) {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const { accessToken, workspace } = await request.json()

if (!accessToken) {
Expand Down
10 changes: 8 additions & 2 deletions apps/sim/app/api/tools/asana/get-task/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'

export const dynamic = 'force-dynamic'

const logger = createLogger('AsanaGetTaskAPI')

export async function POST(request: Request) {
export async function POST(request: NextRequest) {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const { accessToken, taskGid, workspace, project, limit } = await request.json()

if (!accessToken) {
Expand Down
10 changes: 8 additions & 2 deletions apps/sim/app/api/tools/asana/search-tasks/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'

export const dynamic = 'force-dynamic'

const logger = createLogger('AsanaSearchTasksAPI')

export async function POST(request: Request) {
export async function POST(request: NextRequest) {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const { accessToken, workspace, text, assignee, projects, completed } = await request.json()

if (!accessToken) {
Expand Down
10 changes: 8 additions & 2 deletions apps/sim/app/api/tools/asana/update-task/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'

export const dynamic = 'force-dynamic'

const logger = createLogger('AsanaUpdateTaskAPI')

export async function PUT(request: Request) {
export async function PUT(request: NextRequest) {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const { accessToken, taskGid, name, notes, assignee, completed, due_on } = await request.json()

if (!accessToken) {
Expand Down
10 changes: 8 additions & 2 deletions apps/sim/app/api/tools/confluence/attachment/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'

Expand All @@ -8,8 +9,13 @@ const logger = createLogger('ConfluenceAttachmentAPI')
export const dynamic = 'force-dynamic'

// Delete an attachment
export async function DELETE(request: Request) {
export async function DELETE(request: NextRequest) {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const { domain, accessToken, cloudId: providedCloudId, attachmentId } = await request.json()

if (!domain) {
Expand Down
10 changes: 8 additions & 2 deletions apps/sim/app/api/tools/confluence/attachments/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'

Expand All @@ -8,8 +9,13 @@ const logger = createLogger('ConfluenceAttachmentsAPI')
export const dynamic = 'force-dynamic'

// List attachments on a page
export async function GET(request: Request) {
export async function GET(request: NextRequest) {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const { searchParams } = new URL(request.url)
const domain = searchParams.get('domain')
const accessToken = searchParams.get('accessToken')
Expand Down
17 changes: 14 additions & 3 deletions apps/sim/app/api/tools/confluence/comment/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'

Expand Down Expand Up @@ -46,8 +47,13 @@ const deleteCommentSchema = z
)

// Update a comment
export async function PUT(request: Request) {
export async function PUT(request: NextRequest) {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const body = await request.json()

const validation = putCommentSchema.safeParse(body)
Expand Down Expand Up @@ -128,8 +134,13 @@ export async function PUT(request: Request) {
}

// Delete a comment
export async function DELETE(request: Request) {
export async function DELETE(request: NextRequest) {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const body = await request.json()

const validation = deleteCommentSchema.safeParse(body)
Expand Down
Loading