feat: ability to refresh tokens for oauth flows (#4302)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Akash K
2024-08-29 13:27:31 +05:30
committed by GitHub
parent 2ed7221182
commit 181ad098e0
20 changed files with 488 additions and 68 deletions

View File

@@ -159,6 +159,30 @@ describe("hopp test [options] <file_path_or_id>", () => {
expect(error).toBeNull();
});
describe("OAuth 2 Authorization type with Authorization Code Grant Type", () => {
test("Successfully translates the authorization information to headers/query params and sends it along with the request", async () => {
const args = `test ${getTestJsonFilePath(
"oauth2-auth-code-coll.json",
"collection"
)}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
describe("multipart/form-data content type", () => {
test("Successfully derives the relevant headers based and sends the form data in the request body", async () => {
const args = `test ${getTestJsonFilePath(
"oauth2-auth-code-coll.json",
"collection"
)}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
});
describe("Test `hopp test <file_path_or_id> --env <file_path_or_id>` command:", () => {

View File

@@ -0,0 +1,55 @@
{
"v": 3,
"name": "Multpart form data content type - Collection",
"folders": [],
"requests": [
{
"v": "7",
"endpoint": "https://echo.hoppscotch.io",
"name": "multipart-form-data-sample-req",
"params": [],
"headers": [],
"method": "POST",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "HEADERS",
"grantTypeInfo": {
"authEndpoint": "test-authorization-endpoint",
"tokenEndpoint": "test-token-endpont",
"clientID": "test-client-id",
"clientSecret": "test-client-secret",
"isPKCE": true,
"codeVerifierMethod": "S256",
"grantType": "AUTHORIZATION_CODE",
"token": "test-token"
}
},
"preRequestScript": "",
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully derives the relevant headers based on the content type\", () => {\n pw.expect(pw.response.body.headers['content-type']).toInclude(\"multipart/form-data\");\n});\n\npw.test(\"Successfully sends the form data in the request body\", () => {\n // Dynamic value\n pw.expect(pw.response.body.data).toBeType(\"string\");\n});",
"body": {
"contentType": "multipart/form-data",
"body": [
{
"key": "key1",
"value": "value1",
"active": true,
"isFile": false
},
{
"key": "key2",
"value": [{}],
"active": true,
"isFile": true
}
]
},
"requestVariables": []
}
],
"auth": {
"authType": "none",
"authActive": true
},
"headers": []
}

View File

@@ -0,0 +1,72 @@
{
"v": 3,
"name": "OAuth2 Authorization Code Grant Type - Collection",
"folders": [],
"requests": [
{
"v": "7",
"endpoint": "https://echo.hoppscotch.io",
"name": "oauth2-auth-code-sample-req-pass-by-headers",
"params": [],
"headers": [],
"method": "GET",
"auth": {
"authType": "oauth-2",
"authActive": true,
"addTo": "HEADERS",
"grantTypeInfo": {
"authEndpoint": "test-authorization-endpoint",
"tokenEndpoint": "test-token-endpont",
"clientID": "test-client-id",
"clientSecret": "test-client-secret",
"isPKCE": true,
"codeVerifierMethod": "S256",
"grantType": "AUTHORIZATION_CODE",
"token": "test-token"
}
},
"preRequestScript": "",
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully derives Authorization header from the supplied fields\", ()=> {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBeType(\"string\");\n});",
"body": {
"contentType": null,
"body": null
},
"requestVariables": []
},
{
"v": "7",
"endpoint": "https://echo.hoppscotch.io",
"name": "oauth2-auth-code-sample-req-pass-by-query-params",
"params": [],
"headers": [],
"method": "GET",
"auth": {
"authType": "oauth-2",
"authActive": true,
"addTo": "HEADERS",
"grantTypeInfo": {
"authEndpoint": "test-authorization-endpoint",
"tokenEndpoint": "test-token-endpont",
"clientID": "test-client-id",
"clientSecret": "test-client-secret",
"isPKCE": true,
"codeVerifierMethod": "S256",
"grantType": "AUTHORIZATION_CODE",
"token": "test-token"
}
},
"preRequestScript": "",
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully derives Authorization header from the supplied fields\", ()=> {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBeType(\"string\");\n});",
"body": {
"contentType": null,
"body": null
},
"requestVariables": []
}
],
"auth": {
"authType": "none",
"authActive": true
},
"headers": []
}

View File

@@ -15,7 +15,6 @@ describe("requestRunner", () => {
};
beforeEach(() => {
SAMPLE_REQUEST_CONFIG.supported = false;
SAMPLE_REQUEST_CONFIG.url = "https://example.com";
SAMPLE_REQUEST_CONFIG.method = "GET";
jest.clearAllMocks();
@@ -70,7 +69,6 @@ describe("requestRunner", () => {
it("Should handle axios-error with request info.", () => {
jest.spyOn(axios, "isAxiosError").mockReturnValue(true);
SAMPLE_REQUEST_CONFIG.supported = true;
(axios as unknown as jest.Mock).mockRejectedValueOnce(<AxiosError>{
name: "name",
message: "message",
@@ -91,7 +89,6 @@ describe("requestRunner", () => {
});
it("Should successfully execute.", () => {
SAMPLE_REQUEST_CONFIG.supported = true;
(axios as unknown as jest.Mock).mockResolvedValue(<AxiosResponse>{
data: "data",
status: 200,

View File

@@ -20,8 +20,7 @@ export interface RequestStack {
* @property {boolean} supported - Boolean check for supported or unsupported requests.
*/
export interface RequestConfig extends AxiosRequestConfig {
supported: boolean;
displayUrl?: string
displayUrl?: string;
}
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
@@ -32,7 +31,17 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
*/
effectiveFinalURL: string;
effectiveFinalDisplayURL?: string;
effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
effectiveFinalParams: { key: string; value: string; active: boolean }[];
effectiveFinalHeaders: {
key: string;
value: string;
active: boolean;
description: string;
}[];
effectiveFinalParams: {
key: string;
value: string;
active: boolean;
description: string;
}[];
effectiveFinalBody: FormData | string | null;
}

View File

@@ -72,11 +72,12 @@ export const getEffectiveFinalMetaData = (
* Selecting only non-empty and active pairs.
*/
A.filter(({ key, active }) => !S.isEmpty(key) && active),
A.map(({ key, value }) => {
A.map(({ key, value, description }) => {
return {
active: true,
key: parseTemplateStringE(key, resolvedVariables),
value: parseTemplateStringE(value, resolvedVariables),
description,
};
}),
E.fromPredicate(
@@ -91,9 +92,14 @@ export const getEffectiveFinalMetaData = (
/**
* Filtering and mapping only right-eithers for each key-value as [string, string].
*/
A.filterMap(({ key, value }) =>
A.filterMap(({ key, value, description }) =>
E.isRight(key) && E.isRight(value)
? O.some({ active: true, key: key.right, value: value.right })
? O.some({
active: true,
key: key.right,
value: value.right,
description,
})
: O.none
)
)

View File

@@ -123,12 +123,14 @@ export function getEffectiveRESTRequest(
active: true,
key: "Authorization",
value: `Basic ${btoa(`${username}:${password}`)}`,
description: "",
});
} else if (request.auth.authType === "bearer") {
effectiveFinalHeaders.push({
active: true,
key: "Authorization",
value: `Bearer ${parseTemplateString(request.auth.token, resolvedVariables)}`,
description: "",
});
} else if (request.auth.authType === "oauth-2") {
const { addTo } = request.auth;
@@ -138,6 +140,7 @@ export function getEffectiveRESTRequest(
active: true,
key: "Authorization",
value: `Bearer ${parseTemplateString(request.auth.grantTypeInfo.token, resolvedVariables)}`,
description: "",
});
} else if (addTo === "QUERY_PARAMS") {
effectiveFinalParams.push({
@@ -147,6 +150,7 @@ export function getEffectiveRESTRequest(
request.auth.grantTypeInfo.token,
resolvedVariables
),
description: "",
});
}
} else if (request.auth.authType === "api-key") {
@@ -156,12 +160,14 @@ export function getEffectiveRESTRequest(
active: true,
key: parseTemplateString(key, resolvedVariables),
value: parseTemplateString(value, resolvedVariables),
description: "",
});
} else if (addTo === "QUERY_PARAMS") {
effectiveFinalParams.push({
active: true,
key: parseTemplateString(key, resolvedVariables),
value: parseTemplateString(value, resolvedVariables),
description: "",
});
}
}
@@ -187,6 +193,7 @@ export function getEffectiveRESTRequest(
active: true,
key: "Content-Type",
value: request.body.contentType,
description: "",
});
}

View File

@@ -32,8 +32,6 @@ import { getDurationInSeconds, getMetaDataPairs } from "./getters";
import { preRequestScriptRunner } from "./pre-request";
import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
// !NOTE: The `config.supported` checks are temporary until OAuth2 and Multipart Forms are supported
/**
* Processes given variable, which includes checking for secret variables
* and getting value from system environment
@@ -75,46 +73,20 @@ const processEnvs = (envs: Partial<HoppEnvs>) => {
*/
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
const config: RequestConfig = {
supported: true,
displayUrl: req.effectiveFinalDisplayURL,
};
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
const reqParams = finalParams(req);
const reqHeaders = finalHeaders(req);
config.url = finalEndpoint(req);
config.method = req.method as Method;
config.params = getMetaDataPairs(reqParams);
config.headers = getMetaDataPairs(reqHeaders);
if (req.auth.authActive) {
switch (req.auth.authType) {
case "oauth-2": {
// TODO: OAuth2 Request Parsing
// !NOTE: Temporary `config.supported` check
config.supported = false;
}
default: {
break;
}
}
}
const resolvedContentType =
config.headers["Content-Type"] ?? req.body.contentType;
if (resolvedContentType) {
switch (resolvedContentType) {
case "multipart/form-data": {
// TODO: Parse Multipart Form Data
// !NOTE: Temporary `config.supported` check
config.supported = false;
break;
}
default: {
config.data = finalBody(req);
break;
}
}
}
config.data = finalBody(req);
return config;
};
@@ -149,13 +121,6 @@ export const requestRunner =
duration: 0,
};
// !NOTE: Temporary `config.supported` check
if ((config as RequestConfig).supported === false) {
status = 501;
runnerResponse.status = status;
runnerResponse.statusText = responseErrors[status];
}
const end = hrtime(start);
const duration = getDurationInSeconds(end);
runnerResponse.duration = duration;
@@ -182,10 +147,6 @@ export const requestRunner =
runnerResponse.statusText = statusText;
runnerResponse.status = status;
runnerResponse.headers = headers;
} else if ((e.config as RequestConfig).supported === false) {
status = 501;
runnerResponse.status = status;
runnerResponse.statusText = responseErrors[status];
} else if (e.request) {
return E.left(error({ code: "REQUEST_ERROR", data: E.toError(e) }));
}

View File

@@ -126,6 +126,7 @@
},
"authorization": {
"generate_token": "Generate Token",
"refresh_token": "Refresh Token",
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
"include_in_url": "Include in URL",
"inherited_from": "Inherited {auth} from parent collection {collection} ",
@@ -148,6 +149,9 @@
"token_fetched_successfully": "Token fetched successfully",
"token_fetch_failed": "Failed to fetch token",
"validation_failed": "Validation Failed, please check the form fields",
"no_refresh_token_present": "No Refresh Token present. Please run the token generation flow again",
"refresh_token_request_failed": "Refresh token request failed",
"token_refreshed_successfully": "Token refreshed successfully",
"label_authorization_endpoint": "Authorization Endpoint",
"label_client_id": "Client ID",
"label_client_secret": "Client Secret",

View File

@@ -255,7 +255,7 @@ onMounted(() => {
return
}
const { context, source, token }: PersistedOAuthConfig =
const { context, source, token, refresh_token }: PersistedOAuthConfig =
JSON.parse(localOAuthTempConfig)
if (source === "REST") {
@@ -279,6 +279,10 @@ onMounted(() => {
const grantTypeInfo = auth.grantTypeInfo
grantTypeInfo && (grantTypeInfo.token = token ?? "")
if (refresh_token && grantTypeInfo.grantType === "AUTHORIZATION_CODE") {
grantTypeInfo.refreshToken = refresh_token
}
}
editingProperties.value = unsavedCollectionProperties

View File

@@ -428,7 +428,7 @@ onMounted(() => {
return
}
const { context, source, token }: PersistedOAuthConfig =
const { context, source, token, refresh_token }: PersistedOAuthConfig =
JSON.parse(localOAuthTempConfig)
if (source === "GraphQL") {
@@ -452,6 +452,10 @@ onMounted(() => {
const grantTypeInfo = auth.grantTypeInfo
grantTypeInfo && (grantTypeInfo.token = token ?? "")
if (refresh_token && grantTypeInfo.grantType === "AUTHORIZATION_CODE") {
grantTypeInfo.refreshToken = refresh_token
}
}
editingProperties.value = unsavedCollectionProperties

View File

@@ -165,12 +165,18 @@
</span>
</div>
<div class="p-2">
<div class="p-2 gap-1 flex">
<HoppButtonSecondary
filled
:label="`${t('authorization.generate_token')}`"
@click="generateOAuthToken()"
/>
<HoppButtonSecondary
v-if="runTokenRefresh"
filled
:label="`${t('authorization.refresh_token')}`"
@click="refreshOauthToken()"
/>
</div>
</div>
</template>
@@ -192,6 +198,7 @@ import { getCombinedEnvVariables } from "~/helpers/preRequest"
import { AggregateEnvironment } from "~/newstore/environments"
import authCode, {
AuthCodeOauthFlowParams,
AuthCodeOauthRefreshParams,
getDefaultAuthCodeOauthFlowParams,
} from "~/services/oauth/flows/authCode"
import clientCredentials, {
@@ -356,6 +363,48 @@ const supportedGrantTypes = [
}
)
const refreshToken = async () => {
const grantTypeInfo = auth.value.grantTypeInfo
if (!("refreshToken" in grantTypeInfo)) {
return E.left("NO_REFRESH_TOKEN_PRESENT" as const)
}
const refreshToken = grantTypeInfo.refreshToken
if (!refreshToken) {
return E.left("NO_REFRESH_TOKEN_PRESENT" as const)
}
const params: AuthCodeOauthRefreshParams = {
clientID: clientID.value,
clientSecret: clientSecret.value,
tokenEndpoint: tokenEndpoint.value,
refreshToken,
}
const unwrappedParams = replaceTemplateStringsInObjectValues(params)
const refreshTokenFunc = authCode.refreshToken
if (!refreshTokenFunc) {
return E.left("REFRESH_TOKEN_FUNCTION_NOT_DEFINED" as const)
}
const res = await refreshTokenFunc(unwrappedParams)
if (E.isLeft(res)) {
return E.left("OAUTH_REFRESH_TOKEN_FAILED" as const)
}
setAccessTokenInActiveContext(
res.right.access_token,
res.right.refresh_token
)
return E.right(undefined)
}
const runAction = () => {
const params: AuthCodeOauthFlowParams = {
authEndpoint: authEndpoint.value,
@@ -456,6 +505,7 @@ const supportedGrantTypes = [
return {
runAction,
refreshToken,
elements,
}
}),
@@ -854,13 +904,28 @@ const selectedGrantType = computed(() => {
)
})
const setAccessTokenInActiveContext = (accessToken?: string) => {
const setAccessTokenInActiveContext = (
accessToken?: string,
refreshToken?: string
) => {
if (props.isCollectionProperty && accessToken) {
auth.value.grantTypeInfo = {
...auth.value.grantTypeInfo,
token: accessToken,
}
// set the refresh token if provided
// we also make sure the grantTypes supporting refreshTokens are
if (
refreshToken &&
auth.value.grantTypeInfo.grantType === "AUTHORIZATION_CODE"
) {
auth.value.grantTypeInfo = {
...auth.value.grantTypeInfo,
refreshToken,
}
}
return
}
@@ -874,6 +939,16 @@ const setAccessTokenInActiveContext = (accessToken?: string) => {
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.token =
accessToken
}
if (
refreshToken &&
tabService.currentActiveTab.value.document.request.auth.authType ===
"oauth-2"
) {
// @ts-expect-error - todo: narrow the grantType to only supporting refresh tokens
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.refreshToken =
refreshToken
}
}
const changeSelectedGrantType = (
@@ -905,10 +980,53 @@ const runAction = computed(() => {
return selectedGrantType.value?.formElements.value?.runAction
})
const runTokenRefresh = computed(() => {
// the only grant type that supports refresh tokens is the authCode grant type
if (selectedGrantType.value?.id === "authCode") {
return selectedGrantType.value?.formElements.value?.refreshToken
}
return null
})
const currentOAuthGrantTypeFormElements = computed(() => {
return selectedGrantType.value?.formElements.value?.elements.value
})
const refreshOauthToken = async () => {
if (!runTokenRefresh.value) {
return
}
const res = await runTokenRefresh.value()
if (E.isLeft(res)) {
const errorMessages = {
NO_REFRESH_TOKEN_PRESENT: t(
"authorization.oauth.no_refresh_token_present"
),
REFRESH_TOKEN_FUNCTION_NOT_DEFINED: t(
"authorization.oauth.refresh_token_request_failed"
),
OAUTH_REFRESH_TOKEN_FAILED: t(
"authorization.oauth.refresh_token_request_failed"
),
}
const isKnownError = res.left in errorMessages
if (!isKnownError) {
toast.error(t("authorization.oauth.refresh_token_failed"))
return
}
toast.error(errorMessages[res.left])
return
}
toast.success(t("authorization.oauth.token_refreshed_successfully"))
}
const generateOAuthToken = async () => {
if (
grantTypesInvolvingRedirect.includes(auth.value.grantTypeInfo.grantType)

View File

@@ -97,6 +97,11 @@ onMounted(async () => {
...persistedOAuthConfig,
token: tokenInfo.right.access_token,
}
if (tokenInfo.right.refresh_token) {
authConfig.refresh_token = tokenInfo.right.refresh_token
}
persistenceService.setLocalConfig(
"oauth_temp_config",
JSON.stringify(authConfig)
@@ -118,6 +123,14 @@ onMounted(async () => {
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.token =
tokenInfo.right.access_token
if (
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo
.grantType === "AUTHORIZATION_CODE"
) {
tabService.currentActiveTab.value.document.request.auth.grantTypeInfo.refreshToken =
tokenInfo.right.refresh_token
}
toast.success(t("authorization.oauth.token_fetched_successfully"))
}

View File

@@ -46,6 +46,13 @@ export type AuthCodeOauthFlowParams = z.infer<
typeof AuthCodeOauthFlowParamsSchema
>
export type AuthCodeOauthRefreshParams = {
tokenEndpoint: string
clientID: string
clientSecret?: string
refreshToken: string
}
export const getDefaultAuthCodeOauthFlowParams =
(): AuthCodeOauthFlowParams => ({
authEndpoint: "",
@@ -233,6 +240,7 @@ const handleRedirectForAuthCodeOauthFlow = async (localConfig: string) => {
const withAccessTokenSchema = z.object({
access_token: z.string(),
refresh_token: z.string().optional(),
})
const parsedTokenResponse = withAccessTokenSchema.safeParse(
@@ -284,9 +292,60 @@ const encodeArrayBufferAsUrlEncodedBase64 = (buffer: ArrayBuffer) => {
return hashBase64URL
}
const refreshToken = async ({
tokenEndpoint,
clientID,
refreshToken,
clientSecret,
}: AuthCodeOauthRefreshParams) => {
const formData = new URLSearchParams()
formData.append("grant_type", "refresh_token")
formData.append("refresh_token", refreshToken)
formData.append("client_id", clientID)
if (clientSecret) {
formData.append("client_secret", clientSecret)
}
const { response } = interceptorService.runRequest({
url: tokenEndpoint,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
data: formData.toString(),
})
const res = await response
if (E.isLeft(res)) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const responsePayload = decodeResponseAsJSON(res.right)
if (E.isLeft(responsePayload)) {
return E.left("AUTH_TOKEN_REQUEST_FAILED" as const)
}
const withAccessTokenAndRefreshTokenSchema = z.object({
access_token: z.string(),
refresh_token: z.string().optional(),
})
const parsedTokenResponse = withAccessTokenAndRefreshTokenSchema.safeParse(
responsePayload.right
)
return parsedTokenResponse.success
? E.right(parsedTokenResponse.data)
: E.left("AUTH_TOKEN_REQUEST_INVALID_RESPONSE" as const)
}
export default createFlowConfig(
"AUTHORIZATION_CODE" as const,
AuthCodeOauthFlowParamsSchema,
initAuthCodeOauthFlow,
handleRedirectForAuthCodeOauthFlow
handleRedirectForAuthCodeOauthFlow,
refreshToken
)

View File

@@ -22,6 +22,7 @@ export type PersistedOAuthConfig = {
state: string
}
token?: string
refresh_token?: string
}
const persistenceService = getService(PersistenceService)
@@ -66,6 +67,7 @@ export function createFlowConfig<
Flow extends string,
AuthParams extends Record<string, unknown>,
InitFuncReturnObject extends Record<string, unknown>,
RefreshTokenParams extends Record<string, unknown>,
>(
flow: Flow,
params: ZodType<AuthParams>,
@@ -81,8 +83,14 @@ export function createFlowConfig<
string,
{
access_token: string
refresh_token?: string
}
>
>,
refreshToken?: (
params: RefreshTokenParams
) => Promise<
E.Either<string, { access_token: string; refresh_token?: string }>
>
) {
return {
@@ -90,6 +98,7 @@ export function createFlowConfig<
params,
init,
onRedirectReceived,
refreshToken,
}
}

View File

@@ -5,11 +5,14 @@ import { GQLHeader as GQLHeaderV1 } from "../../graphql/v/1"
import { GQLHeader as GQLHeaderV2 } from "../../graphql/v/6"
import { HoppRESTHeaders as V1_HoppRESTHeaders } from "../../rest/v/1"
import { HoppRESTHeaders as V2_HoppRESTHeaders } from "../../rest/v/7"
import { HoppRESTAuth } from "../../rest/v/7"
import { HoppGQLAuth } from "../../graphql/v/6"
import { v2_baseCollectionSchema, V2_SCHEMA } from "./2"
const v3_baseCollectionSchema = v2_baseCollectionSchema.extend({
v: z.literal(3),
headers: z.union([V2_HoppRESTHeaders, z.array(GQLHeaderV2)]),
auth: z.union([HoppRESTAuth, HoppGQLAuth]),
})
type Input = z.input<typeof v3_baseCollectionSchema> & {

View File

@@ -15,7 +15,7 @@ export {
} from "./v/2"
export { GQLHeader } from "./v/6"
export { HoppGQLAuth, HoppGQLAuthOAuth2 } from "./v/5"
export { HoppGQLAuth, HoppGQLAuthOAuth2 } from "./v/6"
export { HoppGQLAuthAPIKey } from "./v/4"

View File

@@ -1,7 +1,33 @@
import { defineVersion } from "verzod"
import { z } from "zod"
import { V5_SCHEMA } from "./5"
import { HoppRESTAuthOAuth2 } from "./../../rest/v/7"
import {
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppGQLAuthInherit,
HoppGQLAuthNone,
} from "./2"
import { HoppGQLAuthAPIKey } from "./4"
export { HoppRESTAuthOAuth2 as HoppGQLAuthOAuth2 } from "../../rest/v/7"
export const HoppGQLAuth = z
.discriminatedUnion("authType", [
HoppGQLAuthNone,
HoppGQLAuthInherit,
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppGQLAuthAPIKey,
HoppRESTAuthOAuth2, // both rest and gql have the same auth type for oauth2
])
.and(
z.object({
authActive: z.boolean(),
})
)
export type HoppGQLAuth = z.infer<typeof HoppGQLAuth>
export const GQLHeader = z.object({
key: z.string().catch(""),
@@ -15,6 +41,7 @@ export type GQLHeader = z.infer<typeof GQLHeader>
export const V6_SCHEMA = V5_SCHEMA.extend({
v: z.literal(6),
headers: z.array(GQLHeader).catch([]),
auth: HoppGQLAuth,
})
export default defineVersion({

View File

@@ -13,12 +13,10 @@ import V3_VERSION from "./v/3"
import V4_VERSION from "./v/4"
import V5_VERSION from "./v/5"
import V6_VERSION, { HoppRESTReqBody } from "./v/6"
import V7_VERSION, { HoppRESTAuth } from "./v/7"
import { HoppRESTHeaders, HoppRESTParams } from "./v/7"
import V7_VERSION from "./v/7"
import { HoppRESTRequestVariables } from "./v/2"
import { HoppRESTAuth } from "./v/5"
export * from "./content-types"
@@ -37,11 +35,8 @@ export {
PasswordGrantTypeParams,
} from "./v/3"
export {
AuthCodeGrantTypeParams,
HoppRESTAuth,
HoppRESTAuthOAuth2,
} from "./v/5"
export { AuthCodeGrantTypeParams } from "./v/5"
export { HoppRESTAuthOAuth2, HoppRESTAuth } from "./v/7"
export { HoppRESTAuthAPIKey } from "./v/4"
@@ -159,6 +154,7 @@ export function safelyExtractRESTRequest(
const result = HoppRESTAuth.safeParse(x.auth)
if (result.success) {
// @ts-ignore
req.auth = result.data
}
}

View File

@@ -3,6 +3,56 @@ import { z } from "zod"
import { V6_SCHEMA } from "./6"
import { AuthCodeGrantTypeParams as AuthCodeGrantTypeParamsOld } from "./5"
import {
ClientCredentialsGrantTypeParams,
ImplicitOauthFlowParams,
PasswordGrantTypeParams,
} from "./3"
import {
HoppRESTAuthAPIKey,
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthInherit,
HoppRESTAuthNone,
} from "./1"
// Add refreshToken to all grant types except Implicit
export const AuthCodeGrantTypeParams = AuthCodeGrantTypeParamsOld.extend({
refreshToken: z.string().optional(),
})
export const HoppRESTAuthOAuth2 = z.object({
authType: z.literal("oauth-2"),
grantTypeInfo: z.discriminatedUnion("grantType", [
AuthCodeGrantTypeParams,
ClientCredentialsGrantTypeParams,
PasswordGrantTypeParams,
ImplicitOauthFlowParams,
]),
addTo: z.enum(["HEADERS", "QUERY_PARAMS"]).catch("HEADERS"),
})
export type HoppRESTAuthOAuth2 = z.infer<typeof HoppRESTAuthOAuth2>
export const HoppRESTAuth = z
.discriminatedUnion("authType", [
HoppRESTAuthNone,
HoppRESTAuthInherit,
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthOAuth2,
HoppRESTAuthAPIKey,
])
.and(
z.object({
authActive: z.boolean(),
})
)
export type HoppRESTAuth = z.infer<typeof HoppRESTAuth>
export const HoppRESTParams = z.array(
z.object({
key: z.string().catch(""),
@@ -29,6 +79,7 @@ export const V7_SCHEMA = V6_SCHEMA.extend({
v: z.literal("7"),
params: HoppRESTParams,
headers: HoppRESTHeaders,
auth: HoppRESTAuth,
})
export default defineVersion({
@@ -54,6 +105,7 @@ export default defineVersion({
v: "7" as const,
params,
headers,
// no need to update anything for HoppRESTAuth, because the newly added refreshToken is optional
}
},
})