Skip to content

Commit 5f723df

Browse files
committed
fix(credentials): expire a coordinated token an internal operation's provider refuses
1 parent 50a022d commit 5f723df

2 files changed

Lines changed: 23 additions & 0 deletions

File tree

‎apps/sim/tools/index.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3894,6 +3894,8 @@ describe('Internal Route Trust', () => {
38943894
const toolId = 'test_internal_minted_token_refusal'
38953895
beforeEach(() => {
38963896
mockForgetRejectedClientCredentialToken.mockReset()
3897+
mockInvalidateCoordinatedClientToken.mockReset()
3898+
mockInvalidateCoordinatedClientToken.mockResolvedValue(undefined)
38973899
;(tools as Record<string, unknown>)[toolId] = {
38983900
id: toolId,
38993901
name: 'Internal Minted Token Refusal Test',
@@ -3937,6 +3939,9 @@ describe('Internal Route Trust', () => {
39373939

39383940
expect(result.success).toBe(false)
39393941
expect(mockForgetRejectedClientCredentialToken).toHaveBeenCalledWith('minted-token')
3942+
expect(mockInvalidateCoordinatedClientToken).toHaveBeenCalledWith('minted-token', {
3943+
minimumAgeMs: 60_000,
3944+
})
39403945
})
39413946

39423947
it('drops the cached token after a 403 only once it is old enough to blame', async () => {
@@ -3946,19 +3951,24 @@ describe('Internal Route Trust', () => {
39463951
expect(mockForgetRejectedClientCredentialToken).toHaveBeenCalledWith('minted-token', {
39473952
minimumAgeMs: 60_000,
39483953
})
3954+
expect(mockInvalidateCoordinatedClientToken).toHaveBeenCalledWith('minted-token', {
3955+
minimumAgeMs: 60_000,
3956+
})
39493957
})
39503958

39513959
it('keeps the token after a call the provider accepts', async () => {
39523960
const result = await runWith('service_account', 200)
39533961

39543962
expect(result.success).toBe(true)
39553963
expect(mockForgetRejectedClientCredentialToken).not.toHaveBeenCalled()
3964+
expect(mockInvalidateCoordinatedClientToken).not.toHaveBeenCalled()
39563965
})
39573966

39583967
it("never drops a signed-in account's token", async () => {
39593968
await runWith('oauth', 401)
39603969

39613970
expect(mockForgetRejectedClientCredentialToken).not.toHaveBeenCalled()
3971+
expect(mockInvalidateCoordinatedClientToken).not.toHaveBeenCalled()
39623972
})
39633973

39643974
it('reports an expired saved token instead of the bare refusal', async () => {

‎apps/sim/tools/index.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2981,6 +2981,7 @@ async function executeDeclaredInternalOperation({
29812981
requestId,
29822982
result.status === 403 ? REJECTED_TOKEN_MINIMUM_AGE_MS : undefined
29832983
)
2984+
await expireRefusedCoordinatedToken(params.accessToken, requestId)
29842985
}
29852986
if (result instanceof Response && result.status === 401 && refreshCredential) {
29862987
await result.body?.cancel()
@@ -3098,6 +3099,18 @@ async function expireRejectedServiceAccountToken(
30983099
if (!(error instanceof Error) || !('status' in error) || error.status !== 401) return
30993100
if (typeof accessToken !== 'string' || !accessToken || reportsScopeRefusal(error)) return
31003101
await forgetRefusedServiceAccountToken(accessToken, requestId)
3102+
await expireRefusedCoordinatedToken(accessToken, requestId)
3103+
}
3104+
3105+
/**
3106+
* Expires a refused token coordinated across workers (a client-credentials grant's shared token),
3107+
* so the next call on any worker mints a replacement. A token minted within
3108+
* {@link REJECTED_TOKEN_MINIMUM_AGE_MS} is kept, since the refusal may come from the endpoint.
3109+
*/
3110+
async function expireRefusedCoordinatedToken(
3111+
accessToken: string,
3112+
requestId: string
3113+
): Promise<void> {
31013114
try {
31023115
const { invalidateCoordinatedClientToken } = await import(
31033116
'@/lib/credentials/coordinated-client-token'

0 commit comments

Comments
 (0)