Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for cognito standalone maps URLs #48

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 67 additions & 5 deletions src/cognito/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/";
Expand Down Expand Up @@ -135,11 +136,15 @@ 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);
const signedUrl = new URL(transformRequest(url, "Tile").url);

// Host and pathname should still be the same
expect(signedUrl.hostname).toStrictEqual(originalUrl.hostname);
Expand All @@ -159,11 +164,68 @@ 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:
// <Access Key ID>/<CURRENT DATE>/<SIGNING REGION>/<SIGNING SERVICE NAME>/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([["Style"], ["SpriteJSON"], ["Glyphs"]])(
"getMapAuthenticationOptions should contain transformRequest function to sign the AWS Location URLs for %i using our custom signer",
async (resourceType) => {
const authHelper = await withIdentityPoolId(cognitoIdentityPoolId);
const transformRequest = authHelper.getMapAuthenticationOptions().transformRequest;

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"], ["SpriteJSON"], ["Glyphs"], ["Tile"]])(
"getMapAuthenticationOptions should contain transformRequest function to sign the AWS Location URLs for %i using our custom signer",
async (resourceType) => {
const authHelper = await withIdentityPoolId(cognitoIdentityPoolId);
const transformRequest = authHelper.getMapAuthenticationOptions().transformRequest;

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);
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:
// <Access Key ID>/<CURRENT DATE>/<SIGNING REGION>/<SIGNING SERVICE NAME>/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;
Expand Down
25 changes: 23 additions & 2 deletions src/cognito/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,32 @@ 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);

// 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 (!resourceType || resourceType !== "Tile") {
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,
Expand Down
10 changes: 4 additions & 6 deletions src/utils/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
}

export class Signer {
static signUrl(urlToSign: string, region: string, accessInfo: any): string {
static signUrl(urlToSign: string, region: string, serviceName: string, accessInfo: any): string {

Check warning on line 86 in src/utils/signer.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected any. Specify a different type
const method = "GET";
let body: undefined;

Expand All @@ -93,7 +93,7 @@
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();
Expand All @@ -103,6 +103,7 @@
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 ?? {};
Expand All @@ -112,14 +113,11 @@
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,
};
};

Expand Down Expand Up @@ -365,7 +363,7 @@
.sort()
.join(";");

const getHashedPayload = (body: HttpRequest["body"]): string => {

Check warning on line 366 in src/utils/signer.ts

View workflow job for this annotation

GitHub Actions / build

'body' is defined but never used
// Modification - For our use-case, the body is always null,
// so we just return the EMPTY_HASH
// return precalculated empty hash if body is undefined or null
Expand Down
Loading