Skip to content

Commit 529f940

Browse files
authored
Fix OpenAPISecurities and code sample not using operation security requirements (#3671)
1 parent 8a3c159 commit 529f940

File tree

5 files changed

+135
-61
lines changed

5 files changed

+135
-61
lines changed

.changeset/sharp-buses-wash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@gitbook/react-openapi": minor
3+
---
4+
5+
Fix OpenAPISecurities and code sample not using operation security requirements

packages/react-openapi/src/OpenAPICodeSample.tsx

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { generateMediaTypeExamples, generateSchemaExample } from './generateSche
1111
import { stringifyOpenAPI } from './stringifyOpenAPI';
1212
import type { OpenAPIOperationData } from './types';
1313
import { getDefaultServerURL } from './util/server';
14-
import { checkIsReference } from './utils';
14+
import { checkIsReference, extractOperationSecurityInfo } from './utils';
1515

1616
const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const;
1717

@@ -106,7 +106,10 @@ function generateCodeSamples(props: {
106106
(searchParams.size ? `?${searchParams.toString()}` : '');
107107

108108
const genericHeaders = {
109-
...getSecurityHeaders(data.securities),
109+
...getSecurityHeaders({
110+
securityRequirement: data.operation.security,
111+
securities: data.securities,
112+
}),
110113
...headersObject,
111114
};
112115

@@ -278,51 +281,66 @@ function getCustomCodeSamples(props: {
278281
return customCodeSamples;
279282
}
280283

281-
function getSecurityHeaders(securities: OpenAPIOperationData['securities']): {
284+
function getSecurityHeaders(args: {
285+
securityRequirement: OpenAPIV3.OperationObject['security'];
286+
securities: OpenAPIOperationData['securities'];
287+
}): {
282288
[key: string]: string;
283289
} {
284-
const security = securities[0];
290+
const { securityRequirement, securities } = args;
291+
const operationSecurityInfo = extractOperationSecurityInfo({ securityRequirement, securities });
285292

286-
if (!security) {
293+
if (operationSecurityInfo.length === 0) {
287294
return {};
288295
}
289296

290-
switch (security[1].type) {
291-
case 'http': {
292-
let scheme = security[1].scheme;
293-
let format = security[1].bearerFormat ?? 'YOUR_SECRET_TOKEN';
294-
295-
if (scheme?.includes('bearer')) {
296-
scheme = 'Bearer';
297-
} else if (scheme?.includes('basic')) {
298-
scheme = 'Basic';
299-
format = 'username:password';
300-
} else if (scheme?.includes('token')) {
301-
scheme = 'Token';
302-
}
297+
const selectedSecurity = operationSecurityInfo.at(0);
303298

304-
return {
305-
Authorization: `${scheme} ${format}`,
306-
};
307-
}
308-
case 'apiKey': {
309-
if (security[1].in !== 'header') return {};
299+
if (!selectedSecurity) {
300+
return {};
301+
}
310302

311-
const name = security[1].name ?? 'Authorization';
303+
const headers: { [key: string]: string } = {};
304+
305+
for (const security of selectedSecurity.schemes) {
306+
switch (security.type) {
307+
case 'http': {
308+
let scheme = security.scheme;
309+
let format = security.bearerFormat ?? 'YOUR_SECRET_TOKEN';
310+
311+
if (scheme?.includes('bearer')) {
312+
scheme = 'Bearer';
313+
} else if (scheme?.includes('basic')) {
314+
scheme = 'Basic';
315+
format = 'username:password';
316+
} else if (scheme?.includes('token')) {
317+
scheme = 'Token';
318+
}
319+
320+
headers.Authorization = `${scheme} ${format}`;
321+
break;
322+
}
323+
case 'apiKey': {
324+
if (security.in !== 'header') {
325+
break;
326+
}
312327

313-
return {
314-
[name]: 'YOUR_API_KEY',
315-
};
316-
}
317-
case 'oauth2': {
318-
return {
319-
Authorization: 'Bearer YOUR_OAUTH2_TOKEN',
320-
};
321-
}
322-
default: {
323-
return {};
328+
const name = security.name ?? 'Authorization';
329+
headers[name] = 'YOUR_API_KEY';
330+
331+
break;
332+
}
333+
case 'oauth2': {
334+
headers.Authorization = 'Bearer YOUR_OAUTH2_TOKEN';
335+
break;
336+
}
337+
default: {
338+
break;
339+
}
324340
}
325341
}
342+
343+
return headers;
326344
}
327345

328346
function validateHttpMethod(method: string): method is OpenAPIV3.HttpMethods {

packages/react-openapi/src/OpenAPISecurities.tsx

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,24 @@ import { OpenAPISchemaName } from './OpenAPISchemaName';
66
import type { OpenAPIClientContext } from './context';
77
import { t } from './translate';
88
import type { OpenAPIOperationData, OpenAPISecurityWithRequired } from './types';
9-
import { createStateKey, resolveDescription } from './utils';
9+
import { createStateKey, extractOperationSecurityInfo, resolveDescription } from './utils';
1010

1111
/**
1212
* Present securities authorization that can be used for this operation.
1313
*/
1414
export function OpenAPISecurities(props: {
15+
securityRequirement: OpenAPIV3.OperationObject['security'];
1516
securities: OpenAPIOperationData['securities'];
1617
context: OpenAPIClientContext;
1718
}) {
18-
const { securities, context } = props;
19+
const { securityRequirement, securities, context } = props;
1920

20-
if (securities.length === 0) {
21+
if (!securities || securities.length === 0) {
2122
return null;
2223
}
2324

25+
const tabsData = extractOperationSecurityInfo({ securityRequirement, securities });
26+
2427
return (
2528
<InteractiveSection
2629
header={t(context.translation, 'authorizations')}
@@ -30,27 +33,31 @@ export function OpenAPISecurities(props: {
3033
toggleIcon={context.icons.chevronRight}
3134
selectIcon={context.icons.chevronDown}
3235
className="openapi-securities"
33-
tabs={securities.map(([key, security]) => {
34-
const description = resolveDescription(security);
35-
return {
36-
key: key,
37-
label: key,
38-
body: (
39-
<div className="openapi-schema">
40-
<div className="openapi-schema-presentation">
41-
{getLabelForType(security, context)}
42-
43-
{description ? (
44-
<Markdown
45-
source={description}
46-
className="openapi-securities-description"
47-
/>
48-
) : null}
49-
</div>
50-
</div>
51-
),
52-
};
53-
})}
36+
tabs={tabsData.map(({ key, label, schemes }) => ({
37+
key,
38+
label,
39+
body: (
40+
<div className="openapi-schema">
41+
{schemes.map((security, index) => {
42+
const description = resolveDescription(security);
43+
return (
44+
<div
45+
key={`${key}-${index}`}
46+
className="openapi-schema-presentation"
47+
>
48+
{getLabelForType(security, context)}
49+
{description ? (
50+
<Markdown
51+
source={description}
52+
className="openapi-securities-description"
53+
/>
54+
) : null}
55+
</div>
56+
);
57+
})}
58+
</div>
59+
),
60+
}))}
5461
/>
5562
);
5663
}

packages/react-openapi/src/OpenAPISpec.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ export function OpenAPISpec(props: {
2626
return (
2727
<>
2828
{securities.length > 0 ? (
29-
<OpenAPISecurities key="securities" securities={securities} context={context} />
29+
<OpenAPISecurities
30+
key="securities"
31+
securityRequirement={operation.security}
32+
securities={securities}
33+
context={context}
34+
/>
3035
) : null}
3136

3237
{parameterGroups.map((group) => {

packages/react-openapi/src/utils.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser'
22
import type { OpenAPIUniversalContext } from './context';
33
import { stringifyOpenAPI } from './stringifyOpenAPI';
44
import { tString } from './translate';
5+
import type { OpenAPIOperationData, OpenAPISecurityWithRequired } from './types';
56

67
export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject {
78
return typeof input === 'object' && !!input && '$ref' in input;
@@ -253,3 +254,41 @@ export function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
253254

254255
return type;
255256
}
257+
258+
export type OperationSecurityInfo = {
259+
key: string;
260+
label: string;
261+
schemes: OpenAPISecurityWithRequired[];
262+
};
263+
264+
/**
265+
* Extract security information for an operation based on its security requirements and the spec security schemes.
266+
*/
267+
export function extractOperationSecurityInfo(args: {
268+
securityRequirement: OpenAPIV3.OperationObject['security'];
269+
securities: OpenAPIOperationData['securities'];
270+
}): OperationSecurityInfo[] {
271+
const { securityRequirement, securities } = args;
272+
const securitiesMap = new Map(securities);
273+
274+
// When no security requirement include every schemes
275+
if (!securityRequirement || securityRequirement.length === 0) {
276+
return securities.map(([key, security]) => ({
277+
key,
278+
label: key,
279+
schemes: [security],
280+
}));
281+
}
282+
283+
return securityRequirement.map((requirement, idx) => {
284+
const schemeKeys = Object.keys(requirement);
285+
286+
return {
287+
key: `security-${idx}`,
288+
label: schemeKeys.join(' & '),
289+
schemes: schemeKeys
290+
.map((schemeKey) => securitiesMap.get(schemeKey))
291+
.filter((s) => s !== undefined),
292+
};
293+
});
294+
}

0 commit comments

Comments
 (0)