Browse Source

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

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
Akash K 6 months ago
parent
commit
181ad098e0

+ 24 - 0
packages/hoppscotch-cli/src/__tests__/e2e/commands/test.spec.ts

@@ -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:", () => {

+ 55 - 0
packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/multipart-form-data-coll.json

@@ -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": []
+}

+ 72 - 0
packages/hoppscotch-cli/src/__tests__/e2e/fixtures/collections/oauth2-auth-code-coll.json

@@ -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": []
+}

+ 0 - 3
packages/hoppscotch-cli/src/__tests__/functions/request/requestRunner.spec.ts

@@ -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,

+ 13 - 4
packages/hoppscotch-cli/src/interfaces/request.ts

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

+ 9 - 3
packages/hoppscotch-cli/src/utils/getters.ts

@@ -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
       )
     )

+ 7 - 0
packages/hoppscotch-cli/src/utils/pre-request.ts

@@ -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: "",
     });
   }
 

+ 4 - 43
packages/hoppscotch-cli/src/utils/request.ts

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

+ 4 - 0
packages/hoppscotch-common/locales/en.json

@@ -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",

+ 5 - 1
packages/hoppscotch-common/src/components/collections/graphql/index.vue

@@ -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

Some files were not shown because too many files changed in this diff