History

Detect query-located API keys; drop unrenderable auth strategies

An apiKey with in=query (Viator's legacy ?apiKey=) reached the add flow as a method with a '(query)' label and zero placements — an empty, unfillable card that would register as a no-auth method. Query keys now become real query placements end to end (the invoke layer always knew how to render them). Cookie-located schemes stop producing methods entirely: placements carry header|query only, and a cookie scheme is usually the vendor console's own session, not a mintable credential.

Rhys Sullivan committed 2 months ago

Commit bcaf9e11 parent 12ae0913 files · +83 −21Browse files

Unified · Split

3 files
  1. packages/plugins/openapi/src/sdk/derive-auth.test.ts +42 −0
  2. packages/plugins/openapi/src/sdk/derive-auth.ts +24 −11
  3. packages/plugins/openapi/src/sdk/preview.ts +17 −10
packages/plugins/openapi/src/sdk/derive-auth.test.ts+42−0
@@ -71,3 +71,45 @@ describe("detectedAuthenticationTemplates", () => { · 70 unmodified lines
71 ).toEqual(["OAuth2 Authorization Code · oauth_app", "OAuth2 Client Credentials · oauth_app"]);71 ).toEqual(["OAuth2 Authorization Code · oauth_app", "OAuth2 Client Credentials · oauth_app"]);
72 });72 });
73});73});
74
75describe("query-located api keys", () => {
76 it("a query apiKey preset places the secret in the query string", () => {
77 const templates = detectedAuthenticationTemplates(
78 [
79 {
80 label: "Legacy-API-key (query)",
81 headers: {},
82 secretHeaders: [],
83 secretQueryParams: ["apiKey"],
84 },
85 ],
86 [],
87 "https://example.com",
88 );
89 expect(templates).toHaveLength(1);
90 const template = templates[0];
91 expect(template?.kind).toBe("apikey");
92 expect(template?.kind === "apikey" ? template.placements : []).toEqual([
93 { carrier: "query", name: "apiKey" },
94 ]);
95 });
96
97 it("a strategy mixing a header and a query param asks for two inputs", () => {
98 const templates = detectedAuthenticationTemplates(
99 [
100 {
101 label: "exp-api-key + apiKey (query)",
102 headers: { "exp-api-key": null },
103 secretHeaders: ["exp-api-key"],
104 secretQueryParams: ["apiKey"],
105 },
106 ],
107 [],
108 "https://example.com",
109 );
110 const template = templates[0];
111 const placements = template?.kind === "apikey" ? template.placements : [];
112 expect(placements.map((placement) => placement.carrier)).toEqual(["header", "query"]);
113 expect(new Set(placements.map((placement) => placement.variable)).size).toBe(2);
114 });
115});
packages/plugins/openapi/src/sdk/derive-auth.ts+24−11
@@ -104,20 +104,33 @@ const apiKeyTemplateFromHeaderPreset = ( · 103 unmodified lines
104 preset: HeaderPreset,104 preset: HeaderPreset,
105 slug: AuthTemplateSlug,105 slug: AuthTemplateSlug,
106): APIKeyAuthentication => {106): APIKeyAuthentication => {
107 const variables = variablesForHeaders(preset.secretHeaders);107 const secretQueryParams = preset.secretQueryParams ?? [];
108 // One variable namespace across carriers, so a strategy sending two secrets
109 // (a header and a query param) asks for two distinct inputs.
110 const variables = variablesForHeaders([...preset.secretHeaders, ...secretQueryParams]);
108 return {111 return {
109 slug,112 slug,
110 kind: "apikey",113 kind: "apikey",
111 placements: preset.secretHeaders.map((headerName) => {114 placements: [
112 const prefix = headerPrefix(preset, headerName);115 ...preset.secretHeaders.map((headerName) => {
113 const variable = variables.get(headerName);116 const prefix = headerPrefix(preset, headerName);
114 return {117 const variable = variables.get(headerName);
115 carrier: "header" as const,118 return {
116 name: headerName,119 carrier: "header" as const,
117 ...(prefix ? { prefix } : {}),120 name: headerName,
118 ...(variable ? { variable } : {}),121 ...(prefix ? { prefix } : {}),
119 };122 ...(variable ? { variable } : {}),
120 }),123 };
124 }),
125 ...secretQueryParams.map((paramName) => {
126 const variable = variables.get(paramName);
127 return {
128 carrier: "query" as const,
129 name: paramName,
130 ...(variable ? { variable } : {}),
131 };
132 }),
133 ],
121 };134 };
122};135};
123 136
packages/plugins/openapi/src/sdk/preview.ts+17−10
@@ -122,6 +122,9 @@ export const HeaderPreset = Schema.Struct({ · 121 unmodified lines
122 headers: Schema.Record(Schema.String, Schema.NullOr(Schema.String)),122 headers: Schema.Record(Schema.String, Schema.NullOr(Schema.String)),
123 /** Which headers should be stored as secrets */123 /** Which headers should be stored as secrets */
124 secretHeaders: Schema.Array(Schema.String),124 secretHeaders: Schema.Array(Schema.String),
125 /** Query parameters the strategy sends the secret in (apiKey in=query,
126 * e.g. Viator's legacy `?apiKey=`). Absent on older stored previews. */
127 secretQueryParams: Schema.optional(Schema.Array(Schema.String)),
125});128});
126export type HeaderPreset = typeof HeaderPreset.Type;129export type HeaderPreset = typeof HeaderPreset.Type;
127 130
@@ -347,6 +350,7 @@ const buildHeaderPresets = ( · 219 unmodified lines
347 350
348 const headers: Record<string, string | null> = {};351 const headers: Record<string, string | null> = {};
349 const secretHeaders: string[] = [];352 const secretHeaders: string[] = [];
353 const secretQueryParams: string[] = [];
350 const labelParts: string[] = [];354 const labelParts: string[] = [];
351 355
352 for (const scheme of resolved) {356 for (const scheme of resolved) {
@@ -363,8 +367,16 @@ const buildHeaderPresets = ( · 10 unmodified lines
363 headers[headerName] = null;367 headers[headerName] = null;
364 secretHeaders.push(headerName);368 secretHeaders.push(headerName);
365 labelParts.push(scheme.name);369 labelParts.push(scheme.name);
370 } else if (scheme.type === "apiKey" && Option.getOrElse(scheme.in, () => "") === "query") {
371 secretQueryParams.push(Option.getOrElse(scheme.headerName, () => scheme.name));
372 labelParts.push(`${scheme.name} (query)`);
366 } else if (scheme.type === "apiKey") {373 } else if (scheme.type === "apiKey") {
367 labelParts.push(`${scheme.name} (${Option.getOrElse(scheme.in, () => "unknown")})`);374 // Cookie (and unknown) locations are not renderable as a stored
375 // method — auth placements carry header|query — and a cookie scheme
376 // is usually the vendor console's own session, not a mintable
377 // credential. Contributing a label here used to produce a method
378 // with zero placements: an empty, unfillable card in the add flow.
379 continue;
368 } else if (scheme.type === "oauth2" || scheme.type === "openIdConnect") {380 } else if (scheme.type === "oauth2" || scheme.type === "openIdConnect") {
369 return [];381 return [];
370 } else {382 } else {
@@ -372,21 +384,16 @@ const buildHeaderPresets = ( · 1 unmodified line
372 }384 }
373 }385 }
374 386
375 if (Object.keys(headers).length === 0 && resolved.length > 0) {387 // A strategy in which nothing is renderable (cookie-only, or an exotic
376 return [388 // scheme type) yields no preset rather than an empty one.
377 HeaderPreset.make({389 if (secretHeaders.length === 0 && secretQueryParams.length === 0) return [];
378 label: labelParts.join(" + "),
379 headers: {},
380 secretHeaders: [],
381 }),
382 ];
383 }
384 390
385 return [391 return [
386 HeaderPreset.make({392 HeaderPreset.make({
387 label: labelParts.join(" + "),393 label: labelParts.join(" + "),
388 headers,394 headers,
389 secretHeaders,395 secretHeaders,
396 ...(secretQueryParams.length > 0 ? { secretQueryParams } : {}),
390 }),397 }),
391 ];398 ];
392 });399 });

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t