feat(cli): add support for AWS Signature Authorization type (#4305)

This commit is contained in:
James George
2024-08-30 01:42:32 -07:00
committed by GitHub
parent 703b71de2c
commit 9ca899a28a
6 changed files with 231 additions and 6 deletions

View File

@@ -42,6 +42,7 @@
"private": false, "private": false,
"dependencies": { "dependencies": {
"axios": "1.7.5", "axios": "1.7.5",
"aws4fetch": "1.0.19",
"chalk": "5.3.0", "chalk": "5.3.0",
"commander": "11.1.0", "commander": "11.1.0",
"isolated-vm": "4.7.2", "isolated-vm": "4.7.2",

View File

@@ -385,7 +385,6 @@ describe("hopp test [options] <file_path_or_id>", () => {
test("Supports the usage of request variables along with environment variables", async () => { test("Supports the usage of request variables along with environment variables", async () => {
const env = { const env = {
...process.env, ...process.env,
secretBasicAuthUsernameEnvVar: "username",
secretBasicAuthPasswordEnvVar: "password", secretBasicAuthPasswordEnvVar: "password",
}; };
@@ -407,6 +406,30 @@ describe("hopp test [options] <file_path_or_id>", () => {
expect(error).toBeNull(); 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 <file_path_or_id> --delay <delay_in_ms>` command:", () => { describe("Test `hopp test <file_path_or_id> --delay <delay_in_ms>` command:", () => {

View File

@@ -0,0 +1,101 @@
{
"v": 3,
"name": "AWS Signature Auth - collection",
"folders": [],
"requests": [
{
"v": "7",
"id": "cm0dm70cw000687bnxi830zz7",
"auth": {
"addTo": "HEADERS",
"region": "<<awsRegion>>",
"authType": "aws-signature",
"accessKey": "<<accessKey>>",
"secretKey": "<<secretVarKey>>",
"authActive": true,
"serviceName": "<<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": "<<url>>",
"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": "<<secretKey>>",
"active": true
}
]
},
{
"v": "7",
"id": "cm0dm70cw000687bnxi830zz7",
"auth": {
"addTo": "QUERY_PARAMS",
"region": "<<awsRegion>>",
"authType": "aws-signature",
"accessKey": "<<accessKey>>",
"secretKey": "<<secretKey>>",
"authActive": true,
"serviceName": "<<serviceName>>",
"serviceToken": "<<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": "<<url>>",
"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": []
}

View File

@@ -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
}
]
}

View File

@@ -16,6 +16,7 @@ import * as TE from "fp-ts/TaskEither";
import { flow, pipe } from "fp-ts/function"; import { flow, pipe } from "fp-ts/function";
import * as S from "fp-ts/string"; import * as S from "fp-ts/string";
import qs from "qs"; import qs from "qs";
import { AwsV4Signer } from "aws4fetch";
import { EffectiveHoppRESTRequest } from "../interfaces/request"; import { EffectiveHoppRESTRequest } from "../interfaces/request";
import { HoppCLIError, error } from "../types/errors"; import { HoppCLIError, error } from "../types/errors";
@@ -53,7 +54,13 @@ export const preRequestScriptRunner = (
variables: [...(selected ?? []), ...(global ?? [])], 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) => TE.mapLeft((reason) =>
isHoppCLIError(reason) isHoppCLIError(reason)
? reason ? reason
@@ -72,12 +79,14 @@ export const preRequestScriptRunner = (
* *
* @returns An object with extra fields defining a complete request * @returns An object with extra fields defining a complete request
*/ */
export function getEffectiveRESTRequest( export async function getEffectiveRESTRequest(
request: HoppRESTRequest, request: HoppRESTRequest,
environment: Environment environment: Environment
): E.Either< ): Promise<
HoppCLIError, E.Either<
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs } HoppCLIError,
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
>
> { > {
const envVariables = environment.variables; const envVariables = environment.variables;
@@ -170,6 +179,59 @@ export function getEffectiveRESTRequest(
description: "", 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: "",
});
});
}
} }
} }

3
pnpm-lock.yaml generated
View File

@@ -319,6 +319,9 @@ importers:
packages/hoppscotch-cli: packages/hoppscotch-cli:
dependencies: dependencies:
aws4fetch:
specifier: 1.0.19
version: 1.0.19
axios: axios:
specifier: 1.7.5 specifier: 1.7.5
version: 1.7.5 version: 1.7.5