Skip to content

Commit 4a2a6e7

Browse files
fix(credentials): isolate managed delegation
1 parent caad5d0 commit 4a2a6e7

2 files changed

Lines changed: 47 additions & 18 deletions

File tree

apps/sim/app/api/auth/oauth/token/route.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,38 @@ describe('OAuth Token API Routes', () => {
128128
expect(authOAuthUtilsMockFns.mockGetCredential).toHaveBeenCalled()
129129
})
130130

131+
it('does not authenticate managed delegation for an ordinary OAuth credential', async () => {
132+
mockAuthorizeCredentialUse.mockResolvedValueOnce({
133+
ok: true,
134+
authType: 'internal_jwt',
135+
requesterUserId: 'workflow-owner-id',
136+
credentialOwnerUserId: 'workflow-owner-id',
137+
})
138+
authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({
139+
id: 'credential-id',
140+
accessToken: 'test-token',
141+
refreshToken: 'refresh-token',
142+
accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000),
143+
providerId: 'google',
144+
})
145+
authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockResolvedValueOnce({
146+
accessToken: 'fresh-token',
147+
refreshed: false,
148+
})
149+
150+
const response = await POST(
151+
createMockRequest(
152+
'POST',
153+
{ credentialId: 'credential-id', workflowId: 'workflow-id' },
154+
{ 'x-sim-managed-oauth-delegation': 'Bearer stale-delegation' }
155+
)
156+
)
157+
158+
expect(response.status).toBe(200)
159+
await expect(response.json()).resolves.toMatchObject({ accessToken: 'fresh-token' })
160+
expect(mockAuthenticateManagedOAuthDelegation).not.toHaveBeenCalled()
161+
})
162+
131163
it('should handle missing credentialId', async () => {
132164
const req = createMockRequest('POST', {})
133165

apps/sim/app/api/auth/oauth/token/route.ts

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,24 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
127127
}
128128
}
129129

130-
let managedOAuthPrincipal: WorkflowExecutionDelegatedPrincipal | null = null
131-
const managedOAuthDelegation = parsed.data.headers?.[MANAGED_OAUTH_DELEGATION_HEADER]
132-
if (managedOAuthDelegation && credentialId) {
130+
const resolved = credentialId ? await resolveOAuthAccountId(credentialId) : null
131+
if (resolved?.credentialType === 'managed_oauth' && resolved.credentialId) {
132+
const managedOAuthDelegation = parsed.data.headers?.[MANAGED_OAUTH_DELEGATION_HEADER]
133+
if (!managedOAuthDelegation) {
134+
return NextResponse.json(
135+
{
136+
code: 'MANAGED_CREDENTIAL_DELEGATION_REQUIRED',
137+
error: 'Managed credentials can only be used by an authenticated workflow execution',
138+
},
139+
{ status: 403 }
140+
)
141+
}
142+
143+
let managedOAuthPrincipal: WorkflowExecutionDelegatedPrincipal
133144
try {
134145
managedOAuthPrincipal = await authenticateManagedOAuthDelegation(
135146
managedOAuthDelegation,
136-
credentialId
147+
resolved.credentialId
137148
)
138149
} catch (error) {
139150
if (!(error instanceof InvalidManagedOAuthDelegationError)) throw error
@@ -145,10 +156,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
145156
{ status: 401 }
146157
)
147158
}
148-
}
149-
150-
const resolved = credentialId ? await resolveOAuthAccountId(credentialId) : null
151-
if (resolved?.credentialType === 'managed_oauth' && resolved.credentialId) {
152159
if (!toolId) {
153160
return NextResponse.json(
154161
{
@@ -187,16 +194,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
187194
)
188195
}
189196

190-
if (!managedOAuthPrincipal) {
191-
return NextResponse.json(
192-
{
193-
code: 'MANAGED_CREDENTIAL_DELEGATION_REQUIRED',
194-
error: 'Managed credentials can only be used by an authenticated workflow execution',
195-
},
196-
{ status: 403 }
197-
)
198-
}
199-
200197
try {
201198
const result = await resolveManagedOAuthCredentialToken.execute({
202199
principal: managedOAuthPrincipal,

0 commit comments

Comments
 (0)