'How to protect swagger endpoint in .NET Core API?

I have an api build in .net core 2.1. To restrict access to various endpoints, I use IdentityServer4 and [Authorize] attribute. However, my goal during development is to expose the api swagger documentation to our developers so that they may use it no matter where they work from. The challenge that I face is how do I protect the swagger index.html file so that only they can see the details of the api.

I have created a custom index.html file in the wwwroot/swagger/ui folder and that all works, however, that file uses data from /swagger/v1/swagger.json endpoint which is not protected. I would like to know how can I override the return value for that specific endpoint so that I may add my own authentication to it?

EDIT:

Currently, I have achieved the above with the following middleware:

public class SwaggerInterceptor
{
    private readonly RequestDelegate _next;

    public SwaggerInterceptor(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        var uri = context.Request.Path.ToString();
        if (uri.StartsWith("/swagger/ui/index.html"))
        {
            var param = context.Request.QueryString.Value;

            if (!param.Equals("?key=123"))
            {
                context.Response.StatusCode = 404;
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsync("{\"result:\" \"Not Found\"}", Encoding.UTF8);
                return;
            }
        }

        await _next.Invoke(context);
    }
}

public class Startup 
{
    //omitted code

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseMiddleware<SwaggerInterceptor>();
        //omitted code
    }
}

What I don't like about this approach as it will inspect every single request. Is there a better way to achieve this? The above only protects the index.html file, but I can adjust it to protect the json endpoint in the similar fashion.



Solution 1:[1]

You can choose some options:

  • basic authorization
  • OpenId Connect authorization using identity server

Basic Authorization

In this case you just close your swagger endpoints.

// Startup.cs
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddAuthentication()
            .AddScheme<BasicAuthenticationOptions, BasicAuthenticationHandler>("Basic", _ => {});
        ...  
    }

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

        app.UseEndpoints(endpoints =>
        {
            ...
            
            var pipeline = endpoints.CreateApplicationBuilder().Build();
            var basicAuthAttr = new AuthorizeAttribute { AuthenticationSchemes = "Basic" };
            endpoints
                .Map("/swagger/{documentName}/swagger.json", pipeline)
                .RequireAuthorization(basicAuthAttr);
            endpoints
                .Map("/swagger/index.html", pipeline)
                .RequireAuthorization(basicAuthAttr);
        });
    }
}

// BasicAuthenticationHandler.cs
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
    ...
}

OIDC Authorization Using IdentityServer4

I have written the article for this case: https://medium.com/dev-genius/csharp-protecting-swagger-endpoints-82ae5cfc7eb1

Solution 2:[2]

Here it is using OpenIdConnect and Swashbuckle in Asp.Net Core 3.1. Now, if I type https://myurl.com/swagger I get routed to my normal login page and after logging in successfully, I can see the swagger.

public class Startup
{ 
    //<snip/>

    public void Configure(IApplicationBuilder app)
    { 
      //<snip/>

        app.UseAuthentication();

        app.UseAuthorization();

        app.UseSwagger();

        app.UseSwaggerUI(c => { c.SwaggerEndpoint("v1/swagger.json", "Some name"); });

        app.UseEndpoints(routes =>
        {
            var pipeline = routes.CreateApplicationBuilder().Build();
            
            routes.Map("/swagger", pipeline).RequireAuthorization(new AuthorizeAttribute {AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme});
            routes.Map("/swagger/index.html", pipeline).RequireAuthorization(new AuthorizeAttribute {AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme});
            routes.Map("/swagger/v1/swagger.json", pipeline).RequireAuthorization(new AuthorizeAttribute { AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme });
            routes.Map("/swagger/{documentName}/swagger.json", pipeline).RequireAuthorization(new AuthorizeAttribute { AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme });

            routes.MapDefaultControllerRoute();
        });
     }
}

EDIT: Somehow I thought the below was working, but when I retested it later it turned out that actually it was giving error: The request reached the end of the pipeline without executing the endpoint. So, I changed to include a fixed set of endpoints under /swagger that contain afaik the key data.

routes.Map("/swagger/{**any}", pipeline).RequireAuthorization(new AuthorizeAttribute {AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme});

Note: this {**any} part of the route template protects all files under /swagger as well, so for example /swagger/index.html, /swagger/v1/swagger.json etc etc.

Solution 3:[3]

I believe your best option is what you already did. Build your own middleware, as I don't know any middleware for validate authentication on static files. You could add the basePath to avoid enter at this specific middleware when its not necessary. Like the code below

app.Map("/swagger", (appBuilder) =>
{
    appBuilder.UseMiddleware<SwaggerInterceptor>();
});

Also this article could help you to build a more generic middleware for validate authentication on static files. https://odetocode.com/blogs/scott/archive/2015/10/06/authorization-policies-and-middleware-in-asp-net-5.aspx

Solution 4:[4]

After some search I solved and it works for me. you must define a middle-ware class to authenticate the user whom wants to visit your swagger-ui view page like this.

public class SwaggerBasicAuthMiddleware
{
private readonly RequestDelegate next;
public SwaggerBasicAuthMiddleware(RequestDelegate next)
{
  this.next = next;
}
public async Task InvokeAsync(HttpContext context)
  {
   if (context.Request.Path.StartsWithSegments("/swagger"))
   {
    string authHeader = context.Request.Headers["Authorization"];
    if (authHeader != null && authHeader.StartsWith("Basic "))
    {
     // Get the credentials from request header
     var header = AuthenticationHeaderValue.Parse(authHeader);
     var inBytes = Convert.FromBase64String(header.Parameter);
     var credentials = Encoding.UTF8.GetString(inBytes).Split(':');
     var username = credentials[0];
     var password = credentials[1];
     // validate credentials
     if (username.Equals("Test") && password.Equals("Test"))
     {
      await next.Invoke(context).ConfigureAwait(false);
      return;
     }
    }
    context.Response.Headers["WWW-Authenticate"] = "Basic";
    context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
   }
   else
   {
    await next.Invoke(context).ConfigureAwait(false);
   }
  }
 }

For simplicity I am using hardcoded credentials but same can enhanced to use it from database also.

Also I have to create a extension method in a static class like this

public static class AuthorizedSampleClass
{
    public static IApplicationBuilder UseSwaggerAuthorized( this IApplicationBuilder builder )
    {
        return builder.UseMiddleware<SwaggerBasicAuthMiddleware>( );
    }
}

in startup.cs remove the env.IsDevelopment() part and add app.UseSwaggerAuthorized( ); right before app.UseSwagger( ); because authentication middleware will be called before accessing swagger ui.

app.UseSwaggerAuthorized( );
app.UseSwagger( );
app.UseSwaggerUI( c => c.SwaggerEndpoint( "/swagger/v1/swagger.json", "Broker.Rest v1" ) );

now press F5 and rout to swagger view, it has done. press "Test" and "Test" as username and password to enter.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Renat Sungatullin
Solution 2
Solution 3 Lutti Coelho
Solution 4 navidsoft