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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions