DEV Community

loading...

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

Manuel Heidrich
Full Stack JavaScript Developer in Brandenburg. Creator of Monitornator. Angular, Svelte, Vue, Ionic, Capacitor, Cordova, NestJS, Express. Making the web awesome since 2002. Also: ๐ŸŽบ ๐Ÿƒ๐Ÿผโ€โ™‚๏ธ ๐Ÿ“ท โœˆ๏ธ ๐Ÿ”
Originally published at manuel-heidrich.dev ใƒปUpdated on ใƒป3 min read

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();

Enter fullscreen mode Exit fullscreen mode

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 or /docs-json. 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 and /docs-json endpoints. 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', '/docs-json'], 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();

Enter fullscreen mode Exit fullscreen mode

The key to getting this to work is the right order, it is important to apply the middleware app.use(['/docs', '/docs-json'], 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', '/docs-json'], 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();

Enter fullscreen mode Exit fullscreen mode

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!

Discussion (4)

Collapse
kiwikilian profile image
Kilian

Great post, actually the only one I found! A possible security oversight: /docs-json is not protected by this. Therefore use an array for the basic auth ['/docs', '/docs-json']. Also I would recommend using the ConfigurationService instead of directly accessing the env variables.

I adapted your solution on this stackoverflow: stackoverflow.com/questions/548028...

Collapse
mahnuh profile image
Manuel Heidrich Author

Thanks @kiwikilian !

I updated my post with your proposal. Honestly, I did not know about the -json part, it is cool though!

I also was wondering why using ConfigurationService instead of directly accessing the env variables would be a better idea?

Collapse
kiwikilian profile image
Kilian

Cool! You can use the JSON format of your OpenAPI with other generator tools for example to generate types and fetch functions for your frontend application.

The ConfigurationService would simply be the most NestJS way of doing it. Itโ€™s awesome to validate your envs so you donโ€™t forget about configuring something. But itโ€™s not necessary, just a further read: docs.nestjs.com/techniques/configu...

Collapse
zaunermax profile image
Maximilian Zauner

Thanks, i was searching for this so much on the internet but could not find anything. Not sure if it's just not intended to make a API doc private, but in our company we have an external app development company, so we need a public staging access to the api docs that need to be authenticated somehow... No idea why that issue hasn't found it's way into the nest docs.