Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for Digest authorization #4339

Merged
merged 21 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: add support for disabling request retry
- Ensure failures reported in pre/post request scripts and request execution results in the CLI failing with a non-zero exit code.
- Remove redundant test case and related CLI test suite updates.
- Clean up.
  • Loading branch information
jamesgeorge007 committed Oct 29, 2024
commit 312fe762c7df1a54ba62dae4eefd555ff2845464
22 changes: 3 additions & 19 deletions packages/hoppscotch-cli/src/__tests__/e2e/commands/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,19 +370,7 @@ describe("hopp test [options] <file_path_or_id>", () => {
);

describe("Request variables", () => {
test("Picks active request variables and ignores inactive entries", async () => {
const COLL_PATH = getTestJsonFilePath(
"request-vars-coll.json",
"collection"
);

const args = `test ${COLL_PATH}`;

const { error } = await runCLI(args);
expect(error).toBeNull();
});

test("Supports the usage of request variables along with environment variables", async () => {
test("Picks active request variables and ignores inactive entries alongside the usage of environment variables", async () => {
const env = {
...process.env,
secretBasicAuthPasswordEnvVar: "password",
Expand Down Expand Up @@ -431,12 +419,8 @@ describe("hopp test [options] <file_path_or_id>", () => {
});
});

describe("Digest auth type", () => {
describe("Digest Authorization type", () => {
test("Successfully translates the authorization information to headers/query params and sends it along with the request", async () => {
const env = {
...process.env,
};

const COLL_PATH = getTestJsonFilePath(
"digest-auth-coll.json",
"collection"
Expand All @@ -447,7 +431,7 @@ describe("hopp test [options] <file_path_or_id>", () => {
);

const args = `test ${COLL_PATH} -e ${ENVS_PATH}`;
const { error } = await runCLI(args, { env });
const { error } = await runCLI(args);

expect(error).toBeNull();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"params": [],
"headers": [],
"endpoint": "<<url>>",
"testScript": "pw.test(\"Status code is 200\", ()=> { pw.expect(pw.response.status).toBe(200);});",
"testScript": "pw.test(\"Status code is 200\", ()=> { pw.expect(pw.response.status).toBe(200);}); \n pw.test(\"Receives the www-authenticate header\", ()=> { pw.expect(pw.response.headers['www-authenticate']).toBeType('string');});",
"preRequestScript": "",
"responses": {},
"requestVariables": []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"v": 3,
"name": "Digest Auth (failure state) - collection",
"folders": [],
"requests": [
{
"v": "8",
"id": "cm0dm70cw000687bnxi830zz7",
"auth": {
"authType": "digest",
"authActive": true,
"username": "<<username>>",
"password": "<<password>>",
"realm": "",
"nonce": "",
"algorithm": "MD5",
"qop": "auth",
"nc": "",
"cnonce": "",
"opaque": "",
"disableRetry": true
},
"body": {
"body": null,
"contentType": null
},
"name": "digest-auth-headers",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "<<url>>",
"testScript": "pw.test(\"Status code is not 200\", ()=> { pw.expect(pw.response.status).not.toBe(200);}); \n pw.test(\"Receives the www-authenticate header\", ()=> { pw.expect(pw.response.headers['www-authenticate']).not.toBeType('string');});",
"preRequestScript": "",
"responses": {},
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"v": 3,
"name": "Digest Auth (success state) - collection",
"folders": [],
"requests": [
{
"v": "8",
"id": "cm0dm70cw000687bnxi830zz7",
"auth": {
"authType": "digest",
"authActive": true,
"username": "<<username>>",
"password": "<<password>>",
"realm": "",
"nonce": "",
"algorithm": "MD5",
"qop": "auth",
"nc": "",
"cnonce": "",
"opaque": "",
"disableRetry": false
},
"body": {
"body": null,
"contentType": null
},
"name": "digest-auth-headers",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "<<url>>",
"testScript": "pw.test(\"Status code is 200\", ()=> { pw.expect(pw.response.status).toBe(200);}); \n pw.test(\"Receives the www-authenticate header\", ()=> { pw.expect(pw.response.headers['www-authenticate']).toBeType('string');});",
"preRequestScript": "",
"responses": {},
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
}
80 changes: 46 additions & 34 deletions packages/hoppscotch-cli/src/utils/auth/digest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { md5 } from "js-md5";
import axios from "axios";
import { md5 } from "js-md5";

import { exceptionColors } from "../getters";

export interface DigestAuthParams {
username: string;
Expand All @@ -15,8 +17,32 @@ export interface DigestAuthParams {
cnonce?: string; // client nonce (optional but typically required in qop='auth')
}

export interface DigestAuthInfo {
realm: string;
nonce: string;
qop: string;
opaque?: string;
algorithm: string;
}

// Utility function to parse Digest auth header values
const parseDigestAuthHeader = (
header: string
): { [key: string]: string } | null => {
const matches = header.match(/([a-z0-9]+)="([^"]+)"/gi);
if (!matches) return null;

const authParams: { [key: string]: string } = {};
matches.forEach((match) => {
const parts = match.split("=");
authParams[parts[0]] = parts[1].replace(/"/g, "");
});

return authParams;
};

// Function to generate Digest Auth Header
export async function generateDigestAuthHeader(params: DigestAuthParams) {
export const generateDigestAuthHeader = async (params: DigestAuthParams) => {
const {
username,
password,
Expand Down Expand Up @@ -55,21 +81,13 @@ export async function generateDigestAuthHeader(params: DigestAuthParams) {
}

return authHeader;
}
};

export interface DigestAuthInfo {
realm: string;
nonce: string;
qop: string;
opaque?: string;
algorithm: string;
}

export async function fetchInitialDigestAuthInfo(
export const fetchInitialDigestAuthInfo = async (
url: string,
method: string
): Promise<DigestAuthInfo> {
console.log("Fetching initial digest auth info...");
method: string,
disableRetry: boolean
): Promise<DigestAuthInfo> => {
try {
const initialResponse = await axios.request({
url,
Expand All @@ -78,7 +96,7 @@ export async function fetchInitialDigestAuthInfo(
});

// Check if the response status is 401 (which is expected in Digest Auth flow)
if (initialResponse.status === 401) {
if (initialResponse.status === 401 && !disableRetry) {
const authHeader = initialResponse.headers["www-authenticate"];

if (authHeader) {
Expand All @@ -101,27 +119,21 @@ export async function fetchInitialDigestAuthInfo(
throw new Error(
"Failed to parse authentication parameters from WWW-Authenticate header"
);
} else if (initialResponse.status === 401 && disableRetry) {
throw new Error(
`401 Unauthorized received. Retry is disabled as specified, so no further attempts will be made.`
);
} else {
throw new Error(`Unexpected response: ${initialResponse.status}`);
}
} catch (error) {
console.error("Error fetching initial digest auth info:", error);
const errMsg = error instanceof Error ? error.message : error;

console.error(
exceptionColors.FAIL(
`\n Error fetching initial digest auth info: ${errMsg} \n`
)
);
throw error; // Re-throw the error to handle it further up the chain if needed
}
}

// Utility function to parse Digest auth header values
function parseDigestAuthHeader(
header: string
): { [key: string]: string } | null {
const matches = header.match(/([a-z0-9]+)="([^"]+)"/gi);
if (!matches) return null;

const authParams: { [key: string]: string } = {};
matches.forEach((match) => {
const parts = match.split("=");
authParams[parts[0]] = parts[1].replace(/"/g, "");
});

return authParams;
}
};
5 changes: 2 additions & 3 deletions packages/hoppscotch-cli/src/utils/pre-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,14 +238,13 @@ export async function getEffectiveRESTRequest(
});
}
} else if (request.auth.authType === "digest") {
// TODO: Implement Digest Auth

const { method, endpoint } = request as HoppRESTRequest;

// Step 1: Fetch the initial auth info (nonce, realm, etc.)
const authInfo = await fetchInitialDigestAuthInfo(
parseTemplateString(endpoint, resolvedVariables),
method
method,
request.auth.disableRetry
);

// Step 2: Set up the parameters for the digest authentication header
Expand Down
1 change: 1 addition & 0 deletions packages/hoppscotch-cli/src/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ export const processRequest =

// Updating report for errors & current result
report.errors.push(preRequestRes.left);
console.error(`Report result is `, report.result);
report.result = report.result;
} else {
// Updating effective-request and consuming updated envs after pre-request script execution
Expand Down
17 changes: 14 additions & 3 deletions packages/hoppscotch-common/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,25 @@
"token": "Token",
"type": "Authorization Type",
"username": "Username",
"advance_config": "Advanced Configuration",
"advance_config_description": "Hoppscotch automatically assigns default values to certain fields if no explicit value is provided",
"aws_signature": {
"access_key": "Access Key",
"secret_key": "Secret Key",
"service_name": "Service Name",
"aws_region": "AWS Region",
"service_token": "Service Token",
"advance_config": "Advanced Configuration",
"advance_config_description": "Hoppscotch automatically assigns default values to certain fields if no explicit value is provided"
"service_token": "Service Token"
},
"digest": {
"initial_fetch_failed": "Fetching initial digest authorization information failed. Please check console more context.",
"realm": "Realm",
"nonce": "Nonce",
"algorithm": "Algorithm",
"qop": "qop",
"nonce_count": "Nonce Count",
"client_nonce": "Client Nonce",
"opaque": "Opaque",
"disable_retry": "Disable Retrying Request"
}
},
"collection": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@
<div class="flex flex-col divide-y divide-dividerLight">
<!-- label as advanced config here -->
<div class="p-4 flex flex-col space-y-1">
<label class="">
{{ t("authorization.aws_signature.advance_config") }}
<label>
{{ t("authorization.advance_config") }}
</label>
<p class="text-secondaryLight">
{{ t("authorization.aws_signature.advance_config_description") }}
{{ t("authorization.advance_config_description") }}
</p>
</div>
<div class="flex flex-1">
Expand Down
Loading