diff --git a/packages/cloudfront-signer/README.md b/packages/cloudfront-signer/README.md index db1ff01bb59f..9c870211ec94 100644 --- a/packages/cloudfront-signer/README.md +++ b/packages/cloudfront-signer/README.md @@ -27,7 +27,43 @@ const signedUrl = getSignedUrl({ }); ``` -### Sign a URL for cookies +### Sign a URL with a Policy + +```javascript +import { getSignedUrl } from "@aws-sdk/cloudfront-signer"; // ESM +// const { getSignedUrl } = require("@aws-sdk/cloudfront-signer"); // CJS + +const cloudfrontDistributionDomain = "https://d111111abcdef8.cloudfront.net"; +const s3ObjectKey = "private-content/private.jpeg"; +const url = `${cloudfrontDistributionDomain}/${s3ObjectKey}`; +const privateKey = "CONTENTS-OF-PRIVATE-KEY"; +const keyPairId = "PUBLIC-KEY-ID-OF-CLOUDFRONT-KEY-PAIR"; +const dateLessThan = "2022-01-01"; + +const policy = { + Statement: [ + { + Resource: url, + Condition: { + DateLessThan: { + "AWS:EpochTime": new Date(dateLessThan).getTime() / 1000, // time in seconds + }, + }, + }, + ], +}; + +const policyString = JSON.stringify(policy); + +const cookies = getSignedUrl({ + keyPairId, + privateKey, + policy: policyString, + // url is automatically extracted from the policy, however you could still overwrite it if needed +}); +``` + +### Get signed cookies for a resource ```javascript import { getSignedCookies } from "@aws-sdk/cloudfront-signer"; // ESM @@ -47,3 +83,38 @@ const cookies = getSignedCookies({ privateKey, }); ``` + +### Get signed cookies with a Policy + +```javascript +import { getSignedCookies } from "@aws-sdk/cloudfront-signer"; // ESM +// const { getSignedCookies } = require("@aws-sdk/cloudfront-signer"); // CJS + +const cloudfrontDistributionDomain = "https://d111111abcdef8.cloudfront.net"; +const s3ObjectKey = "private-content/private.jpeg"; +const url = `${cloudfrontDistributionDomain}/${s3ObjectKey}`; +const privateKey = "CONTENTS-OF-PRIVATE-KEY"; +const keyPairId = "PUBLIC-KEY-ID-OF-CLOUDFRONT-KEY-PAIR"; +const dateLessThan = "2022-01-01"; + +const policy = { + Statement: [ + { + Resource: url, + Condition: { + DateLessThan: { + "AWS:EpochTime": new Date(dateLessThan).getTime() / 1000, // time in seconds + }, + }, + }, + ], +}; + +const policyString = JSON.stringify(policy); + +const cookies = getSignedCookies({ + keyPairId, + privateKey, + policy: policyString, +}); +``` diff --git a/packages/cloudfront-signer/src/sign.spec.ts b/packages/cloudfront-signer/src/sign.spec.ts index d98d879989db..5f17caf852fa 100644 --- a/packages/cloudfront-signer/src/sign.spec.ts +++ b/packages/cloudfront-signer/src/sign.spec.ts @@ -1,8 +1,5 @@ import { parseUrl } from "@smithy/url-parser"; import { createSign, createVerify } from "crypto"; -import { mkdtempSync, rmdirSync, writeFileSync } from "fs"; -import { tmpdir } from "os"; -import { resolve } from "path"; import { getSignedCookies, getSignedUrl } from "./index"; @@ -341,6 +338,19 @@ describe("getSignedUrl", () => { const signatureQueryParam = denormalizeBase64(signature); expect(verifySignature(signatureQueryParam, policy)).toBeTruthy(); }); + it("should sign a URL automatically extracted from a policy provided by the user", () => { + const policy = JSON.stringify({ Statement: [{ Resource: url }] }); + const result = getSignedUrl({ + keyPairId, + privateKey, + policy, + passphrase, + }); + const signature = createSignature(policy); + expect(result).toBe(`${url}?Policy=${encodeToBase64(policy)}&Key-Pair-Id=${keyPairId}&Signature=${signature}`); + const signatureQueryParam = denormalizeBase64(signature); + expect(verifySignature(signatureQueryParam, policy)).toBeTruthy(); + }); }); describe("getSignedCookies", () => { @@ -573,10 +583,9 @@ describe("getSignedCookies", () => { expect(result["CloudFront-Signature"]).toBe(expected["CloudFront-Signature"]); expect(verifySignature(denormalizeBase64(result["CloudFront-Signature"]), policyStr)).toBeTruthy(); }); - it("should sign a URL with a policy provided by the user", () => { + it("should sign cookies with a policy provided by the user without a url", () => { const policy = '{"foo":"bar"}'; const result = getSignedCookies({ - url, keyPairId, privateKey, policy, diff --git a/packages/cloudfront-signer/src/sign.ts b/packages/cloudfront-signer/src/sign.ts index 2022637702b4..016c61c43165 100644 --- a/packages/cloudfront-signer/src/sign.ts +++ b/packages/cloudfront-signer/src/sign.ts @@ -1,49 +1,68 @@ import { createSign } from "crypto"; -/** Input type to getSignedUrl and getSignedCookies. */ +/** + * Input type to getSignedUrl and getSignedCookies. + * @public + */ export type CloudfrontSignInput = CloudfrontSignInputWithParameters | CloudfrontSignInputWithPolicy; -export interface CloudfrontSignInputBase { - /** The URL string to sign. */ - url: string; +/** + * @public + */ +export type CloudfrontSignerCredentials = { /** The ID of the Cloudfront key pair. */ keyPairId: string; /** The content of the Cloudfront private key. */ privateKey: string | Buffer; /** The passphrase of RSA-SHA1 key*/ passphrase?: string; - /** The date string for when the signed URL or cookie can no longer be accessed. */ - dateLessThan?: string; - /** The IP address string to restrict signed URL access to. */ - ipAddress?: string; - /** The date string for when the signed URL or cookie can start to be accessed. */ - dateGreaterThan?: string; -} +}; -export type CloudfrontSignInputWithParameters = CloudfrontSignInputBase & { +/** + * @public + */ +export type CloudfrontSignInputWithParameters = CloudfrontSignerCredentials & { + /** The URL string to sign. */ + url: string; /** The date string for when the signed URL or cookie can no longer be accessed */ dateLessThan: string; - /** For this type policy should not be provided. */ + /** The date string for when the signed URL or cookie can start to be accessed. */ + dateGreaterThan?: string; + /** The IP address string to restrict signed URL access to. */ + ipAddress?: string; + /** + * [policy] should not be provided when using separate + * dateLessThan, dateGreaterThan, or ipAddress inputs. + */ policy?: never; }; -export type CloudfrontSignInputWithPolicy = CloudfrontSignInputBase & { - /** The JSON-encoded policy string */ - policy: string; +/** + * @public + */ +export type CloudfrontSignInputWithPolicy = CloudfrontSignerCredentials & { /** - * For this type dateLessThan should not be provided. + * The URL string to sign. Optional when policy is provided. + * + * This will be used as the initial url if calling getSignedUrl + * with a policy. + * + * This will be ignored if calling getSignedCookies with a policy. */ + url?: string; + /** The JSON-encoded policy string */ + policy: string; + /** When using a policy, a separate dateLessThan should not be provided. */ dateLessThan?: never; - /** - * For this type ipAddress should not be provided. - */ - ipAddress?: string; - /** - * For this type dateGreaterThan should not be provided. - */ + /** When using a policy, a separate dateGreaterThan should not be provided. */ dateGreaterThan?: never; + /** When using a policy, a separate ipAddress should not be provided. */ + ipAddress?: never; }; +/** + * @public + */ export interface CloudfrontSignedCookiesOutput { /** ID of the Cloudfront key pair. */ "CloudFront-Key-Pair-Id": string; @@ -57,6 +76,7 @@ export interface CloudfrontSignedCookiesOutput { /** * Creates a signed URL string using a canned or custom policy. + * @public * @returns the input URL with signature attached as query parameters. */ export function getSignedUrl({ @@ -74,6 +94,11 @@ export function getSignedUrl({ privateKey, passphrase, }); + + if (!url && !policy) { + throw new Error("@aws-sdk/cloudfront-signer: Please provide 'url' or 'policy'."); + } + if (policy) { cloudfrontSignBuilder.setCustomPolicy(policy); } else { @@ -85,10 +110,23 @@ export function getSignedUrl({ }); } - const newURL = new URL(url); + let baseUrl: string | undefined; + if (url) { + baseUrl = url; + } else if (policy) { + const resources = getPolicyResources(policy!); + if (!resources[0]) { + throw new Error( + "@aws-sdk/cloudfront-signer: No URL provided and unable to determine URL from first policy statement resource." + ); + } + baseUrl = resources[0].replace("*://", "https://"); + } + + const newURL = new URL(baseUrl!); newURL.search = Array.from(newURL.searchParams.entries()) .concat(Object.entries(cloudfrontSignBuilder.createCloudfrontAttribute())) - .filter(([key, value]) => value !== undefined) + .filter(([, value]) => value !== undefined) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join("&"); @@ -97,6 +135,7 @@ export function getSignedUrl({ /** * Creates signed cookies using a canned or custom policy. + * @public * @returns an object with keys/values that can be added to cookies. */ export function getSignedCookies({ @@ -138,6 +177,9 @@ export function getSignedCookies({ return cookies; } +/** + * @internal + */ interface Policy { Statement: Array<{ Resource: string; @@ -155,15 +197,24 @@ interface Policy { }>; } +/** + * @internal + */ interface PolicyDates { dateLessThan: number; dateGreaterThan?: number; } +/** + * @internal + */ interface BuildPolicyInput extends PolicyDates, Pick { resource: string; } +/** + * @internal + */ interface CloudfrontAttributes { Expires?: number; Policy?: string; @@ -171,6 +222,20 @@ interface CloudfrontAttributes { Signature: string; } +/** + * Utility to get the allowed resources of a policy. + * @internal + * + * @param policy - The JSON/JSON-encoded policy + */ +function getPolicyResources(policy: string | Policy) { + const parsedPolicy: Policy = typeof policy === "string" ? JSON.parse(policy) : policy; + return (parsedPolicy?.Statement ?? []).map((s) => s.Resource); +} + +/** + * @internal + */ function getResource(url: URL): string { switch (url.protocol) { case "http:": @@ -183,6 +248,9 @@ function getResource(url: URL): string { } } +/** + * @internal + */ class CloudfrontSignBuilder { private keyPairId: string; private privateKey: string | Buffer; @@ -190,15 +258,8 @@ class CloudfrontSignBuilder { private policy: string; private customPolicy = false; private dateLessThan?: number | undefined; - constructor({ - privateKey, - keyPairId, - passphrase, - }: { - keyPairId: string; - privateKey: string | Buffer; - passphrase?: string; - }) { + + constructor({ privateKey, keyPairId, passphrase }: CloudfrontSignerCredentials) { this.keyPairId = keyPairId; this.privateKey = privateKey; this.policy = ""; @@ -371,3 +432,16 @@ class CloudfrontSignBuilder { }; } } + +/** + * @deprecated use CloudfrontSignInput, CloudfrontSignInputWithParameters, or CloudfrontSignInputWithPolicy. + */ +export type CloudfrontSignInputBase = { + url: string; + keyPairId: string; + privateKey: string | Buffer; + passphrase?: string; + dateLessThan?: string; + ipAddress?: string; + dateGreaterThan?: string; +};