Is it possible to add Authentication to access to NestJS' Swagger Explorer
Asked Answered
A

15

66

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.

Audun answered 21/2, 2019 at 8:52 Comment(4)
Most of the time, the way I see this is people pull explorer out of their production instance...Skilling
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 trickPliner
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) :)Audun
@Pliner I want to do that, however, I can't send authorization headers within iframe src or browser url, how did you solve that?Ossy
C
43

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.

Cubism answered 2/6, 2021 at 9:45 Comment(1)
Because there is also a /docs-yaml endpoint I ended up specifying ['/docs*']Mier
L
40

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

Lauralauraceous answered 29/6, 2020 at 14:3 Comment(2)
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?Herrington
Better solution then using @ApiBearerAuth: #64270304Fearful
R
34

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.

Ronnaronnholm answered 16/3, 2021 at 12:55 Comment(1)
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.Ampere
H
23

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"
Hellcat answered 3/3, 2020 at 8:43 Comment(5)
Somehow this does not work for me, the header does not get applied to the request - the curl output stays - curl -X GET "localhost:3000/unit-type" -H "accept: /"Wisecrack
@Wisecrack 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 answerHellcat
This tells about the security of your endpoints, not the swagger itself.Ostend
just .addBearerAuth({ in: 'header', type: 'http' })Bigod
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.Cubism
A
8

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 {
Abscission answered 10/8, 2020 at 23:37 Comment(0)
A
6

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
    },
});
Arnone answered 25/12, 2022 at 8:26 Comment(1)
a good solutionPresidium
B
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;
    }
Bigod answered 3/8, 2020 at 19:42 Comment(0)
G
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('[email protected]')
.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

Germinative answered 12/9, 2019 at 14:34 Comment(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-Geometrician
they made a huge change on the DocumentBuilder methods and their params, I hope someone makes an example of this changes.Enarthrosis
I
4

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));
Interface answered 11/5, 2023 at 3:15 Comment(1)
thanks! After so many trial-error, your way worked!Megadeath
L
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,
        },
    },
});
Landri answered 30/6, 2021 at 12:26 Comment(2)
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 scopeDight
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.Landri
C
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

Clinkscales answered 11/4, 2022 at 13:10 Comment(2)
In Your controller add this @ApiBearerAuth("Bearer")Clinkscales
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.Gudren
S
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

Sturgis answered 6/8, 2021 at 14:0 Comment(1)
it'd be even better if it didn't require the @ApiBearerAuth decorator on controllers/routes.Kruller
G
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);

}
Gault answered 7/9, 2021 at 8:10 Comment(0)
C
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);
  }
});
Christmas answered 30/8, 2022 at 12:17 Comment(0)
R
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();
  });
}
Rightward answered 10/7, 2023 at 14:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.