Skip to content

Commit 80980da

Browse files
committed
fix(credentials): expire a coordinated token an internal operation's provider refuses
1 parent 6b64d80 commit 80980da

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
@@ -3896,6 +3896,8 @@ describe('Internal Route Trust', () => {
38963896
const toolId = 'test_internal_minted_token_refusal'
38973897
beforeEach(() => {
38983898
mockForgetRejectedClientCredentialToken.mockReset()
3899+
mockInvalidateCoordinatedClientToken.mockReset()
3900+
mockInvalidateCoordinatedClientToken.mockResolvedValue(undefined)
38993901
;(tools as Record<string, unknown>)[toolId] = {
39003902
id: toolId,
39013903
name: 'Internal Minted Token Refusal Test',
@@ -3939,6 +3941,9 @@ describe('Internal Route Trust', () => {
39393941

39403942
expect(result.success).toBe(false)
39413943
expect(mockForgetRejectedClientCredentialToken).toHaveBeenCalledWith('minted-token')
3944+
expect(mockInvalidateCoordinatedClientToken).toHaveBeenCalledWith('minted-token', {
3945+
minimumAgeMs: 60_000,
3946+
})
39423947
})
39433948

39443949
it('drops the cached token after a 403 only once it is old enough to blame', async () => {
@@ -3948,19 +3953,24 @@ describe('Internal Route Trust', () => {
39483953
expect(mockForgetRejectedClientCredentialToken).toHaveBeenCalledWith('minted-token', {
39493954
minimumAgeMs: 60_000,
39503955
})
3956+
expect(mockInvalidateCoordinatedClientToken).toHaveBeenCalledWith('minted-token', {
3957+
minimumAgeMs: 60_000,
3958+
})
39513959
})
39523960

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

39563964
expect(result.success).toBe(true)
39573965
expect(mockForgetRejectedClientCredentialToken).not.toHaveBeenCalled()
3966+
expect(mockInvalidateCoordinatedClientToken).not.toHaveBeenCalled()
39583967
})
39593968

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

39633972
expect(mockForgetRejectedClientCredentialToken).not.toHaveBeenCalled()
3973+
expect(mockInvalidateCoordinatedClientToken).not.toHaveBeenCalled()
39643974
})
39653975

39663976
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)