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) }));
}