58

I'm currently using Swagger in my NestJS project, and I have the explorer enabled:

in main.js

const options = new DocumentBuilder()
    .setTitle('My App')
    .setSchemes('https')
    .setDescription('My App API documentation')
    .setVersion('1.0')
    .build()

const document = SwaggerModule.createDocument(app, options)
SwaggerModule.setup('docs', app, document, {
    customSiteTitle: 'My App documentation',
})

With this, the explorer is accessible in /docs which is what I expected. But I was wondering if it's possible to add any Authentication layer to the explorer, so only certain requests are accepted.

I want to make this explorer accessible in production, but only for authenticated users.

Hasan Haghniya
  • 2,347
  • 4
  • 19
  • 29
josec89
  • 1,932
  • 1
  • 16
  • 19
  • 1
    Most of the time, the way I see this is people pull explorer _out_ of their production instance... – Rich Duncan Feb 21 '19 at 13:16
  • 1
    I would suggest to add a security in your reverse proxy (apache or nginx or varnish etc). Quite easy to add a rule with basic auth or blocking the access for instance. If you really want to manage it within Nest, using a Middleware should do the trick – zenbeni Feb 21 '19 at 14:48
  • Yeah, I my plan was to use one of the middlewares we have for the application, but maybe move this to a different layer (or even remove from production altogether is the only way) :) – josec89 Feb 22 '19 at 14:02
  • @zenbeni I want to do that, however, I can't send authorization headers within iframe src or browser url, how did you solve that? – Kunok Mar 11 '19 at 19:48

15 Answers15

36

Just add .addBearerAuth() (without any parameters) to your swagger options

and @ApiBearerAuth() to your Controller methods

const options = new DocumentBuilder()
    .setTitle('My App')
    .setSchemes('https')
    .setDescription('My App API documentation')
    .setVersion('1.0')
    .addBearerAuth()
    .build()

blackgreen
  • 34,072
  • 23
  • 111
  • 129
linuxistsuper
  • 487
  • 4
  • 6
  • 1
    I assumed the top level bearer would apply it to everything, but I was wrong -- I guess you really do need it on every controller. Edit: is there any way to persist the authentication between refreshes? – CTS_AE Mar 23 '22 at 22:57
36

Securing access to your Swagger with HTTP Basic Auth using NestJS with Express

First run npm i express-basic-auth then add the following to your main.{ts,js}:

import * as basicAuth from "express-basic-auth";

// ...

// Sometime after NestFactory add this to add HTTP Basic Auth
app.use(
  // Paths you want to protect with basic auth
  "/docs*",
  basicAuth({
    challenge: true,
    users: {
      yourUserName: "p4ssw0rd",
    },
  })
);

// Your code
const options = new DocumentBuilder()
  .setTitle("My App")
  .setSchemes("https")
  .setDescription("My App API documentation")
  .setVersion("1.0")
  .build();

const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup(
  // Make sure you use the same path just without `/` and `*`
  "docs",
  app,
  document,
  {
    customSiteTitle: "My App documentation",
  }
);

// ...

With this in place you will be prompted on any of the /docs route with a HTTP Basic Auth prompt. We add the * to also protect the generated JSON (/docs-json) and YAML (/docs-json) OpenAPI files. If you have any other route beginning with /docs, that should not be protected, you should rather explicitly name the routes you want to protect in an array ['/docs', '/docs-json', '/docs-yaml'].

You should not put the credentials in your code/repository but rather in your .env and access via the ConfigService.

I have seen this solution first here.

KiwiKilian
  • 981
  • 1
  • 7
  • 10
28

Updated following breaking/API changes in @nestjs/swagger version 4.0.

Hi, Took a lot of try&fail to get this right. The comments in the code is what is important to understand. The names rely on each other for this to work.

main.ts

    const options = new DocumentBuilder()
        .setTitle('my-title')
        .setDescription('my-descirption')
        .setVersion('1.0')
        .addBearerAuth(
          {
            type: 'http',
            scheme: 'bearer',
            bearerFormat: 'JWT',
            name: 'JWT',
            description: 'Enter JWT token',
            in: 'header',
          },
          'JWT-auth', // This name here is important for matching up with @ApiBearerAuth() in your controller!
        )
        .build();
      const document = SwaggerModule.createDocument(app, options);
      SwaggerModule.setup('api', app, document);

And in your controller you do the following (note @ApiBearerAuth() using the same name as the name on the swagger options in main.ts):

app.controller.ts

    @Roles(Role.Admin)
      @UseGuards(JwtAuthGuard, RolesGuard)
      @ApiTags('Admin')
      @ApiOperation({ summary: 'Get admin section' })
      @Get('admin')
      @ApiBearerAuth('JWT-auth') // This is the one that needs to match the name in main.ts
      getAdminArea(@Request() req) {
        return req.user;
      }

Hope this saves somebody the time it took me to understand what was going on.

Lior Kupers
  • 528
  • 6
  • 18
  • 3
    Ugh, finally. This is the only write-up that actually explained it well enough for my idiot brain to get it. One additional thing that could be added is that `@ApiBearerAuth('JWT-auth')` can be used as a decorator on the entire controller class, too. like `@Controller('users') @ApiBearerAuth('JWT-auth') export class UserController {...}` You can also put `.addSecurityRequirements('JWT-auth')` before `.build()` in the `main.ts` to apply that auth scheme to the whole system. – Patrick Mar 06 '23 at 18:34
22

UPDATE

As per recent changes in DocumentBuilder methods, this how it worked for me. Sharing for the people who are using new versions.

const options = new DocumentBuilder()
.setTitle('My API')
.setDescription('API used for testing purpose')
.setVersion('1.0.0')
.setBasePath('api')
.addBearerAuth(
  { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
  'access-token',
)
.build();

const document = SwaggerModule.createDocument(app, options);

Update Also, please use @ApiBearerAuth() on your controller function to add auth.

@Get('/test')
@ApiBearerAuth()

access-token is the name for reference in swagger doc. Your token in the header will be passed as below:

curl -X GET "http://localhost:3004/test" -H "accept: application/json" -H "Authorization: Bearer test-token"
pravindot17
  • 1,199
  • 1
  • 15
  • 32
  • 5
    Somehow this does not work for me, the header does not get applied to the request - the curl output stays - curl -X GET "http://localhost:3000/unit-type" -H "accept: */*" – Jacobdo May 04 '20 at 14:25
  • 2
    @Jacobdo can you see lock icon on your endpoint in swagger doc? You can click on it and pass the access token, if not then you need to add `@ApiBearerAuth()` in controller function, see updated answer – pravindot17 May 05 '20 at 03:38
  • 3
    This tells about the security of your endpoints, not the swagger itself. – Maciej Sikorski May 11 '20 at 13:15
  • 4
    just `.addBearerAuth({ in: 'header', type: 'http' })` – Ali Turki Aug 03 '20 at 19:37
  • 2
    The question is about securing access to the swagger page itself, not showing the auth options on routes swagger displays. See my answer for actually securing your `/docs` endpoint with HTTP Basic Auth. – KiwiKilian Jun 02 '21 at 09:47
7

THIS IS FOR APIKEY NOT BEARER

In case somebody gets to this post and looking for apiKey (instead of bearer) you need to follow this

in main.ts

    const options = new DocumentBuilder()
        .setTitle('CMOR')
        .setDescription('CMOR API documentation')
        .setVersion('1.0')
        .addServer('/api')
        .addApiKey({
            type: 'apiKey', // this should be apiKey
            name: 'api-key', // this is the name of the key you expect in header
            in: 'header',
        }, 'access-key' // this is the name to show and used in swagger
        ) 
        .build();

then in your controller or methods

@ApiTags('analyzer')
@ApiSecurity('access-key') // this is the name you set in Document builder
@Controller('analyzer')
export class ScreenAnalyzerController {
Reza
  • 18,865
  • 13
  • 88
  • 163
5

The following example is working very well

.addBearerAuth({ in: 'header', type: 'http' })

You should tell where is the token location in the in prop

and since you override the default options you should pass the type

  const options = new DocumentBuilder()
    .setTitle('Api docs for mobile')
    .setDescription('The api docs for the mobile application')
    .setVersion('1.0')
    .addBearerAuth({ in: 'header', type: 'http' })
    .build();

the addBearerAuth implementation

    addBearerAuth(options = {
        type: 'http'
    }, name = 'bearer') {
        this.addSecurity(name, Object.assign({ scheme: 'bearer', bearerFormat: 'JWT' }, options));
        return this;
    }
Ali Turki
  • 1,265
  • 2
  • 16
  • 21
4

For anyone with similar challenge, you can add Authentication to your Swagger UI in Nestjs as shown below.

const options = new DocumentBuilder()
.setTitle('Sample Project API')
.setDescription('This is a sample project to demonstrate auth in Swagger UI')
.setVersion('1.0')
.addTag('Nestjs Swagger UI')
.setContactEmail('your_contact@mail.com')
.addBearerAuth('Authorization', 'header', 'basic')
.setBasePath('api')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('docs', app, document);

So .addBearerAuth takes 3 arguments (key-name, location, authentication-type). authorization-type can be basic, bearer or apikey

  • 2
    I got an error when specifying "bearer" as the authentication-type to the .addBearerAuth method. Turns out if you just don't include the third parameter, it enables bearer authentication. Using the 'basic' value did turn on username/password http auth- – chrismarx Sep 12 '19 at 21:58
  • they made a huge change on the DocumentBuilder methods and their params, I hope someone makes an example of this changes. – Lu Blue Feb 19 '20 at 10:58
4

after adding .addBearerAuth() to your swagger options, you should add @ApiBearerAuth() to your Controller or it's methods.

NOTE: in order to keep token in swagger UI in browser after refreshing page you should set this in swagger options:

SwaggerModule.setup('docs', app, document, {
    swaggerOptions: {
        persistAuthorization: true, // this
    },
});
Ali Sherafat
  • 3,506
  • 2
  • 38
  • 51
3

You can do that by adding addApiKey or addBearerAuth examples of which are described in other answers to this question.

From my side, I can add OAuth2 authentication There are some differences in implementation between @nestjs/swagger3** and @nestjs/swagger4**

For the @nestjs/swagger3**

const options = new DocumentBuilder()
    .setTitle('API')
    .setDescription('API')
    .setVersion('1.0')
    .setSchemes('https', 'http')
    .addOAuth2('implicit', AUTH_URL, TOKEN_URL)
    .build();

const document = SwaggerModule.createDocument(app, options);

SwaggerModule.setup(swaggerPath, app, document, {
    swaggerOptions: {
        oauth2RedirectUrl: REDIRECT_URL, // after successfully logging
        oauth: {
            clientId: CLIENT_ID,
        },
    },
});

The addOAuth2 also supports flows as password, application and accessCode

For the @nestjs/swagger4**

const options = new DocumentBuilder()
    .setTitle('API')
    .setDescription('API description')
    .setVersion(version)
    .addServer(host)
    .addOAuth2(
        {
            type: 'oauth2',
            flows: {
                implicit: {
                    authorizationUrl: AUTH_URL + `?nonce=${getRandomNumber(9)}`, // nonce parameter is required and can be random, for example nonce=123456789
                    tokenUrl: TOKEN_URL,
                    scopes: SCOPES, // { profile: 'profile' }
                },
            },
        },
        'Authentication'
    )
    .build();

const document = SwaggerModule.createDocument(app, options);

SwaggerModule.setup(swaggerPath, app, document, {
    swaggerOptions: {
        oauth2RedirectUrl: REDIRECT_URL, // after successfully logging
        oauth: {
            clientId: CLIENT_ID,
        },
    },
});
CyberEternal
  • 2,259
  • 2
  • 12
  • 31
  • I followed this demo with the swagger4 but I'm having an issue with the scopes object, To register the API I used the scopeURL, and when I set only the name like you suggested **profile**, I get an error which says that I can't request this scope – Pini Cheyni Jul 04 '21 at 16:38
  • Actually, I have not used the scopeURL. I have set the scope as an object like in an example. and there can be added many properties like `{profile: 'profile', email:'email', ...}`. The value of `scopes` can be also an array, like `['profile', 'email', ...]`. But I'm not sure that you can use scopeURL as a value of the `scope` parameter since it can't be a string. You can check the module codes and see that. – CyberEternal Jul 05 '21 at 06:42
2

In your main.ts file add this

 const config = new DocumentBuilder()
    .setTitle('App title')
    .setDescription("Api description")
    .setVersion('1.0')
    .addTag('ApiTag')
    .setContact('name', 'ulr', "email")
    .addBearerAuth({ type: 'http', schema: 'Bearer', bearerFormat: 'Token' } as SecuritySchemeObject, 'Bearer')
    .build();

Controller file

@ApiBearerAuth("Bearer")
@Controller('posts')
export class PostController {
  constructor(private readonly postService: PostService) { }
}

Set your token

user13183485
  • 19
  • 1
  • 3
  • In Your controller add this @ApiBearerAuth("Bearer") – user13183485 Apr 11 '22 at 13:14
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 11 '22 at 14:05
1

For anyone wants authentication globally, u can add .addSecurityRequirements behind .build(). Don't worry login router will not effect, it depend on ur logic it require token or not.

 const document = SwaggerModule.createDocument(
      app,
      new DocumentBuilder()
        .setTitle('Book store')
        .setDescription('The Book store API description')
        .setVersion('1.0')
        .addBearerAuth(
          {
            type: 'http',
            scheme: 'Bearer',
            bearerFormat: 'JWT',
            in: 'header',
          },
          'token',
        )
        .addSecurityRequirements('token')
        .build(),
    );

    // access http://localhost:${PORT}/docs
    SwaggerModule.setup('docs', app, document);
    app.use('/apidoc-json/', (req: Request, res: any) => res.send(document));
0

For anyone who is not able to solve with the above answers.

Here is how I was able to add the bearer token

const options = new DocumentBuilder()
.setTitle('My API')
.setDescription('My api')
.setVersion('1.0.0')
.addBearerAuth(
  {
    type: 'http',
    scheme: 'bearer',
    bearerFormat: 'JWT',
    name: 'JWT',
    description: 'Enter JWT token',
    in: 'header',
  },
  'token'
)
.build();

Once you add this don't forget to add the decorator @ApiBearerAuth('token')

And one more thing to notice here is the second argument in the .addBearerAuth({...}, 'token') method needs to add in the decorator then only you will be able to see the Authorization in the curl request.

@Controller('api')
@ApiBearerAuth('token')

You can keep it empty too @ApiBearerAuth() and remove the second argument from the

.addBearerAuth(
{
    type: 'http',
    scheme: 'bearer',
    bearerFormat: 'JWT',
    name: 'JWT',
    description: 'Enter JWT token',
    in: 'header',
})

NestJS documentation needs to be improved

iamsujit
  • 1,679
  • 2
  • 12
  • 20
0

based on previous answers, you may see this error (if you want to use express-basic-auth module

Type 'typeof expressBasicAuth' has no call signatures.

Type originates at this import. A namespace-style import cannot be called or constructed, and will cause a failure at runtime. Consider using a default import or import require here instead

for this situation you can use .default in main.ts

import * as basicAuth from 'express-basic-auth';

async function bootstrap() {

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

  const options = new DocumentBuilder()
      .setTitle('api')
      .setDescription('API description')
      .setVersion('1.0')
      .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup('docs', app, document);

}
Mohammad Yaser Ahmadi
  • 4,664
  • 3
  • 17
  • 39
0

For anyone that uses the root as the swagger endpoint, you can do this:

import basicAuth from "express-basic-auth";
import { Request } from "express";

app.use(["/", "/-json"], function (req: Request, res, next) {
  if (req.accepts().includes("application/json")) {
    next();
  } else {
    const auth = basicAuth({
      challenge: true,
      users: {
        [process.env.SWAGGER_USER]: process.env.SWAGGER_PASSWORD,
      },
    });
    auth(req, res, next);
  }
});
acw
  • 807
  • 3
  • 15
0

As per the documentation here in case you want to protect access to the swagger page /api-docs i.e only users with access can view the page have the below code in main.ts

 const apiDocumentationCredentials = {
  name: 'admin',
  pass: 'admin',
};
async function bootstrap() {
  const app = await NestFactory.create<INestApplication>(ApplicationModule);
  const httpAdapter = app.getHttpAdapter();
  httpAdapter.use('/api-docs', (req, res, next) => {
    function parseAuthHeader(input: string): { name: string; pass: string } {
      const [, encodedPart] = input.split(' ');
      const buff = Buffer.from(encodedPart, 'base64');
      const text = buff.toString('ascii');
      const [name, pass] = text.split(':');
      return { name, pass };
    }
    function unauthorizedResponse(): void {
      if (httpAdapter.getType() === 'fastify') {
        res.statusCode = 401;
        res.setHeader('WWW-Authenticate', 'Basic');
      } else {
        res.status(401);
        res.set('WWW-Authenticate', 'Basic');
      }
      next();
    }
    if (!req.headers.authorization) {
      return unauthorizedResponse();
    }
    const credentials = parseAuthHeader(req.headers.authorization);
    if (
      credentials?.name !== apiDocumentationCredentials.name ||
      credentials?.pass !== apiDocumentationCredentials.pass
    ) {
      return unauthorizedResponse();
    }
    next();
  });
}
Nelson Bwogora
  • 2,225
  • 1
  • 18
  • 21