As of v3.219.0
of the @aws-sdk/cloudfront-signer
package, adding a response-content-disposition
query parameter to the URL being sent to the getSignedUrl
function will result in an Access Denied
error on the output Cloudfront URL:
const disposition = 'attachment;filename="test.txt"';
const signedUrl = getSignedUrl({
keyPairId: config.cloudfrontKeyPairId,
privateKey: config.cloudfrontSigningKey,
url: `https://d1x2y3z4.cloudfront.net/test.txt?response-content-disposition=${encodeURIComponent(
contentDisposition
)}"`,
dateLessThan: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
});
console.log(signedUrl);
// https://d1x2y3z4.cloudfront.net/test.txt?response-content-disposition=attachment;filename=newFilename.txt&Policy=XXXXXX&Key-Pair-Id=YYYYYY&Signature=ZZZZZZ
The above code will result in an Access Denied
error when the URL is accessed:
<Error>
<Code>AccessDenied</Code>
<Message>Access denied</Message>
</Error>
Evidently, the issue happens when the encoded URL (using encodeURIComponent
) is reverted by the getSignedUrl
function.
From the source, it seems that the input url
parameter is fed into the @aws-sdk/url-parser
package, which is presumably where the encoded URL is being lost in translation.
import { parseUrl } from "@aws-sdk/url-parser";
...
/**
* Creates a signed URL string using a canned or custom policy.
* @returns the input URL with signature attached as query parameters.
*/
export function getSignedUrl({
dateLessThan,
dateGreaterThan,
url,
keyPairId,
privateKey,
ipAddress,
policy,
}: CloudfrontSignInput): string {
const parsedUrl = parseUrl(url);
...
}
The Workaround
The workaround is to manually set the query parameter after parsing the signed URL:
const disposition = `attachment;filename=newFilename.txt;`;
const url = new URL(`test.txt`, `https://d1x2y3z4.cloudfront.net`);
url.searchParams.append('response-content-disposition', disposition);
const signedUrl = getSignedUrl({
url: url.href,
keyPairId: config.cloudfrontKeyPairId,
privateKey: config.cloudfrontSigningKey,
dateLessThan: expires.toISOString(),
dateLessThan: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
});
const fixedSignedUrl = new URL(signedUrl);
fixedSignedUrl.searchParams.set('response-content-disposition', disposition);
return fixedSignedUrl.href;
Which works out just fine! ๐
From the comments on this GitHub issue, a โfixโ doesnโt seem to be in the works. I ended up losing many a few hours trying to figure out what was going on. Hoping that this helps out someone else.