Getting NSwag to work behind reverse proxy

Getting NSwag to work behind reverse proxy

I've been working with Swagger through NSwag a lot recently and also needed to get it to work when hidden behind a reverse proxy - i.e. another service forwarding a request to the service exposing the Swagger UI.

Some of the issues I've had along the way:

  • Not working at all...
  • "Try it out" not working because:

    • HTTP/HTTPS not being picked up
    • Document being cached by different host or scheme meaning the opposite would not work

    It wasn't that straight forward but through a lot of googling and reading GitHub issues I got a working solution put together.

In my specific case I was running ASP.NET Core 3.1 in a linux docker container in Azure, which some of the solution might be colored by.

If I remember correctly the most important parts are the X-Forwarded-Host and X-Forwarded-PathBase HTTP headers, so it's important that these are forwarded from your proxy to your Swagger service.

public class Startup
{
    // ...

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ...

        app.UseXForwardedHeaders();
        app.UseSwaggerWithReverseProxySupport();
    }
}

static class StartupExtensions
{
    public static IApplicationBuilder UseXForwardedHeaders(this IApplicationBuilder app)
    {
        var options = new ForwardedHeadersOptions
        {
            ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
        };

        // Only loopback proxies are allowed by default.
        // Clear that restriction because forwarders are being enabled by explicit configuration.
        // https://stackoverflow.com/a/56469499/5358985
        options.KnownNetworks.Clear();
        options.KnownProxies.Clear();

        app.UseForwardedHeaders(options);

        app.Use((context, next) =>
        {
            if (context.Request.Headers.TryGetValue("X-Forwarded-PathBase", out var pathBases))
            {
                context.Request.PathBase = pathBases.First();
            }
            return next();
        });

        return app;
    }

    public static IApplicationBuilder UseSwaggerWithReverseProxySupport(this IApplicationBuilder app)
    {
        app.UseOpenApi(config =>
        {
            // Without this the document will be cached with wrong URLs 
            // and not work if later accessed from another host/path/scheme
            config.CreateDocumentCacheKey = request => request.Headers["X-Forwarded-Host"].FirstOrDefault() +
                                                        request.Headers["X-Forwarded-PathBase"].FirstOrDefault() +
                                                        request.IsHttps;

            // Change document host and base path from headers (if set)
            config.PostProcess = (document, request) =>
            {
                document.BasePath = request.Headers["X-Forwarded-PathBase"].FirstOrDefault();
                document.Host = request.Headers["X-Forwarded-Host"].FirstOrDefault();
            };
        });

        app.UseSwaggerUi3(settings =>
        {
            settings.TransformToExternalPath = (route, request) =>
            {
                var pathBase = request.Headers["X-Forwarded-PathBase"].FirstOrDefault();
                return !string.IsNullOrEmpty(pathBase)
                    ? $"{pathBase}{route}"
                    : route;
            };
        });

        return app;
    }
}