diff --git a/.changeset/sharp-buses-wash.md b/.changeset/sharp-buses-wash.md new file mode 100644 index 000000000..5ae5bf1c9 --- /dev/null +++ b/.changeset/sharp-buses-wash.md @@ -0,0 +1,5 @@ +--- +"@gitbook/react-openapi": minor +--- + +Fix OpenAPISecurities and code sample not using operation security requirements diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index cc21da1f3..6858ef4d4 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -11,7 +11,7 @@ import { generateMediaTypeExamples, generateSchemaExample } from './generateSche import { stringifyOpenAPI } from './stringifyOpenAPI'; import type { OpenAPIOperationData } from './types'; import { getDefaultServerURL } from './util/server'; -import { checkIsReference } from './utils'; +import { checkIsReference, extractOperationSecurityInfo } from './utils'; const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const; @@ -106,7 +106,10 @@ function generateCodeSamples(props: { (searchParams.size ? `?${searchParams.toString()}` : ''); const genericHeaders = { - ...getSecurityHeaders(data.securities), + ...getSecurityHeaders({ + securityRequirement: data.operation.security, + securities: data.securities, + }), ...headersObject, }; @@ -278,51 +281,66 @@ function getCustomCodeSamples(props: { return customCodeSamples; } -function getSecurityHeaders(securities: OpenAPIOperationData['securities']): { +function getSecurityHeaders(args: { + securityRequirement: OpenAPIV3.OperationObject['security']; + securities: OpenAPIOperationData['securities']; +}): { [key: string]: string; } { - const security = securities[0]; + const { securityRequirement, securities } = args; + const operationSecurityInfo = extractOperationSecurityInfo({ securityRequirement, securities }); - if (!security) { + if (operationSecurityInfo.length === 0) { return {}; } - switch (security[1].type) { - case 'http': { - let scheme = security[1].scheme; - let format = security[1].bearerFormat ?? 'YOUR_SECRET_TOKEN'; + const selectedSecurity = operationSecurityInfo.at(0); - if (scheme?.includes('bearer')) { - scheme = 'Bearer'; - } else if (scheme?.includes('basic')) { - scheme = 'Basic'; - format = 'username:password'; - } else if (scheme?.includes('token')) { - scheme = 'Token'; + if (!selectedSecurity) { + return {}; + } + + const headers: { [key: string]: string } = {}; + + for (const security of selectedSecurity.schemes) { + switch (security.type) { + case 'http': { + let scheme = security.scheme; + let format = security.bearerFormat ?? 'YOUR_SECRET_TOKEN'; + + if (scheme?.includes('bearer')) { + scheme = 'Bearer'; + } else if (scheme?.includes('basic')) { + scheme = 'Basic'; + format = 'username:password'; + } else if (scheme?.includes('token')) { + scheme = 'Token'; + } + + headers.Authorization = `${scheme} ${format}`; + break; } + case 'apiKey': { + if (security.in !== 'header') { + break; + } - return { - Authorization: `${scheme} ${format}`, - }; - } - case 'apiKey': { - if (security[1].in !== 'header') return {}; + const name = security.name ?? 'Authorization'; + headers[name] = 'YOUR_API_KEY'; - const name = security[1].name ?? 'Authorization'; - - return { - [name]: 'YOUR_API_KEY', - }; - } - case 'oauth2': { - return { - Authorization: 'Bearer YOUR_OAUTH2_TOKEN', - }; - } - default: { - return {}; + break; + } + case 'oauth2': { + headers.Authorization = 'Bearer YOUR_OAUTH2_TOKEN'; + break; + } + default: { + break; + } } } + + return headers; } function validateHttpMethod(method: string): method is OpenAPIV3.HttpMethods { diff --git a/packages/react-openapi/src/OpenAPISecurities.tsx b/packages/react-openapi/src/OpenAPISecurities.tsx index 8b007ff3c..69aca699e 100644 --- a/packages/react-openapi/src/OpenAPISecurities.tsx +++ b/packages/react-openapi/src/OpenAPISecurities.tsx @@ -6,21 +6,24 @@ import { OpenAPISchemaName } from './OpenAPISchemaName'; import type { OpenAPIClientContext } from './context'; import { t } from './translate'; import type { OpenAPIOperationData, OpenAPISecurityWithRequired } from './types'; -import { createStateKey, resolveDescription } from './utils'; +import { createStateKey, extractOperationSecurityInfo, resolveDescription } from './utils'; /** * Present securities authorization that can be used for this operation. */ export function OpenAPISecurities(props: { + securityRequirement: OpenAPIV3.OperationObject['security']; securities: OpenAPIOperationData['securities']; context: OpenAPIClientContext; }) { - const { securities, context } = props; + const { securityRequirement, securities, context } = props; - if (securities.length === 0) { + if (!securities || securities.length === 0) { return null; } + const tabsData = extractOperationSecurityInfo({ securityRequirement, securities }); + return ( { - const description = resolveDescription(security); - return { - key: key, - label: key, - body: ( -
-
- {getLabelForType(security, context)} - - {description ? ( - - ) : null} -
-
- ), - }; - })} + tabs={tabsData.map(({ key, label, schemes }) => ({ + key, + label, + body: ( +
+ {schemes.map((security, index) => { + const description = resolveDescription(security); + return ( +
+ {getLabelForType(security, context)} + {description ? ( + + ) : null} +
+ ); + })} +
+ ), + }))} /> ); } diff --git a/packages/react-openapi/src/OpenAPISpec.tsx b/packages/react-openapi/src/OpenAPISpec.tsx index 1144c83ba..0facdd5ea 100644 --- a/packages/react-openapi/src/OpenAPISpec.tsx +++ b/packages/react-openapi/src/OpenAPISpec.tsx @@ -26,7 +26,12 @@ export function OpenAPISpec(props: { return ( <> {securities.length > 0 ? ( - + ) : null} {parameterGroups.map((group) => { diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index 55539a07b..6ad2128eb 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -2,6 +2,7 @@ import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser' import type { OpenAPIUniversalContext } from './context'; import { stringifyOpenAPI } from './stringifyOpenAPI'; import { tString } from './translate'; +import type { OpenAPIOperationData, OpenAPISecurityWithRequired } from './types'; export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject { return typeof input === 'object' && !!input && '$ref' in input; @@ -253,3 +254,41 @@ export function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string { return type; } + +export type OperationSecurityInfo = { + key: string; + label: string; + schemes: OpenAPISecurityWithRequired[]; +}; + +/** + * Extract security information for an operation based on its security requirements and the spec security schemes. + */ +export function extractOperationSecurityInfo(args: { + securityRequirement: OpenAPIV3.OperationObject['security']; + securities: OpenAPIOperationData['securities']; +}): OperationSecurityInfo[] { + const { securityRequirement, securities } = args; + const securitiesMap = new Map(securities); + + // When no security requirement include every schemes + if (!securityRequirement || securityRequirement.length === 0) { + return securities.map(([key, security]) => ({ + key, + label: key, + schemes: [security], + })); + } + + return securityRequirement.map((requirement, idx) => { + const schemeKeys = Object.keys(requirement); + + return { + key: `security-${idx}`, + label: schemeKeys.join(' & '), + schemes: schemeKeys + .map((schemeKey) => securitiesMap.get(schemeKey)) + .filter((s) => s !== undefined), + }; + }); +}