Skip to content

[NestJS] Support for @ApiBearerAuth and per-operation security in NestJS mode #94

@hyeonss0417

Description

@hyeonss0417

Summary

When using tspec with NestJS mode (nestjs: true), the securityDefinitions config correctly generates components.securitySchemes in the OpenAPI spec, but there's no way to apply security requirements to individual API operations.

Current Behavior

1. securityDefinitions works correctly ✅

tspec.config.json:

{
  "specPathGlobs": ["src/**/*.controller.ts"],
  "nestjs": true,
  "openapi": {
    "securityDefinitions": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  }
}

Generated OpenAPI spec:

{
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  }
}

This works as expected! 🎉

2. No way to apply security to operations ❌

Problem with @ApiBearerAuth

In NestJS, we typically use @ApiBearerAuth() decorator to mark endpoints that require authentication:

import { ApiBearerAuth } from "@nestjs/swagger";

@Controller("users")
export class UserController {
  @Get("me")
  @ApiBearerAuth("bearerAuth")  // This is ignored by tspec
  async getCurrentUser(): Promise<User> {
    // ...
  }
}

However, tspec's NestJS integration doesn't recognize @ApiBearerAuth decorator.

Problem with Custom Composite Decorators

We use a custom @Auth() decorator that combines guard and swagger decorator:

import { applyDecorators, UseGuards } from "@nestjs/common";
import { ApiBearerAuth } from "@nestjs/swagger";
import { SessionAuthGuard } from "../guards/session-auth.guard";

/**
 * 인증이 필요한 엔드포인트에 적용하는 컴포지트 데코레이터.
 * SessionAuthGuard와 ApiBearerAuth를 동시에 적용합니다.
 */
export function Auth(): MethodDecorator & ClassDecorator {
  return applyDecorators(
    UseGuards(SessionAuthGuard),
    ApiBearerAuth("access-token"),
  );
}

Usage:

@Controller("users")
export class UserController {
  @Get("me")
  @Auth()  // Custom decorator - not recognized by tspec
  async getCurrentUser(): Promise<User> {
    // ...
  }
}

Neither the direct @ApiBearerAuth nor the custom @Auth() decorator is recognized by tspec.

Expected:

{
  "paths": {
    "/users/me": {
      "get": {
        "security": [{ "bearerAuth": [] }]
      }
    }
  }
}

Actual:

{
  "paths": {
    "/users/me": {
      "get": {
        // No security field - lock icon doesn't appear in Swagger UI
      }
    }
  }
}

Documentation Reference

The NestJS Integration Guide mentions in the Limitations section:

"Interceptors/Guards: These are not reflected in the generated spec"

However, @ApiBearerAuth is a Swagger decorator (not a Guard), and it's listed under "Swagger Decorators" section but not actually supported.

Feature Request

Please consider adding support for one or more of the following:

Option 1: Support @ApiBearerAuth decorator

@Get("me")
@ApiBearerAuth("bearerAuth")
async getCurrentUser(): Promise<User> { }

Option 2: Config option for custom auth decorator names

Allow users to specify custom decorator names that should be treated as security decorators:

{
  "openapi": {
    "securityDefinitions": {
      "bearerAuth": { "type": "http", "scheme": "bearer" }
    },
    "authDecorators": {
      "Auth": "bearerAuth",
      "AdminAuth": "bearerAuth"
    }
  }
}

Option 3: Custom decorator handler function

Allow users to provide a function that determines security for each operation:

const spec = await generateTspec({
  nestjs: true,
  resolveOperationSecurity: (controllerClass, methodName, decorators) => {
    if (decorators.includes("Auth") || decorators.includes("ApiBearerAuth")) {
      return [{ bearerAuth: [] }];
    }
    return undefined;
  }
});

Option 4: Global security with exclusions

Apply security to all operations by default, with ability to exclude specific ones:

{
  "openapi": {
    "securityDefinitions": {
      "bearerAuth": { "type": "http", "scheme": "bearer" }
    },
    "security": [{ "bearerAuth": [] }],
    "publicTags": ["Health", "Auth"]  // Exclude these tags from security
  }
}

Current Workaround

We're manually adding security to operations at runtime:

const tspecDocument = await generateTspec();

// Add security to non-public endpoints
for (const path of Object.values(tspecDocument.paths)) {
  for (const method of Object.values(path)) {
    const operation = method as { tags?: string[]; security?: object[] };
    const isPublic = operation.tags?.some(tag => ["Health", "Auth"].includes(tag));
    if (!isPublic) {
      operation.security = [{ "bearerAuth": [] }];
    }
  }
}

This works but is not ideal.

Environment

  • tspec version: 0.2.10
  • Node.js version: 22.x
  • NestJS version: 10.x

Thank you for the great library! 🙏

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions