Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion lib/swagger-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
SwaggerCustomOptions,
SwaggerDocumentOptions
} from './interfaces';
import {
SecuritySchemeObject
} from './interfaces/open-api-spec.interface';
import { MetadataLoader } from './plugin/metadata-loader';
import { SwaggerScanner } from './swagger-scanner';
import {
Expand Down Expand Up @@ -42,12 +45,16 @@ export class SwaggerModule {
document.components
);

return {
const finalDocument: OpenAPIObject = {
openapi: '3.0.0',
paths: {},
...config,
...document
};

this.filterSecuritySchemeHeaders(finalDocument);

return finalDocument;
}

public static async loadPluginMetadata(
Expand Down Expand Up @@ -362,4 +369,79 @@ export class SwaggerModule {
SwaggerModule.serveStatic(serveStaticSlashEndingPath, app);
}
}

/**
* Collects header names that are implicitly handled by security schemes.
* For example, bearer/basic auth uses the "Authorization" header,
* and apiKey schemes with `in: 'header'` use their configured `name`.
*/
private static getSecuritySchemeHeaderNames(
document: OpenAPIObject
): Set<string> {
const headerNames = new Set<string>();
const securitySchemes = document.components?.securitySchemes;
if (!securitySchemes) {
return headerNames;
}

for (const scheme of Object.values(securitySchemes)) {
if ('$ref' in scheme) {
continue;
}
const securityScheme = scheme as SecuritySchemeObject;
if (securityScheme.type === 'http') {
// HTTP auth schemes (bearer, basic, etc.) use the Authorization header
headerNames.add('authorization');
} else if (
securityScheme.type === 'apiKey' &&
securityScheme.in === 'header' &&
securityScheme.name
) {
headerNames.add(securityScheme.name.toLowerCase());
}
}
return headerNames;
}

/**
* Removes header parameters from operations that are already covered
* by security scheme definitions (e.g., "Authorization" for bearer auth).
*/
private static filterSecuritySchemeHeaders(document: OpenAPIObject): void {
const securityHeaders = this.getSecuritySchemeHeaderNames(document);
if (securityHeaders.size === 0) {
return;
}
Comment on lines +410 to +414
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filterSecuritySchemeHeaders() filters header parameters whenever any security scheme exists in components.securitySchemes, even if that scheme is never applied via document.security or operation.security. This can incorrectly remove legitimate @Headers('Authorization') / apiKey header parameters on operations that are not secured (or when the project only defines schemes to show the Authorize dialog but doesn’t add security requirements). Consider filtering per-operation only when the effective security requirements (operation.security if defined, else document.security) are non-empty, and only for the scheme names referenced there (respecting operation.security = [] overriding global security).

Copilot uses AI. Check for mistakes.

const paths = document.paths;
if (!paths) {
return;
}

for (const pathItem of Object.values(paths)) {
for (const method of [
'get',
'put',
'post',
'delete',
'options',
'head',
'patch',
'trace'
]) {
const operation = pathItem[method];
if (!operation?.parameters) {
continue;
}
operation.parameters = operation.parameters.filter(
(param: any) =>
!(
param.in === 'header' &&
typeof param.name === 'string' &&
securityHeaders.has(param.name.toLowerCase())
)
);
}
}
}
}
142 changes: 142 additions & 0 deletions test/swagger-module-security-headers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { Controller, Get, Headers } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '../lib';

describe('SwaggerModule - Security scheme header filtering', () => {
@Controller('test')
class TestController {
@Get('bearer')
getWithAuthHeader(@Headers('Authorization') auth: string) {
return auth;
}

@Get('custom-header')
getWithCustomHeader(@Headers('X-Custom') custom: string) {
return custom;
}
}

let app;

beforeAll(async () => {
app = await NestFactory.create(
{
module: class {},
controllers: [TestController]
},
{ logger: false }
);
});

afterAll(async () => {
await app.close();
});

it('should filter out Authorization header parameter when bearer auth is configured', () => {
const config = new DocumentBuilder()
.setTitle('Test')
.setVersion('1.0')
.addBearerAuth()
.addSecurityRequirements('bearer')
.build();

const document = SwaggerModule.createDocument(app, config);
const params = document.paths['/test/bearer']?.get?.parameters || [];

// The Authorization header should NOT appear as a parameter
const authParam = params.find(
(p: any) => p.in === 'header' && p.name === 'Authorization'
);
expect(authParam).toBeUndefined();
});

it('should filter out Authorization header parameter when basic auth is configured', () => {
const config = new DocumentBuilder()
.setTitle('Test')
.setVersion('1.0')
.addBasicAuth()
.addSecurityRequirements('basic')
.build();

const document = SwaggerModule.createDocument(app, config);
const params = document.paths['/test/bearer']?.get?.parameters || [];

const authParam = params.find(
(p: any) => p.in === 'header' && p.name === 'Authorization'
);
expect(authParam).toBeUndefined();
});

it('should filter out custom API key header parameter when apiKey scheme uses it', () => {
const config = new DocumentBuilder()
.setTitle('Test')
.setVersion('1.0')
.addApiKey({ type: 'apiKey', in: 'header', name: 'X-Custom' }, 'custom')
.addSecurityRequirements('custom')
.build();

const document = SwaggerModule.createDocument(app, config);
const params =
document.paths['/test/custom-header']?.get?.parameters || [];

const customParam = params.find(
(p: any) => p.in === 'header' && p.name === 'X-Custom'
);
expect(customParam).toBeUndefined();
});

it('should NOT filter out non-security header parameters', () => {
const config = new DocumentBuilder()
.setTitle('Test')
.setVersion('1.0')
.addBearerAuth()
.addSecurityRequirements('bearer')
.build();

const document = SwaggerModule.createDocument(app, config);
const params =
document.paths['/test/custom-header']?.get?.parameters || [];

// X-Custom header should still appear since it's not a security header
const customParam = params.find(
(p: any) => p.in === 'header' && p.name === 'X-Custom'
);
expect(customParam).toBeDefined();
});

it('should not filter any headers when no security schemes are configured', () => {
const config = new DocumentBuilder()
.setTitle('Test')
.setVersion('1.0')
.build();

const document = SwaggerModule.createDocument(app, config);
const params = document.paths['/test/bearer']?.get?.parameters || [];

// Authorization should still appear since there are no security schemes
const authParam = params.find(
(p: any) => p.in === 'header' && p.name === 'Authorization'
);
expect(authParam).toBeDefined();
});

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage is missing for the case where security schemes are defined (e.g. via addBearerAuth() / addApiKey()) but no security requirements are set (document.security undefined/empty and no operation.security). With the current implementation, those headers would still be filtered, so adding a regression test for this scenario would both document and prevent the unintended behavior.

Suggested change
it('should not filter security headers when schemes are defined without requirements', () => {
const config = new DocumentBuilder()
.setTitle('Test')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
const params = document.paths['/test/bearer']?.get?.parameters || [];
// Authorization should still appear since bearer auth is defined
// but no document-level or operation-level security requirement uses it
const authParam = params.find(
(p: any) => p.in === 'header' && p.name === 'Authorization'
);
expect(authParam).toBeDefined();
});

Copilot uses AI. Check for mistakes.
it('should handle case-insensitive header name matching', () => {
const config = new DocumentBuilder()
.setTitle('Test')
.setVersion('1.0')
.addBearerAuth()
.addSecurityRequirements('bearer')
.build();

const document = SwaggerModule.createDocument(app, config);
const bearerParams = document.paths['/test/bearer']?.get?.parameters || [];

// Even though @Headers('Authorization') uses title case,
// it should be filtered since bearer auth uses the Authorization header
const authParam = bearerParams.find(
(p: any) =>
p.in === 'header' && p.name.toLowerCase() === 'authorization'
);
expect(authParam).toBeUndefined();
});
});