feat: add support for AWS Signature auth type (#4142)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
Anwarul Islam
2024-08-30 14:30:13 +06:00
committed by GitHub
parent 5a2eed60c9
commit 703b71de2c
26 changed files with 1499 additions and 666 deletions

View File

@@ -185,7 +185,7 @@ export function runRESTRequest$(
const res = getFinalEnvsFromPreRequest(
tab.value.document.request.preRequestScript,
getCombinedEnvVariables()
).then((envs) => {
).then(async (envs) => {
if (cancelCalled) return E.left("cancellation" as const)
if (E.isLeft(envs)) {
@@ -254,7 +254,9 @@ export function runRESTRequest$(
variables: finalEnvsWithNonEmptyValues,
})
const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest)
const [stream, cancelRun] = createRESTNetworkRequestStream(
await effectiveRequest
)
cancelFunc = cancelRun
const subscription = stream

View File

@@ -0,0 +1,56 @@
import { getService } from "~/modules/dioc"
import { getCombinedEnvVariables } from "../preRequest"
import { RESTTabService } from "~/services/tab/rest"
import { parseTemplateStringE } from "@hoppscotch/data"
import * as E from "fp-ts/Either"
export const replaceTemplateStringsInObjectValues = <
T extends Record<string, unknown>,
>(
obj: T,
source: "REST" | "GQL" = "REST"
) => {
const envs = getCombinedEnvVariables()
const restTabsService = getService(RESTTabService)
const requestVariables =
source === "REST"
? restTabsService.currentActiveTab.value.document.request.requestVariables.map(
({ key, value }) => ({
key,
value,
secret: false,
})
)
: []
// Ensure request variables are prioritized by removing any selected/global environment variables with the same key
const selectedEnvVars = envs.selected.filter(
({ key }) =>
!requestVariables.some(({ key: reqVarKey }) => reqVarKey === key)
)
const globalEnvVars = envs.global.filter(
({ key }) =>
!requestVariables.some(({ key: reqVarKey }) => reqVarKey === key)
)
const envVars = [...selectedEnvVars, ...globalEnvVars, ...requestVariables]
const newObj: Partial<T> = {}
for (const key in obj) {
const val = obj[key]
if (typeof val === "string") {
const parseResult = parseTemplateStringE(val, envVars)
newObj[key] = E.isRight(parseResult)
? (parseResult.right as T[typeof key])
: (val as T[typeof key])
} else {
newObj[key] = val
}
}
return newObj as T
}

View File

@@ -1,5 +1,6 @@
import { GQLHeader, HoppGQLAuth, makeGQLRequest } from "@hoppscotch/data"
import { OperationType } from "@urql/core"
import { AwsV4Signer } from "aws4fetch"
import * as E from "fp-ts/Either"
import {
GraphQLEnumType,
@@ -286,6 +287,33 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
} else if (addTo === "QUERY_PARAMS") {
params[key] = value
}
} else if (auth.authType === "aws-signature") {
const { accessKey, secretKey, region, serviceName, addTo } = auth
const currentDate = new Date()
const amzDate = currentDate.toISOString().replace(/[:-]|\.\d{3}/g, "")
const signer = new AwsV4Signer({
datetime: amzDate,
signQuery: addTo === "QUERY_PARAMS",
accessKeyId: accessKey,
secretAccessKey: secretKey,
region: region ?? "us-east-1",
service: serviceName,
url,
})
const sign = await signer.sign()
if (addTo === "HEADERS") {
sign.headers.forEach((v, k) => {
finalHeaders[k] = v
})
} else if (addTo === "QUERY_PARAMS") {
for (const [k, v] of sign.url.searchParams) {
params[k] = v
}
}
}
}

View File

@@ -4,18 +4,6 @@ import * as N from "fp-ts/number"
import * as S from "fp-ts/string"
import { lodashIsEqualEq, mapThenEq, undefinedEq } from "./eq"
export type HoppGQLParam = {
key: string
value: string
active: boolean
}
export type HoppGQLHeader = {
key: string
value: string
active: boolean
}
export type FormDataKeyValue = {
key: string
active: boolean

View File

@@ -24,6 +24,7 @@ import {
import { arrayFlatMap, arraySort } from "../functional/array"
import { toFormData } from "../functional/formData"
import { tupleWithSameKeysToRecord } from "../functional/record"
import { AwsV4Signer } from "aws4fetch"
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
/**
@@ -47,7 +48,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
* @param showKeyIfSecret Whether to show the key if the value is a secret
* @returns The list of headers
*/
export const getComputedAuthHeaders = (
export const getComputedAuthHeaders = async (
envVars: Environment["variables"],
req?:
| HoppRESTRequest
@@ -131,6 +132,34 @@ export const getComputedAuthHeaders = (
description: "",
})
}
} else if (request.auth.authType === "aws-signature") {
const { addTo } = request.auth
if (addTo === "HEADERS") {
const currentDate = new Date()
const amzDate = currentDate.toISOString().replace(/[:-]|\.\d{3}/g, "")
const { method, endpoint } = req as HoppRESTRequest
const signer = new AwsV4Signer({
method: method,
datetime: amzDate,
accessKeyId: parseTemplateString(request.auth.accessKey, envVars),
secretAccessKey: parseTemplateString(request.auth.secretKey, envVars),
region:
parseTemplateString(request.auth.region, envVars) ?? "us-east-1",
service: parseTemplateString(request.auth.serviceName, envVars),
url: parseTemplateString(endpoint, envVars),
})
const sign = await signer.sign()
sign.headers.forEach((x, k) => {
headers.push({
active: true,
key: k,
value: x,
description: "",
})
})
}
}
return headers
@@ -186,7 +215,7 @@ export type ComputedHeader = {
* @param showKeyIfSecret Whether to show the key if the value is a secret
* @returns The headers that are generated along with the source of that header
*/
export const getComputedHeaders = (
export const getComputedHeaders = async (
req:
| HoppRESTRequest
| {
@@ -196,14 +225,16 @@ export const getComputedHeaders = (
envVars: Environment["variables"],
parse = true,
showKeyIfSecret = false
): ComputedHeader[] => {
): Promise<ComputedHeader[]> => {
return [
...getComputedAuthHeaders(
envVars,
req,
undefined,
parse,
showKeyIfSecret
...(
await getComputedAuthHeaders(
envVars,
req,
undefined,
parse,
showKeyIfSecret
)
).map((header) => ({
source: "auth" as const,
header,
@@ -227,22 +258,55 @@ export type ComputedParam = {
* @param envVars The environment variables active
* @returns The params that are generated along with the source of that header
*/
export const getComputedParams = (
export const getComputedParams = async (
req: HoppRESTRequest,
envVars: Environment["variables"]
): ComputedParam[] => {
): Promise<ComputedParam[]> => {
// When this gets complex, its best to split this function off (like with getComputedHeaders)
// API-key auth can be added to query params
if (!req.auth || !req.auth.authActive) {
return []
}
if (!req.auth || !req.auth.authActive) return []
if (req.auth.authType !== "api-key" && req.auth.authType !== "oauth-2") {
if (
req.auth.authType !== "api-key" &&
req.auth.authType !== "oauth-2" &&
req.auth.authType !== "aws-signature"
)
return []
}
if (req.auth.addTo !== "QUERY_PARAMS") {
return []
if (req.auth.addTo !== "QUERY_PARAMS") return []
if (req.auth.authType === "aws-signature") {
const { addTo } = req.auth
const params: ComputedParam[] = []
if (addTo === "QUERY_PARAMS") {
const currentDate = new Date()
const amzDate = currentDate.toISOString().replace(/[:-]|\.\d{3}/g, "")
const signer = new AwsV4Signer({
method: req.method,
datetime: amzDate,
signQuery: true,
accessKeyId: parseTemplateString(req.auth.accessKey, envVars),
secretAccessKey: parseTemplateString(req.auth.secretKey, envVars),
region: parseTemplateString(req.auth.region, envVars) ?? "us-east-1",
service: parseTemplateString(req.auth.serviceName, envVars),
url: parseTemplateString(req.endpoint, envVars),
})
const sign = await signer.sign()
for (const [k, v] of sign.url.searchParams) {
params.push({
source: "auth" as const,
param: {
active: true,
key: k,
value: v,
description: "",
},
})
}
}
return params
}
if (req.auth.authType === "api-key") {
@@ -259,19 +323,21 @@ export const getComputedParams = (
]
}
const { grantTypeInfo } = req.auth
return [
{
source: "auth",
param: {
active: true,
key: "access_token",
value: parseTemplateString(grantTypeInfo.token, envVars, false, true),
description: "",
if (req.auth.authType === "oauth-2") {
const { grantTypeInfo } = req.auth
return [
{
source: "auth",
param: {
active: true,
key: "access_token",
value: parseTemplateString(grantTypeInfo.token, envVars),
description: "",
},
},
},
]
]
}
return []
}
// Resolves environment variables in the body
@@ -405,18 +471,15 @@ function getFinalBodyFromRequest(
*
* @returns An object with extra fields defining a complete request
*/
export function getEffectiveRESTRequest(
export async function getEffectiveRESTRequest(
request: HoppRESTRequest,
environment: Environment,
showKeyIfSecret = false
): EffectiveHoppRESTRequest {
): Promise<EffectiveHoppRESTRequest> {
const effectiveFinalHeaders = pipe(
getComputedHeaders(
request,
environment.variables,
true,
showKeyIfSecret
).map((h) => h.header),
(await getComputedHeaders(request, environment.variables)).map(
(h) => h.header
),
A.concat(request.headers),
A.filter((x) => x.active && x.key !== ""),
A.map((x) => ({
@@ -437,7 +500,9 @@ export function getEffectiveRESTRequest(
)
const effectiveFinalParams = pipe(
getComputedParams(request, environment.variables).map((p) => p.param),
(await getComputedParams(request, environment.variables)).map(
(p) => p.param
),
A.concat(request.params),
A.filter((x) => x.active && x.key !== ""),
A.map((x) => ({
@@ -500,8 +565,8 @@ export function getEffectiveRESTRequest(
export function getEffectiveRESTRequestStream(
request$: Observable<HoppRESTRequest>,
environment$: Observable<Environment>
): Observable<EffectiveHoppRESTRequest> {
): Observable<Promise<EffectiveHoppRESTRequest>> {
return combineLatest([request$, environment$]).pipe(
map(([request, env]) => getEffectiveRESTRequest(request, env))
map(async ([request, env]) => await getEffectiveRESTRequest(request, env))
)
}