Repository: RobinTail/express-zod-api
Pull request #523 (2022-07-25 10:03:02)

Security schemas for the Middlewares
Related to the discussion #521 

ℹ️  Scopes will be added later.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e4d2d47..487b1c9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,64 @@
 
 ## Version 7
 
+### v7.7.0
+
+- Feature #523: Ability to specify Security schemas of your Middlewares and depict the Authentication of your API.
+  - OpenAPI generator now can depict the [Authentication](https://swagger.io/docs/specification/authentication/) of your
+    endpoints as a part of the generated documentation.
+  - There is a new optional property `security` of `createMiddleware()`.
+  - You can specify a single or several security schemas in that property.
+  - For several security schemas `security` support a new `LogicalContainer` that can contain upto 2 nested levels.
+  - Supported security types: `basic`, `bearer`, `input`, `header`, `cookie`, `openid` and `oauth2`.
+  - OpenID and OAuth2 security types are currently have the limited support: without scopes.
+
+```typescript
+// example middleware
+import { createMiddleware } from "express-zod-api";
+
+const authMiddleware = createMiddleware({
+  security: {
+    // requires the "key" in inputs and a custom "token" headers
+    and: [
+      { type: "input", name: "key" },
+      { type: "header", name: "token" },
+    ],
+  },
+  input: z.object({
+    key: z.string().min(1),
+  }),
+  middleware: async ({ input: { key }, request }) => {
+    if (key !== "123") {
+      throw createHttpError(401, "Invalid key");
+    }
+    if (request.headers.token !== "456") {
+      throw createHttpError(401, "Invalid token");
+    }
+    return { token: request.headers.token };
+  },
+});
+
+// another example with logical OR
+createMiddleware({
+  security: {
+    // requires either input and header OR bearer header
+    or: [
+      {
+        and: [
+          { type: "input", name: "key" },
+          { type: "header", name: "token" },
+        ],
+      },
+      {
+        type: "bearer",
+        format: "JWT",
+      },
+    ],
+  },
+  //...
+});
+```
+
 ### v7.6.3
 
 - [@rayzr522](https://github.com/rayzr522) has fixed the resolution of types in the ESM build for the `nodenext` case.
diff --git a/README.md b/README.md
index 57c53266..8ab8fa06 100644
--- a/README.md
+++ b/README.md
@@ -272,6 +272,13 @@ Here is an example of the authentication middleware, that checks a `key` from in
 import { createMiddleware, createHttpError, z } from "express-zod-api";
 
 const authMiddleware = createMiddleware({
+  security: {
+    // this information is optional and used for the generated documentation (OpenAPI)
+    and: [
+      { type: "input", name: "key" },
+      { type: "header", name: "token" },
+    ],
+  },
   input: z.object({
     key: z.string().min(1),
   }),
diff --git a/example/example.swagger.yaml b/example/example.swagger.yaml
index 6c1fd243..ba9a795d 100644
--- a/example/example.swagger.yaml
+++ b/example/example.swagger.yaml
@@ -189,6 +189,9 @@ paths:
                   key: 1234-5678-90
                   name: John Doe
                   birthday: 1963-04-21
+      security:
+        - APIKEY_1: []
+          APIKEY_2: []
   /v1/avatar/send:
     get:
       responses:
@@ -326,7 +329,15 @@ components:
   examples: {}
   requestBodies: {}
   headers: {}
-  securitySchemes: {}
+  securitySchemes:
+    APIKEY_1:
+      type: apiKey
+      in: query
+      name: key
+    APIKEY_2:
+      type: apiKey
+      in: header
+      name: token
   links: {}
   callbacks: {}
 tags: []
diff --git a/example/middlewares.ts b/example/middlewares.ts
index e43339ae..b3ba72cc 100644
--- a/example/middlewares.ts
+++ b/example/middlewares.ts
@@ -1,6 +1,12 @@
 import { createMiddleware, Method, createHttpError, z, withMeta } from "../src";
 
 export const authMiddleware = createMiddleware({
+  security: {
+    and: [
+      { type: "input", name: "key" },
+      { type: "header", name: "token" },
+    ],
+  },
   input: withMeta(
     z.object({
       key: z.string().min(1),
diff --git a/src/endpoint.ts b/src/endpoint.ts
index 6246c63d..7872860e 100644
--- a/src/endpoint.ts
+++ b/src/endpoint.ts
@@ -10,9 +10,11 @@ import {
   getInitialInput,
   IOSchema,
 } from "./common-helpers";
+import { combineContainers, LogicalContainer } from "./logical-container";
 import { AuxMethod, Method, MethodsDefinition } from "./method";
 import { AnyMiddlewareDef } from "./middleware";
 import { lastResortHandler, ResultHandlerDefinition } from "./result-handler";
+import { Security } from "./security";
 
 export type Handler<IN, OUT, OPT> = (params: {
   input: IN;
@@ -36,6 +38,7 @@ export abstract class AbstractEndpoint {
   public abstract getInputMimeTypes(): string[];
   public abstract getPositiveMimeTypes(): string[];
   public abstract getNegativeMimeTypes(): string[];
+  public abstract getSecurity(): LogicalContainer<Security>;
 }
 
 type EndpointProps<
@@ -133,6 +136,14 @@ export class Endpoint<
     return this.resultHandler.getNegativeResponse().mimeTypes;
   }
 
+  public override getSecurity(): LogicalContainer<Security> {
+    return this.middlewares.reduce<LogicalContainer<Security>>(
+      (acc, middleware) =>
+        middleware.security ? combineContainers(acc, middleware.security) : acc,
+      { and: [] }
+    );
+  }
+
   #getDefaultCorsHeaders(): Record<string, string> {
     const accessMethods = (this.methods as (M | AuxMethod)[])
       .concat("options")
diff --git a/src/logical-container.ts b/src/logical-container.ts
new file mode 100644
index 00000000..8bb694fe
--- /dev/null
+++ b/src/logical-container.ts
@@ -0,0 +1,103 @@
+import { combinations } from "./common-helpers";
+
+type LogicalOr<T> = { or: T[] };
+type LogicalAnd<T> = { and: T[] };
+
+export type LogicalContainer<T> =
+  | LogicalOr<T | LogicalAnd<T>>
+  | LogicalAnd<T | LogicalOr<T>>
+  | T;
+
+/** @desc combines several LogicalAnds into a one */
+export const flattenAnds = <T>(
+  subject: (T | LogicalAnd<T>)[]
+): LogicalAnd<T> => ({
+  and: subject.reduce<T[]>(
+    (agg, item) =>
+      agg.concat(typeof item === "object" && "and" in item ? item.and : item),
+    []
+  ),
+});
+
+/** @desc creates a LogicalContainer out of another one */
+export const mapLogicalContainer = <T, S>(
+  container: LogicalContainer<T>,
+  fn: (subject: T) => S
+): LogicalContainer<S> => {
+  if (typeof container === "object") {
+    if ("and" in container) {
+      return {
+        and: container.and.map((entry) =>
+          typeof entry === "object" && "or" in entry
+            ? { or: entry.or.map(fn) }
+            : fn(entry)
+        ),
+      };
+    }
+    if ("or" in container) {
+      return {
+        or: container.or.map((entry) =>
+          typeof entry === "object" && "and" in entry
+            ? { and: entry.and.map(fn) }
+            : fn(entry)
+        ),
+      };
+    }
+  }
+  return fn(container);
+};
+
+/** @desc converts LogicalAnd into LogicalOr */
+export const andToOr = <T>(
+  subject: LogicalAnd<T | LogicalOr<T>>
+): LogicalOr<T | LogicalAnd<T>> => {
+  return subject.and.reduce<LogicalOr<T | LogicalAnd<T>>>(
+    (acc, item) => {
+      const combs = combinations(
+        acc.or,
+        typeof item === "object" && "or" in item ? item.or : [item]
+      );
+      if (combs.type === "single") {
+        acc.or.push(...combs.value);
+      } else {
+        acc.or = combs.value.map(flattenAnds);
+      }
+      return acc;
+    },
+    {
+      or: [],
+    }
+  );
+};
+
+/** @desc reducer, combines two LogicalContainers */
+export const combineContainers = <T>(
+  a: LogicalContainer<T>,
+  b: LogicalContainer<T>
+): LogicalContainer<T> => {
+  if (typeof a === "object" && typeof b === "object") {
+    if ("and" in a) {
+      if ("and" in b) {
+        return flattenAnds([a, b]);
+      }
+      if ("or" in b) {
+        return combineContainers(andToOr(a), b);
+      }
+    }
+    if ("or" in a) {
+      if ("and" in b) {
+        return combineContainers(b, a);
+      }
+      if ("or" in b) {
+        const combs = combinations(a.or, b.or);
+        return {
+          or:
+            combs.type === "single"
+              ? combs.value
+              : combs.value.map(flattenAnds),
+        };
+      }
+    }
+  }
+  return { and: [a as T, b as T] };
+};
diff --git a/src/middleware.ts b/src/middleware.ts
index 4ad42fc8..dcd24131 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -3,6 +3,8 @@ import { HttpError } from "http-errors";
 import { Logger } from "winston";
 import { z } from "zod";
 import { FlatObject, IOSchema } from "./common-helpers";
+import { LogicalContainer } from "./logical-container";
+import { Security } from "./security";
 
 interface MiddlewareParams<IN, OPT> {
   input: IN;
@@ -22,6 +24,7 @@ export interface MiddlewareCreationProps<
   OUT extends FlatObject
 > {
   input: IN;
+  security?: LogicalContainer<Security<keyof z.input<IN> & string>>;
   middleware: Middleware<z.output<IN>, OPT, OUT>;
 }
 
diff --git a/src/open-api-helpers.ts b/src/open-api-helpers.ts
index 9991c9a2..e9b8b2da 100644
--- a/src/open-api-helpers.ts
+++ b/src/open-api-helpers.ts
@@ -9,6 +9,8 @@ import {
 import {
   RequestBodyObject,
   ResponseObject,
+  SecurityRequirementObject,
+  SecuritySchemeObject,
 } from "openapi3-ts/src/model/OpenApi";
 import { omit } from "ramda";
 import { z } from "zod";
@@ -25,8 +27,14 @@ import { ZodDateOut, ZodDateOutDef } from "./date-out-schema";
 import { AbstractEndpoint } from "./endpoint";
 import { OpenAPIError } from "./errors";
 import { ZodFile, ZodFileDef } from "./file-schema";
+import {
+  andToOr,
+  LogicalContainer,
+  mapLogicalContainer,
+} from "./logical-container";
 import { copyMeta } from "./metadata";
 import { Method } from "./method";
+import { Security } from "./security";
 import { ZodUpload, ZodUploadDef } from "./upload-schema";
 
 type MediaExamples = Pick<MediaTypeObject, "examples">;
@@ -714,6 +722,91 @@ export const depictResponse = ({
   };
 };
 
+type SecurityHelper<K extends Security["type"]> = (
+  security: Security & { type: K }
+) => SecuritySchemeObject;
+
+const depictBasicSecurity: SecurityHelper<"basic"> = ({}) => ({
+  type: "http",
+  scheme: "basic",
+});
+const depictBearerSecurity: SecurityHelper<"bearer"> = ({
+  format: bearerFormat,
+}) => ({
+  type: "http",
+  scheme: "bearer",
+  ...(bearerFormat ? { bearerFormat } : {}),
+});
+// @todo add description on actual input placement
+const depictInputSecurity: SecurityHelper<"input"> = ({ name }) => ({
+  type: "apiKey",
+  in: "query", // body is not supported yet, https://swagger.io/docs/specification/authentication/api-keys/
+  name,
+});
+const depictHeaderSecurity: SecurityHelper<"header"> = ({ name }) => ({
+  type: "apiKey",
+  in: "header",
+  name,
+});
+const depictCookieSecurity: SecurityHelper<"cookie"> = ({ name }) => ({
+  type: "apiKey",
+  in: "cookie",
+  name,
+});
+// @todo implement scopes
+const depictOpenIdSecurity: SecurityHelper<"openid"> = ({
+  url: openIdConnectUrl,
+}) => ({
+  type: "openIdConnect",
+  openIdConnectUrl,
+});
+// @todo implement scopes
+const depictOAuth2Security: SecurityHelper<"oauth2"> = ({}) => ({
+  type: "oauth2",
+});
+
+export const depictSecurity = (
+  container: LogicalContainer<Security>
+): LogicalContainer<SecuritySchemeObject> => {
+  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
+    basic: depictBasicSecurity,
+    bearer: depictBearerSecurity,
+    input: depictInputSecurity,
+    header: depictHeaderSecurity,
+    cookie: depictCookieSecurity,
+    openid: depictOpenIdSecurity,
+    oauth2: depictOAuth2Security,
+  };
+  return mapLogicalContainer(container, (security) =>
+    (methods[security.type] as SecurityHelper<typeof security.type>)(security)
+  );
+};
+
+export const depictSecurityNames = (
+  container: LogicalContainer<string>
+): SecurityRequirementObject[] => {
+  if (typeof container === "object") {
+    if ("or" in container) {
+      return container.or.map((entry) =>
+        (typeof entry === "string"
+          ? [entry]
+          : entry.and
+        ).reduce<SecurityRequirementObject>(
+          (agg, name) => ({
+            ...agg,
+            [name]: [],
+          }),
+          {}
+        )
+      );
+    }
+    if ("and" in container) {
+      return depictSecurityNames(andToOr(container));
+    }
+  }
+  return depictSecurityNames({ or: [container] });
+};
+
 export const depictRequest = ({
   method,
   path,
diff --git a/src/open-api.ts b/src/open-api.ts
index acf76ec2..b29a0da3 100644
--- a/src/open-api.ts
+++ b/src/open-api.ts
@@ -1,12 +1,20 @@
-import { OpenApiBuilder, OperationObject } from "openapi3-ts";
+import {
+  OpenApiBuilder,
+  OperationObject,
+  SecuritySchemeObject,
+  SecuritySchemeType,
+} from "openapi3-ts";
 import { defaultInputSources } from "./common-helpers";
 import { CommonConfig } from "./config-type";
+import { mapLogicalContainer } from "./logical-container";
 import { Method } from "./method";
 import {
   depictRequestParams,
   depictRequest,
   depictResponse,
   reformatParamsInPath,
+  depictSecurity,
+  depictSecurityNames,
 } from "./open-api-helpers";
 import { Routing, routingCycle, RoutingCycleParams } from "./routing";
 
@@ -21,6 +29,25 @@ interface GeneratorParams {
 }
 
 export class OpenAPI extends OpenApiBuilder {
+  protected lastSecuritySchemaIds: Partial<Record<SecuritySchemeType, number>> =
+    {};
+
+  protected ensureUniqSecuritySchemaName(subject: SecuritySchemeObject) {
+    for (const name in this.rootDoc.components?.securitySchemes || {}) {
+      if (
+        JSON.stringify(subject) ===
+        JSON.stringify(this.rootDoc.components?.securitySchemes?.[name])
+      ) {
+        return name;
+      }
+    }
+    this.lastSecuritySchemaIds[subject.type] =
+      (this.lastSecuritySchemaIds?.[subject.type] || 0) + 1;
+    return `${subject.type.toUpperCase()}_${
+      this.lastSecuritySchemaIds[subject.type]
+    }`;
+  }
+
   public constructor({
     routing,
     config,
@@ -68,6 +95,19 @@ export class OpenAPI extends OpenApiBuilder {
       if (inputSources.includes("body")) {
         operation.requestBody = depictRequest(commonParams);
       }
+      const securityRefs = depictSecurityNames(
+        mapLogicalContainer(
+          depictSecurity(endpoint.getSecurity()),
+          (securitySchema) => {
+            const name = this.ensureUniqSecuritySchemaName(securitySchema);
+            this.addSecurityScheme(name, securitySchema);
+            return name;
+          }
+        )
+      );
+      if (securityRefs.length > 0) {
+        operation.security = securityRefs;
+      }
       const swaggerCompatiblePath = reformatParamsInPath(path);
       this.addPath(swaggerCompatiblePath, {
         ...(this.rootDoc.paths?.[swaggerCompatiblePath] || {}),
diff --git a/src/security.ts b/src/security.ts
new file mode 100644
index 00000000..9a29d3ec
--- /dev/null
+++ b/src/security.ts
@@ -0,0 +1,50 @@
+interface BasicSecurity {
+  type: "basic";
+}
+
+interface BearerSecurity {
+  type: "bearer";
+  format?: "JWT" | string;
+}
+
+interface InputSecurity<K extends string> {
+  type: "input";
+  name: K;
+}
+
+interface CustomHeaderSecurity {
+  type: "header";
+  name: string;
+}
+
+interface CookieSecurity {
+  type: "cookie";
+  name: string;
+}
+
+/**
+ * @todo add scopes
+ * @see https://swagger.io/docs/specification/authentication/openid-connect-discovery/
+ */
+interface OpenIdSecurity {
+  type: "openid";
+  url: string;
+}
+
+/**
+ * @todo add more fields here
+ * @see https://swagger.io/docs/specification/authentication/oauth2/
+ */
+interface OAuth2Security {
+  type: "oauth2";
+}
+
+/** @desc K is an optional input field used by InputSecurity */
+export type Security<K extends string = string> =
+  | BasicSecurity
+  | BearerSecurity
+  | InputSecurity<K>
+  | CustomHeaderSecurity
+  | CookieSecurity
+  | OpenIdSecurity
+  | OAuth2Security;


Test patch:
diff --git a/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap b/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
index 5def5d0b..5af5b933 100644
--- a/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api-helpers.spec.ts.snap
@@ -479,6 +479,122 @@ Array [
 ]
 `;
 
+exports[`Open API helpers depictSecurity() should handle Basic, Bearer and CustomHeader Securities 1`] = `
+Object {
+  "or": Array [
+    Object {
+      "and": Array [
+        Object {
+          "scheme": "basic",
+          "type": "http",
+        },
+        Object {
+          "scheme": "bearer",
+          "type": "http",
+        },
+      ],
+    },
+    Object {
+      "in": "header",
+      "name": "X-Key",
+      "type": "apiKey",
+    },
+  ],
+}
+`;
+
+exports[`Open API helpers depictSecurity() should handle Input and Cookie Securities 1`] = `
+Object {
+  "and": Array [
+    Object {
+      "or": Array [
+        Object {
+          "in": "query",
+          "name": "apiKey",
+          "type": "apiKey",
+        },
+        Object {
+          "in": "cookie",
+          "name": "hash",
+          "type": "apiKey",
+        },
+      ],
+    },
+  ],
+}
+`;
+
+exports[`Open API helpers depictSecurity() should handle OpenID and OAuth2 Securities 1`] = `
+Object {
+  "or": Array [
+    Object {
+      "openIdConnectUrl": "https://test.url",
+      "type": "openIdConnect",
+    },
+    Object {
+      "type": "oauth2",
+    },
+  ],
+}
+`;
+
+exports[`Open API helpers depictSecurityNames() should handle LogicalAnd 1`] = `
+Array [
+  Object {
+    "A": Array [],
+    "B": Array [],
+    "C": Array [],
+  },
+]
+`;
+
+exports[`Open API helpers depictSecurityNames() should handle LogicalAnd 2`] = `
+Array [
+  Object {
+    "A": Array [],
+    "B": Array [],
+  },
+  Object {
+    "A": Array [],
+    "C": Array [],
+  },
+]
+`;
+
+exports[`Open API helpers depictSecurityNames() should handle LogicalOr 1`] = `
+Array [
+  Object {
+    "A": Array [],
+  },
+  Object {
+    "B": Array [],
+  },
+  Object {
+    "C": Array [],
+  },
+]
+`;
+
+exports[`Open API helpers depictSecurityNames() should handle LogicalOr 2`] = `
+Array [
+  Object {
+    "A": Array [],
+  },
+  Object {
+    "B": Array [],
+    "C": Array [],
+  },
+]
+`;
+
+exports[`Open API helpers depictSecurityNames() should handle the plain value 1`] = `
+Array [
+  Object {
+    "A": Array [],
+  },
+]
+`;
+
 exports[`Open API helpers depictString() should depict ZodString with refinements 1`] = `
 Object {
   "description": "test",
diff --git a/tests/unit/__snapshots__/open-api.spec.ts.snap b/tests/unit/__snapshots__/open-api.spec.ts.snap
index 48973542..dbfb5df8 100644
--- a/tests/unit/__snapshots__/open-api.spec.ts.snap
+++ b/tests/unit/__snapshots__/open-api.spec.ts.snap
@@ -263,6 +263,174 @@ servers:
 "
 `;
 
+exports[`Open API generator Metadata should ensure uniq security schema names 1`] = `
+"openapi: 3.0.0
+info:
+  title: Testing Security
+  version: 3.4.5
+paths:
+  /v1/getSomething:
+    get:
+      responses:
+        \\"200\\":
+          description: GET /v1/getSomething Successful response
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  status:
+                    type: string
+                    enum:
+                      - success
+                  data:
+                    type: object
+                    properties:
+                      num:
+                        type: number
+                        format: double
+                        minimum: 5e-324
+                        exclusiveMinimum: false
+                        maximum: 1.7976931348623157e+308
+                        exclusiveMaximum: false
+                    required:
+                      - num
+                required:
+                  - status
+                  - data
+        \\"400\\":
+          description: GET /v1/getSomething Error response
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  status:
+                    type: string
+                    enum:
+                      - error
+                  error:
+                    type: object
+                    properties:
+                      message:
+                        type: string
+                    required:
+                      - message
+                required:
+                  - status
+                  - error
+              examples:
+                example1:
+                  value:
+                    status: error
+                    error:
+                      message: Sample error message
+      parameters:
+        - name: key
+          in: query
+          required: true
+          schema:
+            description: GET /v1/getSomething parameter
+            type: string
+        - name: str
+          in: query
+          required: true
+          schema:
+            description: GET /v1/getSomething parameter
+            type: string
+      security:
+        - APIKEY_1: []
+        - HTTP_1: []
+  /v1/setSomething:
+    post:
+      responses:
+        \\"200\\":
+          description: POST /v1/setSomething Successful response
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  status:
+                    type: string
+                    enum:
+                      - success
+                  data:
+                    type: object
+                    properties: {}
+                    required: []
+                required:
+                  - status
+                  - data
+        \\"400\\":
+          description: POST /v1/setSomething Error response
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  status:
+                    type: string
+                    enum:
+                      - error
+                  error:
+                    type: object
+                    properties:
+                      message:
+                        type: string
+                    required:
+                      - message
+                required:
+                  - status
+                  - error
+              examples:
+                example1:
+                  value:
+                    status: error
+                    error:
+                      message: Sample error message
+      requestBody:
+        content:
+          application/json:
+            schema:
+              description: POST /v1/setSomething request body
+              allOf:
+                - type: object
+                  properties: {}
+                  required: []
+                - type: object
+                  properties: {}
+                  required: []
+      security:
+        - HTTP_1: []
+          APIKEY_2: []
+components:
+  schemas: {}
+  responses: {}
+  parameters: {}
+  examples: {}
+  requestBodies: {}
+  headers: {}
+  securitySchemes:
+    APIKEY_1:
+      type: apiKey
+      in: query
+      name: key
+    HTTP_1:
+      type: http
+      scheme: bearer
+    APIKEY_2:
+      type: apiKey
+      in: cookie
+      name: someCookie
+  links: {}
+  callbacks: {}
+tags: []
+servers:
+  - url: http://example.com
+"
+`;
+
 exports[`Open API generator Metadata should merge endpoint handler examples with its middleware examples 1`] = `
 "openapi: 3.0.0
 info:
@@ -1701,6 +1869,9 @@ paths:
                   key: 1234-5678-90
                   name: John Doe
                   birthday: 1963-04-21
+      security:
+        - APIKEY_1: []
+          APIKEY_2: []
   /v1/avatar/send:
     get:
       responses:
@@ -1838,7 +2009,15 @@ components:
   examples: {}
   requestBodies: {}
   headers: {}
-  securitySchemes: {}
+  securitySchemes:
+    APIKEY_1:
+      type: apiKey
+      in: query
+      name: key
+    APIKEY_2:
+      type: apiKey
+      in: header
+      name: token
   links: {}
   callbacks: {}
 tags: []
diff --git a/tests/unit/logical-container.spec.ts b/tests/unit/logical-container.spec.ts
new file mode 100644
index 00000000..5c1ef2f6
--- /dev/null
+++ b/tests/unit/logical-container.spec.ts
@@ -0,0 +1,111 @@
+import {
+  andToOr,
+  combineContainers,
+  mapLogicalContainer,
+} from "../../src/logical-container";
+
+describe("LogicalContainer", () => {
+  describe("mapLogicalContainer()", () => {
+    test("should map LogicalAnd", () => {
+      expect(
+        mapLogicalContainer(
+          { and: [1, 2, { or: [3, 4] }, 5] },
+          (value) => value * 2
+        )
+      ).toEqual({ and: [2, 4, { or: [6, 8] }, 10] });
+    });
+    test("should map LogicalOr", () => {
+      expect(
+        mapLogicalContainer(
+          { or: [1, 2, { and: [3, 4] }, 5] },
+          (value) => value * 2
+        )
+      ).toEqual({ or: [2, 4, { and: [6, 8] }, 10] });
+    });
+    test("should accept plain value", () => {
+      expect(mapLogicalContainer(1, (value) => value * 2)).toBe(2);
+    });
+  });
+
+  describe("andToOr()", () => {
+    test("should convert LogicalAnd of plains", () => {
+      expect(andToOr({ and: [1, 2, 3] })).toEqual({
+        or: [{ and: [1, 2, 3] }],
+      });
+    });
+    test("should handle LogicalOr inside", () => {
+      expect(andToOr({ and: [1, { or: [2, 3] }, 4] })).toEqual({
+        or: [{ and: [1, 2, 4] }, { and: [1, 3, 4] }],
+      });
+    });
+    test("should handle multiple LogicalOr", () => {
+      expect(andToOr({ and: [1, { or: [2, 3] }, { or: [4, 5] }, 6] })).toEqual({
+        or: [
+          { and: [1, 2, 4, 6] },
+          { and: [1, 2, 5, 6] },
+          { and: [1, 3, 4, 6] },
+          { and: [1, 3, 5, 6] },
+        ],
+      });
+    });
+  });
+
+  describe("combineContainers()", () => {
+    test("should combine flats", () => {
+      expect(combineContainers(1, 2)).toEqual({ and: [1, 2] });
+    });
+    test("should combine two LogicalAnd", () => {
+      expect(combineContainers({ and: [1, 2] }, { and: [3, 4] })).toEqual({
+        and: [1, 2, 3, 4],
+      });
+      expect(
+        combineContainers(
+          { and: [{ or: [1, 2] }, { or: [3, 4] }] },
+          { and: [{ or: [5, 6] }, { or: [7, 8] }] }
+        )
+      ).toEqual({
+        and: [{ or: [1, 2] }, { or: [3, 4] }, { or: [5, 6] }, { or: [7, 8] }],
+      });
+    });
+    test("should combine two LogicalOr", () => {
+      expect(combineContainers({ or: [1, 2] }, { or: [3, 4] })).toEqual({
+        or: [
+          { and: [1, 3] },
+          { and: [1, 4] },
+          { and: [2, 3] },
+          { and: [2, 4] },
+        ],
+      });
+    });
+    test("should handle empty and non-empty LogicalOr", () => {
+      expect(combineContainers({ or: [1, 2] }, { or: [] })).toEqual({
+        or: [1, 2],
+      });
+    });
+    test("should handle two complex LogicalOr", () => {
+      expect(
+        combineContainers(
+          { or: [{ and: [1, 2] }, { and: [3, 4] }] },
+          { or: [{ and: [5, 6] }, { and: [7, 8] }] }
+        )
+      ).toEqual({
+        or: [
+          { and: [1, 2, 5, 6] },
+          { and: [1, 2, 7, 8] },
+          { and: [3, 4, 5, 6] },
+          { and: [3, 4, 7, 8] },
+        ],
+      });
+    });
+    test("should combine LogicalAnd with LogicalOr", () => {
+      expect(combineContainers({ and: [1, 2] }, { or: [3, 4] })).toEqual({
+        or: [{ and: [1, 2, 3] }, { and: [1, 2, 4] }],
+      });
+    });
+    test("should handle reverse order", () => {
+      expect(combineContainers({ or: [1, 2] }, { and: [3, 4] })).toEqual({
+        or: [{ and: [3, 4, 1] }, { and: [3, 4, 2] }],
+      });
+    });
+  });
+});
diff --git a/tests/unit/open-api-helpers.spec.ts b/tests/unit/open-api-helpers.spec.ts
index 9fba7c16..49e6fba3 100644
--- a/tests/unit/open-api-helpers.spec.ts
+++ b/tests/unit/open-api-helpers.spec.ts
@@ -37,6 +37,8 @@ import {
   excludeParamsFromDepiction,
   reformatParamsInPath,
   extractObjectSchema,
+  depictSecurity,
+  depictSecurityNames,
 } from "../../src/open-api-helpers";
 import { serializeSchemaForTest } from "../helpers";
 
@@ -821,4 +823,58 @@ describe("Open API helpers", () => {
       }
     });
   });
+
+  describe("depictSecurity()", () => {
+    test("should handle Basic, Bearer and CustomHeader Securities", () => {
+      expect(
+        depictSecurity({
+          or: [
+            { and: [{ type: "basic" }, { type: "bearer" }] },
+            { type: "header", name: "X-Key" },
+          ],
+        })
+      ).toMatchSnapshot();
+    });
+    test("should handle Input and Cookie Securities", () => {
+      expect(
+        depictSecurity({
+          and: [
+            {
+              or: [
+                { type: "input", name: "apiKey" },
+                { type: "cookie", name: "hash" },
+              ],
+            },
+          ],
+        })
+      ).toMatchSnapshot();
+    });
+    test("should handle OpenID and OAuth2 Securities", () => {
+      expect(
+        depictSecurity({
+          or: [{ type: "openid", url: "https://test.url" }, { type: "oauth2" }],
+        })
+      ).toMatchSnapshot();
+    });
+  });
+
+  describe("depictSecurityNames()", () => {
+    test("should handle LogicalAnd", () => {
+      expect(depictSecurityNames({ and: ["A", "B", "C"] })).toMatchSnapshot();
+      expect(
+        depictSecurityNames({ and: ["A", { or: ["B", "C"] }] })
+      ).toMatchSnapshot();
+    });
+
+    test("should handle LogicalOr", () => {
+      expect(depictSecurityNames({ or: ["A", "B", "C"] })).toMatchSnapshot();
+      expect(
+        depictSecurityNames({ or: ["A", { and: ["B", "C"] }] })
+      ).toMatchSnapshot();
+    });
+
+    test("should handle the plain value", () => {
+      expect(depictSecurityNames("A")).toMatchSnapshot();
+    });
+  });
 });
diff --git a/tests/unit/open-api.spec.ts b/tests/unit/open-api.spec.ts
index 07ce98ac..defd0476 100644
--- a/tests/unit/open-api.spec.ts
+++ b/tests/unit/open-api.spec.ts
@@ -791,5 +791,51 @@ describe("Open API generator", () => {
       }).getSpecAsYaml();
       expect(spec).toMatchSnapshot();
     });
+
+    test("should ensure uniq security schema names", () => {
+      const mw1 = createMiddleware({
+        security: {
+          or: [{ type: "input", name: "key" }, { type: "bearer" }],
+        },
+        input: z.object({
+          key: z.string(),
+        }),
+        middleware: jest.fn(),
+      });
+      const mw2 = createMiddleware({
+        security: {
+          and: [{ type: "bearer" }, { type: "cookie", name: "someCookie" }],
+        },
+        input: z.object({}),
+        middleware: jest.fn(),
+      });
+      const spec = new OpenAPI({
+        config,
+        routing: {
+          v1: {
+            getSomething: defaultEndpointsFactory.addMiddleware(mw1).build({
+              method: "get",
+              input: z.object({
+                str: z.string(),
+              }),
+              output: z.object({
+                num: z.number(),
+              }),
+              handler: async () => ({ num: 123 }),
+            }),
+            setSomething: defaultEndpointsFactory.addMiddleware(mw2).build({
+              method: "post",
+              input: z.object({}),
+              output: z.object({}),
+              handler: async () => ({}),
+            }),
+          },
+        },
+        version: "3.4.5",
+        title: "Testing Security",
+        serverUrl: "http://example.com",
+      }).getSpecAsYaml();
+      expect(spec).toMatchSnapshot();
+    });
   });
 });