From 7591f3b553ab995da9d7f581f7dd224198e6d19f Mon Sep 17 00:00:00 2001 From: Chris Galvan Date: Tue, 5 Nov 2024 09:58:46 -0600 Subject: [PATCH 1/2] Added support for cognito standalone maps URLs Signed-off-by: Chris Galvan --- src/cognito/index.test.ts | 74 ++++++++++++++++++++++++++++++++++++--- src/cognito/index.ts | 23 +++++++++++- src/utils/signer.ts | 10 +++--- 3 files changed, 96 insertions(+), 11 deletions(-) diff --git a/src/cognito/index.test.ts b/src/cognito/index.test.ts index cf8ab09..21c3840 100644 --- a/src/cognito/index.test.ts +++ b/src/cognito/index.test.ts @@ -12,7 +12,8 @@ describe("AuthHelper for Cognito", () => { jest.useFakeTimers(); const region = "us-west-2"; const cognitoIdentityPoolId = `${region}:TEST-IDENTITY-POOL-ID`; - const url = "https://maps.geo.us-west-2.amazonaws.com/"; + const standaloneMapsUrl = "https://maps.geo.us-west-2.amazonaws.com/v2"; + const locationUrl = "https://maps.geo.us-west-2.amazonaws.com/maps/v0/maps/TestMapName"; const govCloudUrl = "https://maps.geo-fips.us-gov-west-1.amazonaws.com/"; const nonAWSUrl = "https://example.com/"; const nonLocationAWSUrl = "https://my.cool.service.us-west-2.amazonaws.com/"; @@ -135,9 +136,13 @@ describe("AuthHelper for Cognito", () => { expect(authHelper.getCredentials()).toStrictEqual(mockedUpdatedCredentials); }); - it("getMapAuthenticationOptions should contain transformRequest function to sign the AWS Urls using our custom signer", async () => { + // For the standalone Places SDK, the url should only be signed when accessing the map tiles + it("getMapAuthenticationOptions should contain transformRequest function to sign the AWS standalone Maps URLs for map tiles using our custom signer", async () => { const authHelper = await withIdentityPoolId(cognitoIdentityPoolId); const transformRequest = authHelper.getMapAuthenticationOptions().transformRequest; + + const url = standaloneMapsUrl + "/tiles"; + const originalUrl = new URL(url); const signedUrl = new URL(transformRequest(url).url); @@ -159,11 +164,72 @@ describe("AuthHelper for Cognito", () => { const securityToken = searchParams.get("X-Amz-Security-Token"); expect(securityToken).toStrictEqual(mockedCredentials.sessionToken); - // The credential starts with our access key, the rest is generated + // The credential is formatted as such: + // ////aws4_request + // We need to validate that the access key matches our mocked credentials, + // and that the signing service name is "geo-maps" for all standalone Maps SDK tile requests const credential = searchParams.get("X-Amz-Credential"); - expect(credential).toContain(mockedCredentials.accessKeyId); + const credentialParts = credential?.split("/"); + expect(credentialParts?.[0]).toStrictEqual(mockedCredentials.accessKeyId); + expect(credentialParts?.[3]).toStrictEqual("geo-maps"); }); + // For the standalone Places SDK, the url should not be signed when accessing all style descriptor, sprites, and glyphs + it.each([["/styles/Standard/descriptor"], ["/styles/Standard/Light/Default/sprites"], ["/glyphs"]])( + "getMapAuthenticationOptions should contain transformRequest function to sign the AWS Location URLs for %i using our custom signer", + async (resourceName) => { + const authHelper = await withIdentityPoolId(cognitoIdentityPoolId); + const transformRequest = authHelper.getMapAuthenticationOptions().transformRequest; + + const url = standaloneMapsUrl + resourceName; + + expect(transformRequest(url)).toStrictEqual({ + url: url, + }); + }, + ); + + // For the consolidated Location SDK, the url should be signed when accessing all resources (style descriptor, sprites, glyphs, and map tiles) + it.each([["/style-descriptor"], ["/sprites"], ["/glyphs"], ["/tiles"]])( + "getMapAuthenticationOptions should contain transformRequest function to sign the AWS Location URLs for %i using our custom signer", + async (resourceName) => { + const authHelper = await withIdentityPoolId(cognitoIdentityPoolId); + const transformRequest = authHelper.getMapAuthenticationOptions().transformRequest; + + const url = locationUrl + resourceName; + + const originalUrl = new URL(url); + const signedUrl = new URL(transformRequest(url).url); + + // Host and pathname should still be the same + expect(signedUrl.hostname).toStrictEqual(originalUrl.hostname); + expect(signedUrl.pathname).toStrictEqual(originalUrl.pathname); + + const searchParams = signedUrl.searchParams; + expect(searchParams.size).toStrictEqual(6); + + // Verify these search params exist on the signed url + // We don't need to test the actual values since they are non-deterministic or constants + const expectedSearchParams = ["X-Amz-Algorithm", "X-Amz-Date", "X-Amz-SignedHeaders", "X-Amz-Signature"]; + expectedSearchParams.forEach((value) => { + expect(searchParams.has(value)).toStrictEqual(true); + }); + + // We can expect the session token to match exactly as passed in + const securityToken = searchParams.get("X-Amz-Security-Token"); + expect(securityToken).toStrictEqual(mockedCredentials.sessionToken); + + // The credential is formatted as such: + // ////aws4_request + // We need to validate that the access key matches our mocked credentials, + // and that the signing service name is "geo" for all consolidated Location SDK requests + const credential = searchParams.get("X-Amz-Credential"); + const credentialParts = credential?.split("/"); + expect(credentialParts?.[0]).toStrictEqual(mockedCredentials.accessKeyId); + expect(credentialParts?.[3]).toStrictEqual("geo"); + }, + ); + it("getMapAuthenticationOptions should contain transformRequest function to sign the AWS GovCloud Urls using our custom signer", async () => { const authHelper = await withIdentityPoolId(cognitoIdentityPoolId); const transformRequest = authHelper.getMapAuthenticationOptions().transformRequest; diff --git a/src/cognito/index.ts b/src/cognito/index.ts index ceaeb56..0ba2001 100644 --- a/src/cognito/index.ts +++ b/src/cognito/index.ts @@ -49,8 +49,29 @@ export async function withIdentityPoolId( transformRequest: (url: string) => { // Only sign Amazon Location Service URLs if (url.match(/^https:\/\/maps\.(geo|geo-fips)\.[a-z0-9-]+\.(amazonaws\.com)/)) { + const urlObj = new URL(url); + + // Split the pathname into parts, using the filter(Boolean) to ignore any empty parts, + // since the first item will be empty because the pathname looks like: + // /v2/styles/Standard/descriptor + const pathParts = urlObj.pathname.split("/").filter(Boolean); + + // The signing service name for the standalone Maps SDK is "geo-maps" + let serviceName = "geo-maps"; + if (pathParts?.[0] == "v2") { + // For this case, we only need to sign the map tiles, so we + // can return the original url if it is for descriptor, sprites, or glyphs + if (pathParts?.[1] != "tiles") { + return { url }; + } + } else { + // The signing service name for the consolidated Location Client is "geo" + // In this case, we need to sign all URLs (sprites, glyphs, map tiles) + serviceName = "geo"; + } + return { - url: Signer.signUrl(url, region, { + url: Signer.signUrl(url, region, serviceName, { access_key: credentials.accessKeyId, secret_key: credentials.secretAccessKey, session_token: credentials.sessionToken, diff --git a/src/utils/signer.ts b/src/utils/signer.ts index 6c22718..f6db6a4 100644 --- a/src/utils/signer.ts +++ b/src/utils/signer.ts @@ -83,7 +83,7 @@ interface Presignable extends Pick { } export class Signer { - static signUrl(urlToSign: string, region: string, accessInfo: any): string { + static signUrl(urlToSign: string, region: string, serviceName: string, accessInfo: any): string { const method = "GET"; let body: undefined; @@ -93,7 +93,7 @@ export class Signer { url: new URL(urlToSign), }; - const options = getOptions(urlToSign, region, accessInfo); + const options = getOptions(urlToSign, region, serviceName, accessInfo); const signedUrl = presignUrl(presignable, options); return signedUrl.toString(); @@ -103,6 +103,7 @@ export class Signer { const getOptions = ( url: string, region: string, + serviceName: string, accessInfo: { access_key: string; secret_key: string; session_token: string }, ) => { const { access_key, secret_key, session_token } = accessInfo ?? {}; @@ -112,14 +113,11 @@ const getOptions = ( sessionToken: session_token, }; - // Service hard-coded to "geo" for our purposes - const service = "geo"; - return { credentials, signingDate: new Date(), signingRegion: region, - signingService: service, + signingService: serviceName, }; }; From c348cfde317d29c951d1c43dc7c6addbabe43fd4 Mon Sep 17 00:00:00 2001 From: Chris Galvan Date: Tue, 5 Nov 2024 10:53:42 -0600 Subject: [PATCH 2/2] Updated logic to use resourceType Signed-off-by: Chris Galvan --- src/cognito/index.test.ts | 22 +++++++++------------- src/cognito/index.ts | 4 ++-- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/cognito/index.test.ts b/src/cognito/index.test.ts index 21c3840..7bfcc02 100644 --- a/src/cognito/index.test.ts +++ b/src/cognito/index.test.ts @@ -144,7 +144,7 @@ describe("AuthHelper for Cognito", () => { const url = standaloneMapsUrl + "/tiles"; const originalUrl = new URL(url); - const signedUrl = new URL(transformRequest(url).url); + const signedUrl = new URL(transformRequest(url, "Tile").url); // Host and pathname should still be the same expect(signedUrl.hostname).toStrictEqual(originalUrl.hostname); @@ -175,31 +175,27 @@ describe("AuthHelper for Cognito", () => { }); // For the standalone Places SDK, the url should not be signed when accessing all style descriptor, sprites, and glyphs - it.each([["/styles/Standard/descriptor"], ["/styles/Standard/Light/Default/sprites"], ["/glyphs"]])( + it.each([["Style"], ["SpriteJSON"], ["Glyphs"]])( "getMapAuthenticationOptions should contain transformRequest function to sign the AWS Location URLs for %i using our custom signer", - async (resourceName) => { + async (resourceType) => { const authHelper = await withIdentityPoolId(cognitoIdentityPoolId); const transformRequest = authHelper.getMapAuthenticationOptions().transformRequest; - const url = standaloneMapsUrl + resourceName; - - expect(transformRequest(url)).toStrictEqual({ - url: url, + expect(transformRequest(standaloneMapsUrl, resourceType)).toStrictEqual({ + url: standaloneMapsUrl, }); }, ); // For the consolidated Location SDK, the url should be signed when accessing all resources (style descriptor, sprites, glyphs, and map tiles) - it.each([["/style-descriptor"], ["/sprites"], ["/glyphs"], ["/tiles"]])( + it.each([["Style"], ["SpriteJSON"], ["Glyphs"], ["Tile"]])( "getMapAuthenticationOptions should contain transformRequest function to sign the AWS Location URLs for %i using our custom signer", - async (resourceName) => { + async (resourceType) => { const authHelper = await withIdentityPoolId(cognitoIdentityPoolId); const transformRequest = authHelper.getMapAuthenticationOptions().transformRequest; - const url = locationUrl + resourceName; - - const originalUrl = new URL(url); - const signedUrl = new URL(transformRequest(url).url); + const originalUrl = new URL(locationUrl); + const signedUrl = new URL(transformRequest(locationUrl, resourceType).url); // Host and pathname should still be the same expect(signedUrl.hostname).toStrictEqual(originalUrl.hostname); diff --git a/src/cognito/index.ts b/src/cognito/index.ts index 0ba2001..dcf66e0 100644 --- a/src/cognito/index.ts +++ b/src/cognito/index.ts @@ -46,7 +46,7 @@ export async function withIdentityPoolId( return { getMapAuthenticationOptions: () => ({ - transformRequest: (url: string) => { + transformRequest: (url: string, resourceType?: string) => { // Only sign Amazon Location Service URLs if (url.match(/^https:\/\/maps\.(geo|geo-fips)\.[a-z0-9-]+\.(amazonaws\.com)/)) { const urlObj = new URL(url); @@ -61,7 +61,7 @@ export async function withIdentityPoolId( if (pathParts?.[0] == "v2") { // For this case, we only need to sign the map tiles, so we // can return the original url if it is for descriptor, sprites, or glyphs - if (pathParts?.[1] != "tiles") { + if (!resourceType || resourceType !== "Tile") { return { url }; } } else {