Skip to content

Commit 64181df

Browse files
authored
Add resourceUrlPattern support to CloudFrontUtilities.getCookiesForCustomPolicy (#6779)
1 parent 07c5bbf commit 64181df

4 files changed

Lines changed: 194 additions & 6 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Amazon Cloudfront",
4+
"contributor": "",
5+
"description": "Add support for resourceUrlPattern to `CloudFrontUtilities.getCookiesForCustomPolicy`."
6+
}

services/cloudfront/src/main/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilities.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -409,13 +409,15 @@ public CookiesForCannedPolicy getCookiesForCannedPolicy(CannedSignerRequest requ
409409
* PrivateKey privateKey = myPrivateKey;
410410
* Instant activeDate = Instant.now().plus(Duration.ofDays(2));
411411
* String ipRange = "192.168.0.1/24";
412+
* String resourceUrlPattern = "https://d111111abcdef8.cloudfront.net/*"; // If not supplied, defaults to the value of resourceUrl.
412413
*
413414
* CookiesForCustomPolicy cookies = utilities.getCookiesForCustomPolicy(r -> r.resourceUrl(resourceUrl)
414415
* .privateKey(privateKey)
415416
* .keyPairId(keyPairId)
416417
* .expirationDate(expirationDate)
417418
* .activeDate(activeDate)
418-
* .ipRange(ipRange));
419+
* .ipRange(ipRange)
420+
* .resourceUrlPattern(resourceUrlPattern));
419421
* // Generates Set-Cookie header values to send to the viewer to allow access
420422
* String signatureHeaderValue = cookies.signatureHeaderValue();
421423
* String keyPairIdHeaderValue = cookies.keyPairIdHeaderValue();
@@ -434,7 +436,13 @@ public CookiesForCustomPolicy getCookiesForCustomPolicy(Consumer<CustomSignerReq
434436
*
435437
* @param request
436438
* A {@link CustomSignerRequest} configured with the following values:
437-
* resourceUrl, privateKey, keyPairId, expirationDate, activeDate (optional), ipRange (optional)
439+
* resourceUrl,
440+
* privateKey,
441+
* keyPairId,
442+
* expirationDate,
443+
* activeDate (optional),
444+
* ipRange (optional),
445+
* resourceUrlPattern (optional)
438446
* @return The signed cookies with custom policy.
439447
*
440448
* <p><b>Example Usage</b>
@@ -450,14 +458,16 @@ public CookiesForCustomPolicy getCookiesForCustomPolicy(Consumer<CustomSignerReq
450458
* Path keyFile = myKeyFile;
451459
* Instant activeDate = Instant.now().plus(Duration.ofDays(2));
452460
* String ipRange = "192.168.0.1/24";
461+
* String resourceUrlPattern = "https://d111111abcdef8.cloudfront.net/*"; // If not supplied, defaults to the value of resourceUrl.
453462
*
454463
* CustomSignerRequest customRequest = CustomSignerRequest.builder()
455464
* .resourceUrl(resourceUrl)
456465
* .privateKey(keyFile)
457-
* .keyPairId(keyFile)
466+
* .keyPairId(keyPairId)
458467
* .expirationDate(expirationDate)
459468
* .activeDate(activeDate)
460469
* .ipRange(ipRange)
470+
* .resourceUrlPattern(resourceUrlPattern)
461471
* .build();
462472
* CookiesForCustomPolicy cookies = utilities.getCookiesForCustomPolicy(customRequest);
463473
* // Generates Set-Cookie header values to send to the viewer to allow access
@@ -468,7 +478,11 @@ public CookiesForCustomPolicy getCookiesForCustomPolicy(Consumer<CustomSignerReq
468478
*/
469479
public CookiesForCustomPolicy getCookiesForCustomPolicy(CustomSignerRequest request) {
470480
try {
471-
String policy = SigningUtils.buildCustomPolicy(request.resourceUrl(), request.activeDate(), request.expirationDate(),
481+
String resourceUrlPattern = request.resourceUrlPattern() == null
482+
? request.resourceUrl()
483+
: request.resourceUrlPattern();
484+
485+
String policy = SigningUtils.buildCustomPolicy(resourceUrlPattern, request.activeDate(), request.expirationDate(),
472486
request.ipRange());
473487
byte[] signatureBytes = signPolicy(policy.getBytes(UTF_8), request.privateKey());
474488
String urlSafePolicy = SigningUtils.makeStringUrlSafe(policy);

services/cloudfront/src/test/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilitiesIntegrationTest.java

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import java.nio.file.Files;
2626
import java.nio.file.Path;
2727
import java.nio.file.StandardOpenOption;
28-
import java.security.Key;
2928
import java.security.KeyPair;
3029
import java.security.KeyPairGenerator;
3130
import java.security.PrivateKey;
@@ -66,7 +65,6 @@
6665
import software.amazon.awssdk.services.cloudfront.model.DistributionConfig;
6766
import software.amazon.awssdk.services.cloudfront.model.DistributionSummary;
6867
import software.amazon.awssdk.services.cloudfront.model.GetKeyGroupResponse;
69-
import software.amazon.awssdk.services.cloudfront.model.KeyGroup;
7068
import software.amazon.awssdk.services.cloudfront.model.KeyGroupConfig;
7169
import software.amazon.awssdk.services.cloudfront.model.KeyGroupSummary;
7270
import software.amazon.awssdk.services.cloudfront.model.Origin;
@@ -319,6 +317,108 @@ void getCookiesForCustomPolicy_withFutureActiveDate_shouldReturn403Response() th
319317
assertThat(response.httpResponse().statusCode()).isEqualTo(expectedStatus);
320318
}
321319

320+
@ParameterizedTest(name = "{0}")
321+
@MethodSource("keyCases")
322+
void getCookiesForCustomPolicy_shouldAllowQueryParametersWhenUsingWildcard(KeyTestCase testCase) throws Exception {
323+
Instant expirationDate = LocalDate.of(2050, 1, 1)
324+
.atStartOfDay()
325+
.toInstant(ZoneOffset.of("Z"));
326+
327+
Instant activeDate = LocalDate.of(2022, 1, 1)
328+
.atStartOfDay()
329+
.toInstant(ZoneOffset.of("Z"));
330+
331+
CookiesForCustomPolicy cookies = cloudFrontUtilities.getCookiesForCustomPolicy(r -> r.resourceUrl(resourceUrl)
332+
.privateKey(testCase.privateKey)
333+
.keyPairId(testCase.keyPairId)
334+
.resourceUrlPattern(resourceUrl + "*")
335+
.activeDate(activeDate)
336+
.expirationDate(expirationDate));
337+
338+
// Request the same resource with an additional query parameter - should still be allowed by the wildcard policy
339+
URI uri = URI.create(resourceUrl + "?foo=bar");
340+
SdkHttpClient client = ApacheHttpClient.create();
341+
HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder()
342+
.request(SdkHttpRequest.builder()
343+
.uri(uri)
344+
.appendHeader("Cookie", cookies.policyHeaderValue())
345+
.appendHeader("Cookie", cookies.signatureHeaderValue())
346+
.appendHeader("Cookie", cookies.keyPairIdHeaderValue())
347+
.method(SdkHttpMethod.GET)
348+
.build())
349+
.build()).call();
350+
assertThat(response.httpResponse().statusCode()).isEqualTo(200);
351+
}
352+
353+
@ParameterizedTest(name = "{0}")
354+
@MethodSource("keyCases")
355+
void getCookiesForCustomPolicy_wildCardPath(KeyTestCase testCase) throws Exception {
356+
String resourceUri = "https://" + domainName;
357+
Instant expirationDate = LocalDate.of(2050, 1, 1)
358+
.atStartOfDay()
359+
.toInstant(ZoneOffset.of("Z"));
360+
361+
Instant activeDate = LocalDate.of(2022, 1, 1)
362+
.atStartOfDay()
363+
.toInstant(ZoneOffset.of("Z"));
364+
365+
CookiesForCustomPolicy cookies = cloudFrontUtilities.getCookiesForCustomPolicy(
366+
r -> r.resourceUrl(resourceUri + "/foo/specific-file")
367+
.privateKey(testCase.privateKey)
368+
.keyPairId(testCase.keyPairId)
369+
.resourceUrlPattern(resourceUri + "/foo/*")
370+
.activeDate(activeDate)
371+
.expirationDate(expirationDate));
372+
373+
// Use the cookies to access a different file under the same wildcard path
374+
URI otherFileUri = URI.create(resourceUri + "/foo/other-file");
375+
SdkHttpClient client = ApacheHttpClient.create();
376+
HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder()
377+
.request(SdkHttpRequest.builder()
378+
.uri(otherFileUri)
379+
.appendHeader("Cookie", cookies.policyHeaderValue())
380+
.appendHeader("Cookie", cookies.signatureHeaderValue())
381+
.appendHeader("Cookie", cookies.keyPairIdHeaderValue())
382+
.method(SdkHttpMethod.GET)
383+
.build())
384+
.build()).call();
385+
assertThat(response.httpResponse().statusCode()).isEqualTo(200);
386+
}
387+
388+
@ParameterizedTest(name = "{0}")
389+
@MethodSource("keyCases")
390+
void getCookiesForCustomPolicy_wildCardPolicyResource_allowsAnyPath(KeyTestCase testCase) throws Exception {
391+
Instant expirationDate = LocalDate.of(2050, 1, 1)
392+
.atStartOfDay()
393+
.toInstant(ZoneOffset.of("Z"));
394+
395+
Instant activeDate = LocalDate.of(2022, 1, 1)
396+
.atStartOfDay()
397+
.toInstant(ZoneOffset.of("Z"));
398+
399+
CookiesForCustomPolicy cookies = cloudFrontUtilities.getCookiesForCustomPolicy(
400+
r -> r.resourceUrl(resourceUrl)
401+
.privateKey(testCase.privateKey)
402+
.keyPairId(testCase.keyPairId)
403+
.resourceUrlPattern("*")
404+
.activeDate(activeDate)
405+
.expirationDate(expirationDate));
406+
407+
// Use the cookies to access a completely different path - the "*" pattern should allow any path
408+
URI differentPathUri = URI.create(resourceUrl.replace("/s3ObjectKey", "/foo/other-file"));
409+
SdkHttpClient client = ApacheHttpClient.create();
410+
HttpExecuteResponse response = client.prepareRequest(HttpExecuteRequest.builder()
411+
.request(SdkHttpRequest.builder()
412+
.uri(differentPathUri)
413+
.appendHeader("Cookie", cookies.policyHeaderValue())
414+
.appendHeader("Cookie", cookies.signatureHeaderValue())
415+
.appendHeader("Cookie", cookies.keyPairIdHeaderValue())
416+
.method(SdkHttpMethod.GET)
417+
.build())
418+
.build()).call();
419+
assertThat(response.httpResponse().statusCode()).isEqualTo(200);
420+
}
421+
322422
@ParameterizedTest(name = "{0}")
323423
@MethodSource("keyCases")
324424
void getSignedUrlWithCustomPolicy_shouldAllowQueryParametersWhenUsingWildcard(KeyTestCase testCase) throws Exception {

services/cloudfront/src/test/java/software/amazon/awssdk/services/cloudfront/CloudFrontUtilitiesTest.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,74 @@ void getSignedURLWithCustomPolicy_policyResourceUrlShouldHandleVariousPatterns(
438438
}
439439

440440

441+
@ParameterizedTest
442+
@MethodSource("provideUrlPatternsAndExpectedResources")
443+
void getCookiesForCustomPolicy_policyResourceUrlShouldHandleVariousPatterns(
444+
String resourceUrlPattern, String expectedResource) {
445+
KeyTestCase testCase = KeyTestCase.createRsaTestCase();
446+
String baseUrl = "https://d1234.cloudfront.net/images/photo.jpg";
447+
Instant expiration = Instant.now().plusSeconds(3600);
448+
CustomSignerRequest request = CustomSignerRequest.builder()
449+
.resourceUrl(baseUrl)
450+
.privateKey(testCase.keyPair.getPrivate())
451+
.keyPairId("keyPairId")
452+
.resourceUrlPattern(resourceUrlPattern)
453+
.expirationDate(expiration)
454+
.build();
455+
456+
CookiesForCustomPolicy cookies = cloudFrontUtilities.getCookiesForCustomPolicy(request);
457+
458+
// Extract and decode the policy from the cookie header value
459+
String policyHeaderValue = cookies.policyHeaderValue();
460+
String encodedPolicy = policyHeaderValue.split("CloudFront-Policy=")[1];
461+
String standardBase64 = encodedPolicy
462+
.replace('-', '+')
463+
.replace('_', '=')
464+
.replace('~', '/');
465+
String decodedPolicy = new String(Base64.getDecoder().decode(standardBase64), UTF_8);
466+
467+
// Build expected policy
468+
StringBuilder expectedPolicy = new StringBuilder();
469+
StringJoiner conditions = new StringJoiner(",", "{", "}");
470+
conditions.add("\"DateLessThan\":{\"AWS:EpochTime\":" + expiration.getEpochSecond() + "}");
471+
472+
expectedPolicy.append("{\"Statement\":[{")
473+
.append("\"Resource\":\"").append(expectedResource).append("\",")
474+
.append("\"Condition\":").append(conditions)
475+
.append("}]}");
476+
477+
assertThat(decodedPolicy.trim()).isEqualTo(expectedPolicy.toString().trim());
478+
// resourceUrl on the cookie object should still be the concrete URL, not the pattern
479+
assertThat(cookies.resourceUrl()).isEqualTo(baseUrl);
480+
}
481+
482+
@Test
483+
void getCookiesForCustomPolicy_withoutResourceUrlPattern_usesResourceUrlInPolicy() {
484+
KeyTestCase testCase = KeyTestCase.createRsaTestCase();
485+
String baseUrl = "https://d1234.cloudfront.net/images/photo.jpg";
486+
Instant expiration = Instant.now().plusSeconds(3600);
487+
CustomSignerRequest request = CustomSignerRequest.builder()
488+
.resourceUrl(baseUrl)
489+
.privateKey(testCase.keyPair.getPrivate())
490+
.keyPairId("keyPairId")
491+
.expirationDate(expiration)
492+
.build();
493+
494+
CookiesForCustomPolicy cookies = cloudFrontUtilities.getCookiesForCustomPolicy(request);
495+
496+
// Extract and decode the policy
497+
String policyHeaderValue = cookies.policyHeaderValue();
498+
String encodedPolicy = policyHeaderValue.split("CloudFront-Policy=")[1];
499+
String standardBase64 = encodedPolicy
500+
.replace('-', '+')
501+
.replace('_', '=')
502+
.replace('~', '/');
503+
String decodedPolicy = new String(Base64.getDecoder().decode(standardBase64), UTF_8);
504+
505+
// When resourceUrlPattern is not set, the policy should use resourceUrl
506+
assertThat(decodedPolicy).contains("\"Resource\":\"" + baseUrl + "\"");
507+
}
508+
441509
private static Stream<Arguments> provideUrlPatternsAndExpectedResources() {
442510
return Stream.of(
443511
Arguments.of("*", "*"),

0 commit comments

Comments
 (0)