Skip to content

Commit c9585a8

Browse files
fix(auth): harden subject delegation and cookies
1 parent 4841ac4 commit c9585a8

2 files changed

Lines changed: 75 additions & 6 deletions

File tree

apps/sim/lib/auth/internal.test.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
* @vitest-environment node
33
*/
44

5+
import { serializePrincipal } from '@sim/auth/principal'
56
import { resetEnvMock } from '@sim/testing'
6-
import { decodeJwt } from 'jose'
7+
import { decodeJwt, SignJWT } from 'jose'
78
import { afterAll, describe, expect, it, vi } from 'vitest'
9+
import { env } from '@/lib/core/config/env'
810

911
vi.unmock('@/lib/auth/internal')
1012

@@ -181,7 +183,32 @@ describe('internal executor delegation claims', () => {
181183
).rejects.toBeInstanceOf(InvalidInternalDelegationTokenError)
182184
})
183185

184-
it('rejects laundering actorless or external principals into a Sim user subject', async () => {
186+
it('round-trips an authenticated chat subject without inventing a Sim user', async () => {
187+
const token = await generateInternalDelegationToken({
188+
workflowId: 'workflow-1',
189+
principal: {
190+
kind: 'system',
191+
serviceId: 'chat',
192+
workspaceId: 'workspace-1',
193+
workflowId: 'workflow-1',
194+
subject: { kind: 'authenticated_email', email: 'person@example.com' },
195+
},
196+
})
197+
198+
await expect(verifyInternalDelegationToken(token)).resolves.toMatchObject({
199+
workflowId: 'workflow-1',
200+
principal: {
201+
kind: 'system',
202+
serviceId: 'chat',
203+
workspaceId: 'workspace-1',
204+
workflowId: 'workflow-1',
205+
subject: { kind: 'authenticated_email', email: 'person@example.com' },
206+
},
207+
})
208+
expect(decodeJwt(token).sub).toBeUndefined()
209+
})
210+
211+
it('rejects laundering actorless or non-Sim principals into a Sim user subject', async () => {
185212
await expect(
186213
generateInternalDelegationToken({
187214
subjectUserId: 'billing-owner',
@@ -213,7 +240,49 @@ describe('internal executor delegation claims', () => {
213240
},
214241
},
215242
})
216-
).rejects.toThrow('External workflow subjects cannot be represented as Sim users')
243+
).rejects.toThrow('Non-Sim workflow subjects cannot be represented as Sim users')
244+
245+
await expect(
246+
generateInternalDelegationToken({
247+
subjectUserId: 'unrelated-user',
248+
workflowId: 'workflow-1',
249+
principal: {
250+
kind: 'system',
251+
serviceId: 'chat',
252+
workspaceId: 'workspace-1',
253+
workflowId: 'workflow-1',
254+
subject: { kind: 'authenticated_email', email: 'person@example.com' },
255+
},
256+
})
257+
).rejects.toThrow('Non-Sim workflow subjects cannot be represented as Sim users')
258+
})
259+
260+
it('rejects a signed delegation that pairs a non-Sim principal with a Sim user subject', async () => {
261+
const issuedAt = Math.floor(Date.now() / 1000)
262+
const token = await new SignJWT({
263+
type: 'internal_delegation',
264+
serviceId: 'executor',
265+
workflowId: 'workflow-1',
266+
principal: serializePrincipal({
267+
kind: 'system',
268+
serviceId: 'chat',
269+
workspaceId: 'workspace-1',
270+
workflowId: 'workflow-1',
271+
subject: { kind: 'authenticated_email', email: 'person@example.com' },
272+
}),
273+
})
274+
.setProtectedHeader({ alg: 'HS256' })
275+
.setJti('delegation-1')
276+
.setSubject('unrelated-user')
277+
.setIssuedAt(issuedAt)
278+
.setExpirationTime(issuedAt + 5 * 60)
279+
.setIssuer('sim-internal')
280+
.setAudience('sim-api')
281+
.sign(new TextEncoder().encode(env.INTERNAL_API_SECRET))
282+
283+
await expect(verifyInternalDelegationToken(token)).rejects.toBeInstanceOf(
284+
InvalidInternalDelegationTokenError
285+
)
217286
})
218287

219288
it('derives issued-at and expiry from one timestamp', async () => {

apps/sim/lib/auth/internal.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ export async function generateInternalDelegationToken(
138138
? requireNonEmptyDelegationClaim(input.subjectUserId, 'subjectUserId')
139139
: undefined
140140
const principalSubject = input.principal ? resolvePrincipalSubject(input.principal) : null
141-
if (principalSubject?.kind === 'external_user' && suppliedSubjectUserId) {
142-
throw new Error('External workflow subjects cannot be represented as Sim users')
141+
if (principalSubject && principalSubject.kind !== 'sim_user' && suppliedSubjectUserId) {
142+
throw new Error('Non-Sim workflow subjects cannot be represented as Sim users')
143143
}
144144
if (!principalSubject && input.principal && suppliedSubjectUserId) {
145145
throw new Error('Actorless workflow principals cannot be represented as Sim users')
@@ -247,7 +247,7 @@ export async function verifyInternalDelegationToken(
247247
if (
248248
(!principal && !subjectUserId) ||
249249
(principalSubject?.kind === 'sim_user' && principalSubject.userId !== subjectUserId) ||
250-
(principalSubject?.kind === 'external_user' && subjectUserId) ||
250+
(principalSubject && principalSubject.kind !== 'sim_user' && subjectUserId) ||
251251
(principal && !principalSubject && subjectUserId)
252252
) {
253253
throw new InvalidInternalDelegationTokenError()

0 commit comments

Comments
 (0)