swaggerUI from behind AWS API Gateway?

Hi,
We’d like to host out swaggerUI but it’s sitting behind an AWS API Gateway. I know about prependSecurity, but the swaggerUI makes additional requests. I’ve reviewed issues: 85, 136 & 474, but those don’t seem to provide a full answer.

We tried creating the endpoints (w/some SwaggerUIOptions) and then mapping over the generated endpoints and adding prependSecurityPure. That doesn’t seem sufficient. Even if that were sufficient, we likely need to open up certain paths on the AWS API Gateway.

Ideally, we could add our JWT token to all those requests (or if they’re simply for static content maybe just let them through w/o an authorizer).

Much appreciated in advance.
Uri

Can you maybe explain a bit more what’s the problem? What kind of functionality are you missing from the swagger UI?

Hi @adamw!
Thanks for the prompt reply. We love the SwaggerUI, we just want to be able to access it in prod too – which is behind an AWS API Gateway. Said API gateway blocks all requests by default and we need to carve out exceptions by path (default for /api/ where swagger sits is to require a JWT auth token). In addition, we expect there are GET requests for e.g. images and styles, so I don’t think that prependSecurity on its own gets us all the way to working in production behind an API Gateway.

What I would like is:

  1. How we identify the other endpoints we need to open up?
  2. Whether we can secure those with a JWT token or have to leave them wide open.
    Hope that makes sense. I’m expecting this information (no code change anticipated) will be helpful not only to us but also to others.

Thanks again!

I guess this depends how you exactly expose SwaggerUI and where the docs live. If the SwaggerUI is available under the /api/docs path, for example, then all requests for HTML, CSS, JS, images will hit children of this path. So one options is to simply configure that path as available.

If you need to customise the endpoints that are used to expose SwaggerUI, as described here, the SwaggerInterpreter().fromEndpoints gives you a List[ServerEndpoint[Any, F]], which handle requests for the HTML and all resources. So if you prepend any security for them, this will also apply to stylesheets etc. I suppose this might also include “static” verification of JWT tokens, based on header values?

If this still doesn’t work, maybe you could share a code example, it will be easier to work on a specific case.

Thank you @adamw . Your comments were invaluable as usual. Apologies for the delay in responding. We got it working, and think the documentation would benefit from a more explicit example. Towards that end, we’re including our code that will hopefully help others.

There are several key concepts here:

  1. We had to grab the jwt token directly from the cookie in this case and expect others will want to do the same.
  2. The authentication error output is a bit different than a typical endpoint – at least in how it’s packaged
  3. While the documentation mentions the ability to get the doc endpoints, I at least, did not find it obvious how to put the pieces together. I also confused the ServerEndpoints and Endpoints as in this context that can get confusing.
  4. One often wants to track metrics, have endpoints, and have docs (swagger/redoc) and they interact. For example, metrics on use of metrics may not be desirable. This code shows how we handle that (ymmv).

I hope this is helpful and I’m definitely open to comments, questions, and feedback.

   def getRoutes: HttpRoutes[IO] =
      val metricsServerEndpoint  : ServerEndpoint[Any, IO]       =
         prometheusMetrics.metricsEndpoint
            .prependSecurityIn("api") // / "metrics" is automatic
      val apiServerEndpoints     : List[ServerEndpoint[Any, IO]] = collectServerEndpoints
      val docEndpoints           : List[ServerEndpoint[Any, IO]] = SwaggerInterpreter(
         OpenAPIDocsOptions.default.copy(markOptionsAsNullable = true),
         swaggerUIOptions=SwaggerUIOptions.default.copy(pathPrefix=List("api", "swagger")))
         .fromServerEndpoints[IO](apiServerEndpoints, Config.title, Config.version)

      val descopeCookieSecurity: EndpointInput[Option[String]] =
         cookie[Option[String]]("<cookie name here>")
            .description("Session Cookie")
      val authenticationErrorOutput: EndpointOutput[ErrorInfo] =
         oneOf[ErrorInfo](
            oneOfVariant(
               statusCode(sttp.model.StatusCode.Unauthorized)
                  .and(jsonBody[ErrorInfo])))
      val docEndpointsSecured    : List[ServerEndpoint[Any, IO]] =
         docEndpoints.map { (endpoint: ServerEndpoint[Any, IO]) =>
            endpoint
               .prependSecurityPure(descopeCookieSecurity, authenticationErrorOutput) {
                  (cookieOpt: Option[String]) => authenticateCookieStaff(cookieOpt).map((_: User) => ())
               }
         }
      val metricsEndpoints       : List[ServerEndpoint[Any, IO]] = List(metricsServerEndpoint)

      // Interpret the endpoints
      // We capture metrics for apiEndpoints and don't for metrics and docs
      val apiInterpreter: Http4sServerInterpreter[IO] = Http4sServerInterpreter[IO](
         Http4sServerOptions
            .customiseInterceptors[IO]
            .metricsInterceptor(prometheusMetrics.metricsInterceptor())
            .exceptionHandler(ExceptionHandler.pure[IO] { (ctx: ExceptionContext) =>
               // ctx.e is the Throwable
               // ctx.request is the ServerRequest
               infoAndQueue(ctx.e, "An unexpected error occurred while handling a request")
               // Return a 500 Internal Server Error response
               Some(ValuedEndpointOutput(statusCode, StatusCode.InternalServerError))
            })
            .options)

      val defaultInterpreter: Http4sServerInterpreter[IO] = Http4sServerInterpreter[IO](
         Http4sServerOptions.customiseInterceptors[IO]
            .exceptionHandler(ExceptionHandler.pure[IO] { (ctx: ExceptionContext) =>
               infoAndQueue(ctx.e, "An unexpected error occurred while handling a request")
               Some(ValuedEndpointOutput(statusCode, StatusCode.InternalServerError))
            })
            .options)

      val apiRoutes   : HttpRoutes[IO] = apiInterpreter.toRoutes(apiServerEndpoints)
      val docRoutes   : HttpRoutes[IO] = defaultInterpreter.toRoutes(docEndpointsSecured)
      val metricRoutes: HttpRoutes[IO] = defaultInterpreter.toRoutes(metricsEndpoints)
      apiRoutes combineK docRoutes combineK metricRoutes

I think this looks fine :slight_smile: One simplification could be to have a single interpreter, and use prometheusMetrics.metricsInterceptor(ignoredEnpoints)variant. The ignored endpoints here would be the metrics endpoints.

I’d be happy to include an example in Tapir’s examples, but I think I would need a more detailed description of the original problem - and what’s the single thing that such an example could illustrate.

Btw.: ServerEndpoint is just an Endpoint with server logic. You can get the endpoint having a ServerEndpoint using _.endpoint.