Skip to content

Commit

Permalink
Merge pull request #1150 from middyjs/bug/InvalidSignatureException
Browse files Browse the repository at this point in the history
Add in retry for expired signatures
  • Loading branch information
willfarrell authored Dec 30, 2023
2 parents 2d9096a + 4db473e commit 94e8fa6
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 55 deletions.
21 changes: 12 additions & 9 deletions packages/appconfig/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
getInternal,
processCache,
modifyCache,
jsonSafeParse
jsonSafeParse,
catchInvalidSignatureException
} from '@middy/util'
import {
StartConfigurationSessionCommand,
Expand Down Expand Up @@ -36,12 +37,12 @@ const appConfigMiddleware = (opts = {}) => {
const configurationCache = {}

function fetchLatestConfiguration (configToken, internalKey) {
const command = new GetLatestConfigurationCommand({
ConfigurationToken: configToken
})
return client
.send(
new GetLatestConfigurationCommand({
ConfigurationToken: configToken
})
)
.send(command)
.catch((e) => catchInvalidSignatureException(e, client, command))
.then((configResp) => {
configurationTokenCache[internalKey] =
configResp.NextPollConfigurationToken
Expand Down Expand Up @@ -70,10 +71,12 @@ const appConfigMiddleware = (opts = {}) => {
for (const internalKey of Object.keys(options.fetchData)) {
if (cachedValues[internalKey]) continue
if (configurationTokenCache[internalKey] == null) {
const command = new StartConfigurationSessionCommand(
options.fetchData[internalKey]
)
values[internalKey] = client
.send(
new StartConfigurationSessionCommand(options.fetchData[internalKey])
)
.send(command)
.catch((e) => catchInvalidSignatureException(e, client, command))
.then((configSessionResp) =>
fetchLatestConfiguration(
configSessionResp.InitialConfigurationToken,
Expand Down
7 changes: 5 additions & 2 deletions packages/dynamodb/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
getCache,
getInternal,
processCache,
modifyCache
modifyCache,
catchInvalidSignatureException
} from '@middy/util'
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb'
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'
Expand Down Expand Up @@ -40,8 +41,10 @@ const dynamodbMiddleware = (opts = {}) => {
for (const internalKey in options.fetchData) {
if (cachedValues[internalKey]) continue
const inputParameters = options.fetchData[internalKey]
const command = new GetItemCommand(inputParameters)
values[internalKey] = client
.send(new GetItemCommand(inputParameters))
.send(command)
.catch((e) => catchInvalidSignatureException(e, client, command))
.then((resp) => unmarshall(resp.Item))
.catch((e) => {
const value = getCache(options.cacheKey).value ?? {}
Expand Down
13 changes: 10 additions & 3 deletions packages/s3-object-response/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { canPrefetch, createPrefetchClient, createClient } from '@middy/util'
import {
canPrefetch,
createPrefetchClient,
createClient
// catchInvalidSignatureException
} from '@middy/util'

import { S3Client, WriteGetObjectResponseCommand } from '@aws-sdk/client-s3'

Expand Down Expand Up @@ -37,9 +42,11 @@ const s3ObjectResponseMiddleware = (opts = {}) => {
delete request.response.body
}

await client.send(
new WriteGetObjectResponseCommand(request.internal.s3ObjectResponse)
const command = new WriteGetObjectResponseCommand(
request.internal.s3ObjectResponse
)
await client.send(command) // Doesn't return a promise?
// .catch((e) => catchInvalidSignatureException(e, client, command))

return { statusCode: 200 }
}
Expand Down
7 changes: 5 additions & 2 deletions packages/s3/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
getInternal,
processCache,
modifyCache,
jsonSafeParse
jsonSafeParse,
catchInvalidSignatureException
} from '@middy/util'
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'
const defaults = {
Expand All @@ -31,8 +32,10 @@ const s3Middleware = (opts = {}) => {
const values = {}
for (const internalKey of Object.keys(options.fetchData)) {
if (cachedValues[internalKey]) continue
const command = new GetObjectCommand(options.fetchData[internalKey])
values[internalKey] = client
.send(new GetObjectCommand(options.fetchData[internalKey]))
.send(command)
.catch((e) => catchInvalidSignatureException(e, client, command))
.then(async (resp) => {
let value = await resp.Body.transformToString()
if (contentTypePattern.test(resp.ContentType)) {
Expand Down
9 changes: 5 additions & 4 deletions packages/secrets-manager/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,12 +289,13 @@ test.serial(
test.serial(
'It should call aws-sdk if cache enabled but cached param has expired using LastRotationDate, fallback to NextRotationDate',
async (t) => {
const now = Date.now() / 1000
const mockService = mockClient(SecretsManagerClient)
.on(DescribeSecretCommand, { SecretId: 'api_key' })
.resolves({
LastRotationDate: Date.now() / 1000 - 25,
LastChangedDate: Date.now() / 1000 - 25,
NextRotationDate: Date.now() / 1000 + 50
LastRotationDate: now - 25,
LastChangedDate: now - 25,
NextRotationDate: now + 50
})
.on(GetSecretValueCommand, { SecretId: 'api_key' })
.resolves({ SecretString: 'token' })
Expand Down Expand Up @@ -325,7 +326,7 @@ test.serial(
await setTimeout(100)
await handler(event, context)

t.is(sendStub.callCount, 2)
t.is(sendStub.callCount, 4)
}
)

Expand Down
28 changes: 15 additions & 13 deletions packages/secrets-manager/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
getInternal,
processCache,
modifyCache,
jsonSafeParse
jsonSafeParse,
catchInvalidSignatureException
} from '@middy/util'
import {
SecretsManagerClient,
Expand Down Expand Up @@ -43,12 +44,12 @@ const secretsManagerMiddleware = (opts = {}) => {
options.fetchRotationDate === true ||
options.fetchRotationDate?.[internalKey]
) {
const command = new DescribeSecretCommand({
SecretId: options.fetchData[internalKey]
})
return client
.send(
new DescribeSecretCommand({
SecretId: options.fetchData[internalKey]
})
)
.send(command)
.catch((e) => catchInvalidSignatureException(e, client, command))
.then((resp) => {
if (options.cacheExpiry < 0) {
options.cacheKeyExpiry[internalKey] =
Expand All @@ -64,13 +65,14 @@ const secretsManagerMiddleware = (opts = {}) => {
})
}
})
.then(() =>
client.send(
new GetSecretValueCommand({
SecretId: options.fetchData[internalKey]
})
)
)
.then(() => {
const command = new GetSecretValueCommand({
SecretId: options.fetchData[internalKey]
})
return client
.send(command)
.catch((e) => catchInvalidSignatureException(e, client, command))
})
.then((resp) => jsonSafeParse(resp.SecretString))
.catch((e) => {
const value = getCache(options.cacheKey).value ?? {}
Expand Down
4 changes: 3 additions & 1 deletion packages/service-discovery/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
getCache,
getInternal,
processCache,
modifyCache
modifyCache,
catchInvalidSignatureException
} from '@middy/util'
import {
ServiceDiscoveryClient,
Expand Down Expand Up @@ -39,6 +40,7 @@ const serviceDiscoveryMiddleware = (opts = {}) => {
)
values[internalKey] = client
.send(command)
.catch((e) => catchInvalidSignatureException(e, client, command))
.then((resp) => resp.Instances)
.catch((e) => {
const value = getCache(options.cacheKey).value ?? {}
Expand Down
49 changes: 49 additions & 0 deletions packages/ssm/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,55 @@ test.serial(
}
)

test.serial(
'It should it should recover from an error if cache enabled but cached param has expired',
async (t) => {
const awsError = new Error(
'InvalidSignatureException: Signature expired: 20231103T171116Z is now earlier than 20231103T171224Z (20231103T171724Z - 5 min.)'
)
awsError.__type = 'InvalidSignatureException'
const mockService = mockClient(SSMClient)
.on(GetParametersCommand, {
Names: ['/dev/service_name/key_name'],
WithDecryption: true
})
.resolvesOnce({
Parameters: [{ Name: '/dev/service_name/key_name', Value: 'key-value' }]
})
.rejectsOnce(awsError)
.resolves({
Parameters: [{ Name: '/dev/service_name/key_name', Value: 'key-value' }]
})
const sendStub = mockService.send

const middleware = async (request) => {
const values = await getInternal(true, request)
t.is(values.key, 'key-value')
}

const handler = middy(() => {})
.use(
ssm({
AwsClient: SSMClient,
cacheExpiry: 4,
fetchData: {
key: '/dev/service_name/key_name'
},
disablePrefetch: true
})
)
.before(middleware)

await handler(event, context)
await setTimeout(5)
await handler(event, context)
await setTimeout(5)
await handler(event, context)

t.is(sendStub.callCount, 4)
}
)

test.serial(
'It should throw error if InvalidParameters returned',
async (t) => {
Expand Down
33 changes: 17 additions & 16 deletions packages/ssm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {
modifyCache,
jsonSafeParse,
getInternal,
sanitizeKey
} from '@middy/util'
sanitizeKey,
catchInvalidSignatureException
} from '../util/index.js'
import {
SSMClient,
GetParametersCommand,
Expand Down Expand Up @@ -66,13 +67,13 @@ const ssmMiddleware = (opts = {}) => {
continue
}

const command = new GetParametersCommand({
Names: batchFetchKeys,
WithDecryption: true
})
batchReq = client
.send(
new GetParametersCommand({
Names: batchFetchKeys,
WithDecryption: true
})
)
.send(command)
.catch((e) => catchInvalidSignatureException(e, client, command))
.then((resp) => {
// Don't sanitize key, mapped to set value in options
return Object.assign(
Expand Down Expand Up @@ -131,15 +132,15 @@ const ssmMiddleware = (opts = {}) => {
}

const fetchPath = (path, nextToken, values = {}) => {
const command = new GetParametersByPathCommand({
Path: path,
NextToken: nextToken,
Recursive: true,
WithDecryption: true
})
return client
.send(
new GetParametersByPathCommand({
Path: path,
NextToken: nextToken,
Recursive: true,
WithDecryption: true
})
)
.send(command)
.catch((e) => catchInvalidSignatureException(e, client, command))
.then((resp) => {
Object.assign(
values,
Expand Down
7 changes: 5 additions & 2 deletions packages/sts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
getCache,
getInternal,
processCache,
modifyCache
modifyCache,
catchInvalidSignatureException
} from '@middy/util'
import { STSClient, AssumeRoleCommand } from '@aws-sdk/client-sts'

Expand Down Expand Up @@ -34,8 +35,10 @@ const stsMiddleware = (opts = {}) => {
// Date cannot be used here to assign default session name, possibility of collision when > 1 role defined
assumeRoleOptions.RoleSessionName ??=
'middy-sts-session-' + Math.ceil(Math.random() * 99999)
const command = new AssumeRoleCommand(assumeRoleOptions)
values[internalKey] = client
.send(new AssumeRoleCommand(assumeRoleOptions))
.send(command)
.catch((e) => catchInvalidSignatureException(e, client, command))
.then((resp) => ({
accessKeyId: resp.Credentials.AccessKeyId,
secretAccessKey: resp.Credentials.SecretAccessKey,
Expand Down
27 changes: 26 additions & 1 deletion packages/util/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
createClient,
canPrefetch,
getInternal,
getCache,
processCache,
catchInvalidSignatureException,
getCache,
modifyCache,
clearCache,
jsonSafeParse,
Expand Down Expand Up @@ -500,6 +501,30 @@ test.serial('processCache should clear all cache', async (t) => {
clearCache()
})

// catchInvalidSignatureException
test.serial(
'catchInvalidSignatureException should retry when InvalidSignatureException',
async (t) => {
const e = new Error('InvalidSignatureException')
e.__type = 'InvalidSignatureException'
const client = { send: sinon.stub() }
catchInvalidSignatureException(e, client, 'command')
t.is(client.send.callCount, 1)
}
)

test.serial(
'catchInvalidSignatureException should throw when not InvalidSignatureException',
async (t) => {
const e = new Error('error')
try {
catchInvalidSignatureException(e)
} catch (e) {
t.is(e.message, 'error')
}
}
)

// modifyCache
test.serial(
'modifyCache should not override value when it does not exist',
Expand Down
7 changes: 7 additions & 0 deletions packages/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ export const processCache = (options, fetch = () => undefined, request) => {
return { value, expiry }
}

export const catchInvalidSignatureException = (e, client, command) => {
if (e.__type === 'InvalidSignatureException') {
return client.send(command)
}
throw e
}

export const getCache = (key) => {
if (!cache[key]) return {}
return cache[key]
Expand Down
Loading

0 comments on commit 94e8fa6

Please sign in to comment.