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:
@@ -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
|
||||
|
||||
56
packages/hoppscotch-common/src/helpers/auth/index.ts
Normal file
56
packages/hoppscotch-common/src/helpers/auth/index.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user