feat: Extract authentication when using the cURL importer (#423)

This commit is contained in:
Jeroen van der Merwe
2026-05-21 18:00:22 +02:00
committed by GitHub
parent 1307ea4e67
commit 463cc6f5a3
2 changed files with 229 additions and 6 deletions
+93 -6
View File
@@ -181,6 +181,78 @@ export function convertCurl(rawData: string) {
};
}
interface ExtractedAuthentication {
authenticationType: string | null;
authentication: Record<string, string>;
filteredHeaders: HttpUrlParameter[]; // headers without authorization
}
function extractAuthenticationFromHeaders(headers: HttpUrlParameter[]): ExtractedAuthentication {
const authorizationHeaderIndex = headers.findIndex(
(h) => h.name.toLowerCase() === "authorization",
);
const authorizationHeader = headers[authorizationHeaderIndex];
if (authorizationHeader == null) {
return {
authenticationType: null,
authentication: {},
filteredHeaders: headers,
};
}
const value = authorizationHeader.value.trim();
const spaceIndex = value.indexOf(" ");
if (spaceIndex <= 0) {
return {
authenticationType: null,
authentication: {},
filteredHeaders: headers,
};
}
const scheme = value.slice(0, spaceIndex).toLowerCase();
const credentials = value.slice(spaceIndex + 1).trim();
// Bearer authentication (RFC 6750)
if (scheme === "bearer") {
const filteredHeaders = headers.filter((_, i) => i !== authorizationHeaderIndex);
return {
authenticationType: "bearer",
authentication: { token: credentials, prefix: "Bearer" },
filteredHeaders,
};
}
// Basic authentication (RFC 7617)
if (scheme === "basic") {
try {
const decoded = Buffer.from(credentials, "base64").toString();
const colonIndex = decoded.indexOf(":");
if (colonIndex > 0) {
const filteredHeaders = headers.filter((_, i) => i !== authorizationHeaderIndex);
return {
authenticationType: "basic",
authentication: {
username: decoded.slice(0, colonIndex),
password: decoded.slice(colonIndex + 1),
},
filteredHeaders,
};
}
} catch {
// Invalid base64, keep header as-is
}
}
return {
authenticationType: null,
authentication: {},
filteredHeaders: headers,
};
}
function importCommand(parseEntries: string[], workspaceId: string) {
// ~~~~~~~~~~~~~~~~~~~~~ //
// Collect all the flags //
@@ -323,8 +395,23 @@ function importCommand(parseEntries: string[], workspaceId: string) {
});
}
// Extract authentication from Authorization headers (Bearer/Basic)
const {
authenticationType: extractedAuthenticationType,
authentication: extractedAuthentication,
filteredHeaders,
} = extractAuthenticationFromHeaders(headers);
// Use extracted authentication from header if found, otherwise fall back to -u/--user parsing
const finalAuthenticationType = extractedAuthenticationType || authenticationType;
const finalAuthentication = extractedAuthenticationType
? extractedAuthentication
: authentication;
// Body (Text or Blob)
const contentTypeHeader = headers.find((header) => header.name.toLowerCase() === "content-type");
const contentTypeHeader = filteredHeaders.find(
(header) => header.name.toLowerCase() === "content-type",
);
const mimeType = contentTypeHeader ? contentTypeHeader.value.split(";")[0]?.trim() : null;
// Extract boundary from Content-Type header for multipart parsing
@@ -398,7 +485,7 @@ function importCommand(parseEntries: string[], workspaceId: string) {
value: decodeURIComponent(parameter.value || ""),
})),
};
headers.push({
filteredHeaders.push({
name: "Content-Type",
value: "application/x-www-form-urlencoded",
enabled: true,
@@ -419,7 +506,7 @@ function importCommand(parseEntries: string[], workspaceId: string) {
form: formDataParams,
};
if (mimeType == null) {
headers.push({
filteredHeaders.push({
name: "Content-Type",
value: "multipart/form-data",
enabled: true,
@@ -442,9 +529,9 @@ function importCommand(parseEntries: string[], workspaceId: string) {
urlParameters,
url,
method,
headers,
authentication,
authenticationType,
headers: filteredHeaders,
authentication: finalAuthentication,
authenticationType: finalAuthenticationType,
body,
bodyType,
folderId: null,
+136
View File
@@ -332,6 +332,142 @@ describe("importer-curl", () => {
});
});
test("Imports Bearer token from Authorization header", () => {
expect(convertCurl('curl -H "Authorization: Bearer token123" https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: "https://yaak.app",
authenticationType: "bearer",
authentication: {
token: "token123",
prefix: "Bearer",
},
headers: [],
}),
],
},
});
});
test("Trims whitespace before Bearer token from Authorization header", () => {
expect(convertCurl('curl -H "Authorization: Bearer token123" https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: "https://yaak.app",
authenticationType: "bearer",
authentication: {
token: "token123",
prefix: "Bearer",
},
headers: [],
}),
],
},
});
});
test("Imports Basic auth from Authorization header (base64 decoded)", () => {
expect(
convertCurl('curl -H "Authorization: Basic dXNlcjpwYXNzd29yZA==" https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: "https://yaak.app",
authenticationType: "basic",
authentication: {
username: "user",
password: "password",
},
headers: [],
}),
],
},
});
});
test("Authorization header takes precedence over -u flag", () => {
expect(
convertCurl('curl -u admin:secret -H "Authorization: Bearer token123" https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: "https://yaak.app",
authenticationType: "bearer",
authentication: {
token: "token123",
prefix: "Bearer",
},
headers: [],
}),
],
},
});
});
test("Authorization header extraction is case-insensitive", () => {
expect(convertCurl('curl -H "authorization: bearer lowercaseToken" https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: "https://yaak.app",
authenticationType: "bearer",
authentication: {
token: "lowercaseToken",
prefix: "Bearer",
},
headers: [],
}),
],
},
});
});
test("Preserves other headers when extracting Authorization", () => {
expect(
convertCurl('curl -H "Authorization: Bearer token123" -H "X-Custom: value" https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: "https://yaak.app",
authenticationType: "bearer",
authentication: {
token: "token123",
prefix: "Bearer",
},
headers: [{ name: "X-Custom", value: "value", enabled: true }],
}),
],
},
});
});
test("Invalid base64 in Basic auth keeps header in headers", () => {
expect(
convertCurl('curl -H "Authorization: Basic not-valid-base64!!!" https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
baseRequest({
url: "https://yaak.app",
headers: [{ name: "Authorization", value: "Basic not-valid-base64!!!", enabled: true }],
}),
],
},
});
});
test("Imports cookie as header", () => {
expect(convertCurl('curl --cookie "foo=bar" https://yaak.app')).toEqual({
resources: {