feat(cli): add support for AWS Signature Authorization type (#4305)
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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:", () => {
|
||||||
|
|||||||
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
3
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user