Skip to content

Commit 92aa194

Browse files
NamesMTnayabatir1kuhe
authored
feat(cloudfront-signer): allow url to be optional when using policy (#5926)
* feat(cloudfront-signer): `getSignedUrl()`: automatically get url from policy's `Resource` * docs(cloudfront-sign): align types with behavior `url` should be optional in InputWithPolicy * fix(cloudfront-signer): add error catch and solves tsc error * test(cloudfront-signer): update E2E tests Co-authored-by: Atir Nayab <[email protected]> * docs(cloudfront-signer): add examples for policy usage Co-authored-by: Atir Nayab <[email protected]> * docs(cloudfront-sign): better types<=>behavior aligning Co-authored-by: Atir Nayab <[email protected]> * chore(cloudfront-signer): clean up types, error handling --------- Co-authored-by: Atir Nayab <[email protected]> Co-authored-by: George Fu <[email protected]>
1 parent 37d500c commit 92aa194

File tree

3 files changed

+195
-41
lines changed

3 files changed

+195
-41
lines changed

packages/cloudfront-signer/README.md

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,43 @@ const signedUrl = getSignedUrl({
2727
});
2828
```
2929

30-
### Sign a URL for cookies
30+
### Sign a URL with a Policy
31+
32+
```javascript
33+
import { getSignedUrl } from "@aws-sdk/cloudfront-signer"; // ESM
34+
// const { getSignedUrl } = require("@aws-sdk/cloudfront-signer"); // CJS
35+
36+
const cloudfrontDistributionDomain = "https://d111111abcdef8.cloudfront.net";
37+
const s3ObjectKey = "private-content/private.jpeg";
38+
const url = `${cloudfrontDistributionDomain}/${s3ObjectKey}`;
39+
const privateKey = "CONTENTS-OF-PRIVATE-KEY";
40+
const keyPairId = "PUBLIC-KEY-ID-OF-CLOUDFRONT-KEY-PAIR";
41+
const dateLessThan = "2022-01-01";
42+
43+
const policy = {
44+
Statement: [
45+
{
46+
Resource: url,
47+
Condition: {
48+
DateLessThan: {
49+
"AWS:EpochTime": new Date(dateLessThan).getTime() / 1000, // time in seconds
50+
},
51+
},
52+
},
53+
],
54+
};
55+
56+
const policyString = JSON.stringify(policy);
57+
58+
const cookies = getSignedUrl({
59+
keyPairId,
60+
privateKey,
61+
policy: policyString,
62+
// url is automatically extracted from the policy, however you could still overwrite it if needed
63+
});
64+
```
65+
66+
### Get signed cookies for a resource
3167

3268
```javascript
3369
import { getSignedCookies } from "@aws-sdk/cloudfront-signer"; // ESM
@@ -47,3 +83,38 @@ const cookies = getSignedCookies({
4783
privateKey,
4884
});
4985
```
86+
87+
### Get signed cookies with a Policy
88+
89+
```javascript
90+
import { getSignedCookies } from "@aws-sdk/cloudfront-signer"; // ESM
91+
// const { getSignedCookies } = require("@aws-sdk/cloudfront-signer"); // CJS
92+
93+
const cloudfrontDistributionDomain = "https://d111111abcdef8.cloudfront.net";
94+
const s3ObjectKey = "private-content/private.jpeg";
95+
const url = `${cloudfrontDistributionDomain}/${s3ObjectKey}`;
96+
const privateKey = "CONTENTS-OF-PRIVATE-KEY";
97+
const keyPairId = "PUBLIC-KEY-ID-OF-CLOUDFRONT-KEY-PAIR";
98+
const dateLessThan = "2022-01-01";
99+
100+
const policy = {
101+
Statement: [
102+
{
103+
Resource: url,
104+
Condition: {
105+
DateLessThan: {
106+
"AWS:EpochTime": new Date(dateLessThan).getTime() / 1000, // time in seconds
107+
},
108+
},
109+
},
110+
],
111+
};
112+
113+
const policyString = JSON.stringify(policy);
114+
115+
const cookies = getSignedCookies({
116+
keyPairId,
117+
privateKey,
118+
policy: policyString,
119+
});
120+
```

packages/cloudfront-signer/src/sign.spec.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { parseUrl } from "@smithy/url-parser";
22
import { createSign, createVerify } from "crypto";
3-
import { mkdtempSync, rmdirSync, writeFileSync } from "fs";
4-
import { tmpdir } from "os";
5-
import { resolve } from "path";
63

74
import { getSignedCookies, getSignedUrl } from "./index";
85

@@ -341,6 +338,19 @@ describe("getSignedUrl", () => {
341338
const signatureQueryParam = denormalizeBase64(signature);
342339
expect(verifySignature(signatureQueryParam, policy)).toBeTruthy();
343340
});
341+
it("should sign a URL automatically extracted from a policy provided by the user", () => {
342+
const policy = JSON.stringify({ Statement: [{ Resource: url }] });
343+
const result = getSignedUrl({
344+
keyPairId,
345+
privateKey,
346+
policy,
347+
passphrase,
348+
});
349+
const signature = createSignature(policy);
350+
expect(result).toBe(`${url}?Policy=${encodeToBase64(policy)}&Key-Pair-Id=${keyPairId}&Signature=${signature}`);
351+
const signatureQueryParam = denormalizeBase64(signature);
352+
expect(verifySignature(signatureQueryParam, policy)).toBeTruthy();
353+
});
344354
});
345355

346356
describe("getSignedCookies", () => {
@@ -573,10 +583,9 @@ describe("getSignedCookies", () => {
573583
expect(result["CloudFront-Signature"]).toBe(expected["CloudFront-Signature"]);
574584
expect(verifySignature(denormalizeBase64(result["CloudFront-Signature"]), policyStr)).toBeTruthy();
575585
});
576-
it("should sign a URL with a policy provided by the user", () => {
586+
it("should sign cookies with a policy provided by the user without a url", () => {
577587
const policy = '{"foo":"bar"}';
578588
const result = getSignedCookies({
579-
url,
580589
keyPairId,
581590
privateKey,
582591
policy,

packages/cloudfront-signer/src/sign.ts

Lines changed: 109 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,68 @@
11
import { createSign } from "crypto";
22

3-
/** Input type to getSignedUrl and getSignedCookies. */
3+
/**
4+
* Input type to getSignedUrl and getSignedCookies.
5+
* @public
6+
*/
47
export type CloudfrontSignInput = CloudfrontSignInputWithParameters | CloudfrontSignInputWithPolicy;
58

6-
export interface CloudfrontSignInputBase {
7-
/** The URL string to sign. */
8-
url: string;
9+
/**
10+
* @public
11+
*/
12+
export type CloudfrontSignerCredentials = {
913
/** The ID of the Cloudfront key pair. */
1014
keyPairId: string;
1115
/** The content of the Cloudfront private key. */
1216
privateKey: string | Buffer;
1317
/** The passphrase of RSA-SHA1 key*/
1418
passphrase?: string;
15-
/** The date string for when the signed URL or cookie can no longer be accessed. */
16-
dateLessThan?: string;
17-
/** The IP address string to restrict signed URL access to. */
18-
ipAddress?: string;
19-
/** The date string for when the signed URL or cookie can start to be accessed. */
20-
dateGreaterThan?: string;
21-
}
19+
};
2220

23-
export type CloudfrontSignInputWithParameters = CloudfrontSignInputBase & {
21+
/**
22+
* @public
23+
*/
24+
export type CloudfrontSignInputWithParameters = CloudfrontSignerCredentials & {
25+
/** The URL string to sign. */
26+
url: string;
2427
/** The date string for when the signed URL or cookie can no longer be accessed */
2528
dateLessThan: string;
26-
/** For this type policy should not be provided. */
29+
/** The date string for when the signed URL or cookie can start to be accessed. */
30+
dateGreaterThan?: string;
31+
/** The IP address string to restrict signed URL access to. */
32+
ipAddress?: string;
33+
/**
34+
* [policy] should not be provided when using separate
35+
* dateLessThan, dateGreaterThan, or ipAddress inputs.
36+
*/
2737
policy?: never;
2838
};
2939

30-
export type CloudfrontSignInputWithPolicy = CloudfrontSignInputBase & {
31-
/** The JSON-encoded policy string */
32-
policy: string;
40+
/**
41+
* @public
42+
*/
43+
export type CloudfrontSignInputWithPolicy = CloudfrontSignerCredentials & {
3344
/**
34-
* For this type dateLessThan should not be provided.
45+
* The URL string to sign. Optional when policy is provided.
46+
*
47+
* This will be used as the initial url if calling getSignedUrl
48+
* with a policy.
49+
*
50+
* This will be ignored if calling getSignedCookies with a policy.
3551
*/
52+
url?: string;
53+
/** The JSON-encoded policy string */
54+
policy: string;
55+
/** When using a policy, a separate dateLessThan should not be provided. */
3656
dateLessThan?: never;
37-
/**
38-
* For this type ipAddress should not be provided.
39-
*/
40-
ipAddress?: string;
41-
/**
42-
* For this type dateGreaterThan should not be provided.
43-
*/
57+
/** When using a policy, a separate dateGreaterThan should not be provided. */
4458
dateGreaterThan?: never;
59+
/** When using a policy, a separate ipAddress should not be provided. */
60+
ipAddress?: never;
4561
};
4662

63+
/**
64+
* @public
65+
*/
4766
export interface CloudfrontSignedCookiesOutput {
4867
/** ID of the Cloudfront key pair. */
4968
"CloudFront-Key-Pair-Id": string;
@@ -57,6 +76,7 @@ export interface CloudfrontSignedCookiesOutput {
5776

5877
/**
5978
* Creates a signed URL string using a canned or custom policy.
79+
* @public
6080
* @returns the input URL with signature attached as query parameters.
6181
*/
6282
export function getSignedUrl({
@@ -74,6 +94,11 @@ export function getSignedUrl({
7494
privateKey,
7595
passphrase,
7696
});
97+
98+
if (!url && !policy) {
99+
throw new Error("@aws-sdk/cloudfront-signer: Please provide 'url' or 'policy'.");
100+
}
101+
77102
if (policy) {
78103
cloudfrontSignBuilder.setCustomPolicy(policy);
79104
} else {
@@ -85,10 +110,23 @@ export function getSignedUrl({
85110
});
86111
}
87112

88-
const newURL = new URL(url);
113+
let baseUrl: string | undefined;
114+
if (url) {
115+
baseUrl = url;
116+
} else if (policy) {
117+
const resources = getPolicyResources(policy!);
118+
if (!resources[0]) {
119+
throw new Error(
120+
"@aws-sdk/cloudfront-signer: No URL provided and unable to determine URL from first policy statement resource."
121+
);
122+
}
123+
baseUrl = resources[0].replace("*://", "https://");
124+
}
125+
126+
const newURL = new URL(baseUrl!);
89127
newURL.search = Array.from(newURL.searchParams.entries())
90128
.concat(Object.entries(cloudfrontSignBuilder.createCloudfrontAttribute()))
91-
.filter(([key, value]) => value !== undefined)
129+
.filter(([, value]) => value !== undefined)
92130
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
93131
.join("&");
94132

@@ -97,6 +135,7 @@ export function getSignedUrl({
97135

98136
/**
99137
* Creates signed cookies using a canned or custom policy.
138+
* @public
100139
* @returns an object with keys/values that can be added to cookies.
101140
*/
102141
export function getSignedCookies({
@@ -138,6 +177,9 @@ export function getSignedCookies({
138177
return cookies;
139178
}
140179

180+
/**
181+
* @internal
182+
*/
141183
interface Policy {
142184
Statement: Array<{
143185
Resource: string;
@@ -155,22 +197,45 @@ interface Policy {
155197
}>;
156198
}
157199

200+
/**
201+
* @internal
202+
*/
158203
interface PolicyDates {
159204
dateLessThan: number;
160205
dateGreaterThan?: number;
161206
}
162207

208+
/**
209+
* @internal
210+
*/
163211
interface BuildPolicyInput extends PolicyDates, Pick<CloudfrontSignInput, "ipAddress"> {
164212
resource: string;
165213
}
166214

215+
/**
216+
* @internal
217+
*/
167218
interface CloudfrontAttributes {
168219
Expires?: number;
169220
Policy?: string;
170221
"Key-Pair-Id": string;
171222
Signature: string;
172223
}
173224

225+
/**
226+
* Utility to get the allowed resources of a policy.
227+
* @internal
228+
*
229+
* @param policy - The JSON/JSON-encoded policy
230+
*/
231+
function getPolicyResources(policy: string | Policy) {
232+
const parsedPolicy: Policy = typeof policy === "string" ? JSON.parse(policy) : policy;
233+
return (parsedPolicy?.Statement ?? []).map((s) => s.Resource);
234+
}
235+
236+
/**
237+
* @internal
238+
*/
174239
function getResource(url: URL): string {
175240
switch (url.protocol) {
176241
case "http:":
@@ -183,22 +248,18 @@ function getResource(url: URL): string {
183248
}
184249
}
185250

251+
/**
252+
* @internal
253+
*/
186254
class CloudfrontSignBuilder {
187255
private keyPairId: string;
188256
private privateKey: string | Buffer;
189257
private passphrase?: string;
190258
private policy: string;
191259
private customPolicy = false;
192260
private dateLessThan?: number | undefined;
193-
constructor({
194-
privateKey,
195-
keyPairId,
196-
passphrase,
197-
}: {
198-
keyPairId: string;
199-
privateKey: string | Buffer;
200-
passphrase?: string;
201-
}) {
261+
262+
constructor({ privateKey, keyPairId, passphrase }: CloudfrontSignerCredentials) {
202263
this.keyPairId = keyPairId;
203264
this.privateKey = privateKey;
204265
this.policy = "";
@@ -371,3 +432,16 @@ class CloudfrontSignBuilder {
371432
};
372433
}
373434
}
435+
436+
/**
437+
* @deprecated use CloudfrontSignInput, CloudfrontSignInputWithParameters, or CloudfrontSignInputWithPolicy.
438+
*/
439+
export type CloudfrontSignInputBase = {
440+
url: string;
441+
keyPairId: string;
442+
privateKey: string | Buffer;
443+
passphrase?: string;
444+
dateLessThan?: string;
445+
ipAddress?: string;
446+
dateGreaterThan?: string;
447+
};

0 commit comments

Comments
 (0)