Fix OpenAPISecurities and code sample not using operation security requirements (#3671)

This commit is contained in:
spastorelli
2025-09-22 10:00:46 +02:00
committed by GitHub
parent 8a3c15911f
commit 529f940a45
5 changed files with 134 additions and 60 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/react-openapi": minor
---
Fix OpenAPISecurities and code sample not using operation security requirements
@@ -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 {
@@ -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 (
<InteractiveSection
header={t(context.translation, 'authorizations')}
@@ -30,27 +33,31 @@ export function OpenAPISecurities(props: {
toggleIcon={context.icons.chevronRight}
selectIcon={context.icons.chevronDown}
className="openapi-securities"
tabs={securities.map(([key, security]) => {
const description = resolveDescription(security);
return {
key: key,
label: key,
body: (
<div className="openapi-schema">
<div className="openapi-schema-presentation">
{getLabelForType(security, context)}
{description ? (
<Markdown
source={description}
className="openapi-securities-description"
/>
) : null}
</div>
</div>
),
};
})}
tabs={tabsData.map(({ key, label, schemes }) => ({
key,
label,
body: (
<div className="openapi-schema">
{schemes.map((security, index) => {
const description = resolveDescription(security);
return (
<div
key={`${key}-${index}`}
className="openapi-schema-presentation"
>
{getLabelForType(security, context)}
{description ? (
<Markdown
source={description}
className="openapi-securities-description"
/>
) : null}
</div>
);
})}
</div>
),
}))}
/>
);
}
+6 -1
View File
@@ -26,7 +26,12 @@ export function OpenAPISpec(props: {
return (
<>
{securities.length > 0 ? (
<OpenAPISecurities key="securities" securities={securities} context={context} />
<OpenAPISecurities
key="securities"
securityRequirement={operation.security}
securities={securities}
context={context}
/>
) : null}
{parameterGroups.map((group) => {
+39
View File
@@ -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),
};
});
}