How to secure the OpenAPI Specification and Swagger UI in a NestJS application

March 21, 2021 ~ 4 min read

How to secure the OpenAPI Specification and Swagger UI in a NestJS application


One cool thing about Nest is its dedicated OpenAPI module which allows you to nearly automatically generate an OpenAPI specification for your API. You practically just have to add some decorators here and there and voila.

„The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection.“ Read more about the OpenAPI Specification here.

However OAS aims to be open by its name, making your API specifications available to everyone might not always be what you want, for example when your project‘s API is not a public one.

So what to do, when you want to benefit from OAS and the Swagger UI by only giving your team members access?

My strategy usually is protecting the Swagger UI with a password and hiding it on production entirely. This is how you could achieve that.

Getting started

Following the Nest docs, after installing all needed dependencies your main.ts could look something like this:

// main.ts

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('API Docs')
    .setDescription('The API documentation')
    .setVersion('1.0')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);

  await app.listen(3000);
}
bootstrap();

Swagger UI will be up and running visiting http://localhost:8000/docs in the browser.

Password protection

So first let’s protect the Swagger UI with HTTP basic auth requiring visitors to enter a username and password to access /docs. This can easily be done by implementing express-basic-auth, a simple plug & play HTTP basic auth middleware for Express.

npm i express-basic-auth

After installing express-basic-auth you would want to enable this middleware for your /docs endpoint. To do so modify main.ts like the following:

// main.ts

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import * as basicAuth from 'express-basic-auth';

import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.use('/docs', basicAuth({
    challenge: true,
    users: {
      [process.env.SWAGGER_USER]: process.env.SWAGGER_PASSWORD,
    },
  }));

  const config = new DocumentBuilder()
    .setTitle('API Docs')
    .setDescription('The API documentation')
    .setVersion('1.0')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);

  await app.listen(3000);
}
bootstrap();

The key to getting this to work is the right order, it is important to apply the middleware app.use(‘/docs’, basicAuth({…}) before you initialize Swagger.

basicAuth() in this scenario expects an object of users, I am using just one here. Keep in mind that it is always a good idea to not hardcode credentials, so relying on environment variables is a good option here. There are quite a few config options to express-basic-auth available, just check out the docs.

Hide Swagger UI on production

As mentioned the second thing I tend to do is hiding Swagger UI entirely on production. The most simple way to do so might be by wrapping a conditional statement around the parts initializing Swagger. Check this out:

// main.ts

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import * as basicAuth from 'express-basic-auth';

import { AppModule } from './app.module';

const SWAGGER_ENVS = ['local', 'dev', 'staging'];

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  if (SWAGGER_ENVS.includes(process.env.NODE_ENV)) {
    app.use('/docs', basicAuth({
      challenge: true,
      users: {
        [process.env.SWAGGER_USER]: process.env.SWAGGER_PASSWORD,
      },
    }));

    const config = new DocumentBuilder()
      .setTitle('API Docs')
      .setDescription('The API documentation')
      .setVersion('1.0')
      .build();

    const document = SwaggerModule.createDocument(app, config);
    SwaggerModule.setup('docs', app, document);
  }

  await app.listen(3000);
}
bootstrap();

This basically only applys the basic auth middleware and initializes Swagger when NODE_ENV is local, dev or staging. Depending on how you handle your environment names or have any other mechanism to check for a prodcution deployment, this may look slightly different in your project, but I think you get the gist.

So that's about it!