diff --git a/packages/hoppscotch-cli/package.json b/packages/hoppscotch-cli/package.json index 7c2daab1c..a4b6ad3ed 100644 --- a/packages/hoppscotch-cli/package.json +++ b/packages/hoppscotch-cli/package.json @@ -42,6 +42,7 @@ "private": false, "dependencies": { "axios": "1.7.5", + "aws4fetch": "1.0.19", "chalk": "5.3.0", "commander": "11.1.0", "isolated-vm": "4.7.2", diff --git a/packages/hoppscotch-cli/src/__tests__/e2e/commands/test.spec.ts b/packages/hoppscotch-cli/src/__tests__/e2e/commands/test.spec.ts index 91b6659af..72ec41b8e 100644 --- a/packages/hoppscotch-cli/src/__tests__/e2e/commands/test.spec.ts +++ b/packages/hoppscotch-cli/src/__tests__/e2e/commands/test.spec.ts @@ -385,7 +385,6 @@ describe("hopp test [options] ", () => { test("Supports the usage of request variables along with environment variables", async () => { const env = { ...process.env, - secretBasicAuthUsernameEnvVar: "username", secretBasicAuthPasswordEnvVar: "password", }; @@ -407,6 +406,30 @@ describe("hopp test [options] ", () => { expect(error).toBeNull(); }); }); + + describe("AWS Signature Authorization type", () => { + test("Successfully translates the authorization information to headers/query params and sends it along with the request", async () => { + const env = { + ...process.env, + secretKey: "test-secret-key", + serviceToken: "test-token", + }; + + const COLL_PATH = getTestJsonFilePath( + "aws-signature-auth-coll.json", + "collection" + ); + const ENVS_PATH = getTestJsonFilePath( + "aws-signature-auth-envs.json", + "environment" + ); + + const args = `test ${COLL_PATH} -e ${ENVS_PATH}`; + const { error } = await runCLI(args, { env }); + + expect(error).toBeNull(); + }); + }); }); describe("Test `hopp test --delay ` command:", () => { diff --git a/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/aws-signature-auth-coll.json b/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/aws-signature-auth-coll.json new file mode 100644 index 000000000..eae88b752 --- /dev/null +++ b/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/aws-signature-auth-coll.json @@ -0,0 +1,101 @@ +{ + "v": 3, + "name": "AWS Signature Auth - collection", + "folders": [], + "requests": [ + { + "v": "7", + "id": "cm0dm70cw000687bnxi830zz7", + "auth": { + "addTo": "HEADERS", + "region": "<>", + "authType": "aws-signature", + "accessKey": "<>", + "secretKey": "<>", + "authActive": true, + "serviceName": "<>", + "serviceToken": "", + "grantTypeInfo": { + "token": "", + "isPKCE": false, + "clientID": "", + "grantType": "AUTHORIZATION_CODE", + "authEndpoint": "", + "clientSecret": "", + "tokenEndpoint": "", + "codeVerifierMethod": "S256" + } + }, + "body": { + "body": null, + "contentType": null + }, + "name": "aws-signature-auth-headers", + "method": "GET", + "params": [], + "headers": [], + "endpoint": "<>", + "testScript": "pw.test(\"Successfully sends relevant AWS signature information via headers\", ()=> {\n const { headers } = pw.response.body\n\n // Dynamic values, hence comparing the type.\n pw.expect(headers[\"authorization\"]).toBeType(\"string\");\n pw.expect(headers[\"x-amz-date\"]).toBeType(\"string\");\n \n pw.expect(headers[\"x-amz-content-sha256\"]).toBe(\"UNSIGNED-PAYLOAD\")\n \n // No session token supplied\n pw.expect(headers[\"x-amz-security-token\"]).toBe(undefined)\n \n});", + "preRequestScript": "", + "requestVariables": [ + { + "key": "secretVarKey", + "value": "<>", + "active": true + } + ] + }, + { + "v": "7", + "id": "cm0dm70cw000687bnxi830zz7", + "auth": { + "addTo": "QUERY_PARAMS", + "region": "<>", + "authType": "aws-signature", + "accessKey": "<>", + "secretKey": "<>", + "authActive": true, + "serviceName": "<>", + "serviceToken": "<>", + "grantTypeInfo": { + "token": "", + "isPKCE": false, + "clientID": "", + "grantType": "AUTHORIZATION_CODE", + "authEndpoint": "", + "clientSecret": "", + "tokenEndpoint": "", + "codeVerifierMethod": "S256" + } + }, + "body": { + "body": null, + "contentType": null + }, + "name": "aws-signature-auth-query-params", + "method": "GET", + "params": [], + "headers": [], + "endpoint": "<>", + "testScript": "pw.test(\"Successfully sends relevant AWS signature information via query params\", ()=> {\n const { args } = pw.response.body\n pw.expect(args[\"X-Amz-Algorithm\"]).toBe(\"AWS4-HMAC-SHA256\");\n pw.expect(args[\"X-Amz-Algorithm\"]).toBe(\"AWS4-HMAC-SHA256\");\n pw.expect(args[\"X-Amz-Credential\"]).toInclude(\"test-access-key\");\n pw.expect(args[\"X-Amz-Credential\"]).toInclude(\"eu-west-1/s3\");\n\n // Dynamic values, hence comparing the type.\n pw.expect(args[\"X-Amz-Date\"]).toBeType(\"string\");\n pw.expect(args[\"X-Amz-Signature\"]).toBeType(\"string\");\n\n pw.expect(args[\"X-Amz-Expires\"]).toBe(\"86400\")\n pw.expect(args[\"X-Amz-SignedHeaders\"]).toBe(\"host\")\n pw.expect(args[\"X-Amz-Security-Token\"]).toBe(\"test-token\")\n \n});", + "preRequestScript": "", + "requestVariables": [ + { + "key": "awsRegion", + "value": "eu-west-1", + "active": true + }, + { + "key": "secretKey", + "value": "test-secret-key-overriden", + "active": true + } + ] + } + ], + "auth": { + "authType": "inherit", + "authActive": true + }, + "headers": [] +} diff --git a/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/environments/aws-signature-auth-envs.json b/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/environments/aws-signature-auth-envs.json new file mode 100644 index 000000000..534bf7113 --- /dev/null +++ b/packages/hoppscotch-cli/src/__tests__/e2e/fixtures/environments/aws-signature-auth-envs.json @@ -0,0 +1,35 @@ +{ + "v": 1, + "id": "cm0dsn3v70004p4qk3l9b7sjm", + "name": "AWS Signature - environments", + "variables": [ + { + "key": "awsRegion", + "value": "us-east-1", + "secret": false + }, + { + "key": "serviceName", + "value": "s3", + "secret": false + }, + { + "key": "accessKey", + "value": "test-access-key", + "secret": true + }, + { + "key": "secretKey", + "secret": true + }, + { + "key": "url", + "value": "https://echo.hoppscotch.io", + "secret": false + }, + { + "key": "serviceToken", + "secret": true + } + ] +} diff --git a/packages/hoppscotch-cli/src/utils/pre-request.ts b/packages/hoppscotch-cli/src/utils/pre-request.ts index b8d41cf1d..2908a25fb 100644 --- a/packages/hoppscotch-cli/src/utils/pre-request.ts +++ b/packages/hoppscotch-cli/src/utils/pre-request.ts @@ -16,6 +16,7 @@ import * as TE from "fp-ts/TaskEither"; import { flow, pipe } from "fp-ts/function"; import * as S from "fp-ts/string"; import qs from "qs"; +import { AwsV4Signer } from "aws4fetch"; import { EffectiveHoppRESTRequest } from "../interfaces/request"; import { HoppCLIError, error } from "../types/errors"; @@ -53,7 +54,13 @@ export const preRequestScriptRunner = ( variables: [...(selected ?? []), ...(global ?? [])], } ), - TE.chainEitherKW((env) => getEffectiveRESTRequest(request, env)), + TE.chainW((env) => + TE.tryCatch( + () => getEffectiveRESTRequest(request, env), + (reason) => error({ code: "PRE_REQUEST_SCRIPT_ERROR", data: reason }) + ) + ), + TE.chainEitherKW((effectiveRequest) => effectiveRequest), TE.mapLeft((reason) => isHoppCLIError(reason) ? reason @@ -72,12 +79,14 @@ export const preRequestScriptRunner = ( * * @returns An object with extra fields defining a complete request */ -export function getEffectiveRESTRequest( +export async function getEffectiveRESTRequest( request: HoppRESTRequest, environment: Environment -): E.Either< - HoppCLIError, - { effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs } +): Promise< + E.Either< + HoppCLIError, + { effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs } + > > { const envVariables = environment.variables; @@ -170,6 +179,59 @@ export function getEffectiveRESTRequest( description: "", }); } + } else if (request.auth.authType === "aws-signature") { + const { addTo } = request.auth; + + const currentDate = new Date(); + const amzDate = currentDate.toISOString().replace(/[:-]|\.\d{3}/g, ""); + const { method, endpoint } = request; + + const signer = new AwsV4Signer({ + method, + datetime: amzDate, + signQuery: addTo === "QUERY_PARAMS", + accessKeyId: parseTemplateString( + request.auth.accessKey, + resolvedVariables + ), + secretAccessKey: parseTemplateString( + request.auth.secretKey, + resolvedVariables + ), + region: + parseTemplateString(request.auth.region, resolvedVariables) ?? + "us-east-1", + service: parseTemplateString( + request.auth.serviceName, + resolvedVariables + ), + url: parseTemplateString(endpoint, resolvedVariables), + sessionToken: + request.auth.serviceToken && + parseTemplateString(request.auth.serviceToken, resolvedVariables), + }); + + const sign = await signer.sign(); + + if (addTo === "HEADERS") { + sign.headers.forEach((value, key) => { + effectiveFinalHeaders.push({ + active: true, + key, + value, + description: "", + }); + }); + } else if (addTo === "QUERY_PARAMS") { + sign.url.searchParams.forEach((value, key) => { + effectiveFinalParams.push({ + active: true, + key, + value, + description: "", + }); + }); + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f522dbd5d..191672185 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -319,6 +319,9 @@ importers: packages/hoppscotch-cli: dependencies: + aws4fetch: + specifier: 1.0.19 + version: 1.0.19 axios: specifier: 1.7.5 version: 1.7.5