domaindrivendev / Swashbuckle.WebApi

Seamlessly adds a swagger to WebApi projects!
BSD 3-Clause "New" or "Revised" License
3.07k stars 678 forks source link

How to restrict access to swagger/* folder? #384

Closed gabriel-a closed 8 years ago

gabriel-a commented 9 years ago

Greetings everyone,

I was wondering if someone found a way to restrict access to swagger/* folder, I tried DelegatingHandler as mentioned in https://github.com/domaindrivendev/Swashbuckle/issues/334 but I could not succeed. Also I tried to add location in web.config for swagger, it didn't work as well.

Anyone has any idea how to restrict access to documentation if the user is not authenticated?

gabriel-a commented 9 years ago

Found a pretty solution:

Created new folder: swagger Added new Web.config file.

<configuration> <system.web> <authorization> <deny users="?" /> </authorization> </system.web> <system.webServer> <modules runAllManagedModulesForAllRequests="true" /> </system.webServer> </configuration>

If you have the authentication in MVC project, then the user have to be logged in to view the documentation.

Thanks:)

JohnGalt1717 commented 8 years ago

Obviously this doesn't work if you're using OWIN or not using built in authentication. It would be really nice if there was a way to do the equivalent of [Authorize] at the top of the controller in a line of code in the config. Obviously using a Delegate handler is possible but it's a brute force approach to what should be a simple solution.

jptrueblood commented 8 years ago

Any solution? I am using OWIN, and am looking for a way to hide/secure the swagger ui from the general public, but am coming up short.

I also have to say, it took some doing to configure for OWIN, but once I had Swashbuckle up and running, I am amazed! Truly an incredibly useful utility for documenting and testing Web API implementations.

jptrueblood commented 8 years ago

Thanks jreames9,

Great idea!

I had a similar thought, and will probably go with this solution in the short term.

However, it would be nice to have this functionality in production for troubleshooting, but this resource would definitely need to be a protected resource.

mvarblow commented 8 years ago

I see the issue is closed, but I don't see the solution for those of us running under OWIN. Did I miss it?

domaindrivendev commented 8 years ago

As suggested - a DelegatingHandler is the easiest way to do this and should work with or without OWIN. See the example below which I've successfully tested with "Forms Authentication":

public class SwaggerAccessMessageHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (IsSwagger(request) && !Thread.CurrentPrincipal.Identity.IsAuthenticated)
        {
            var response = request.CreateResponse(HttpStatusCode.Unauthorized);
            return Task.FromResult(response);
        }
        else
        {
            return base.SendAsync(request, cancellationToken);
        }
    }

    private bool IsSwagger(HttpRequestMessage request)
    {
        return request.RequestUri.PathAndQuery.StartsWith("/swagger");
    }
}

Wire up the handler in your SwaggeConfig.cs just before enabling Swagger as follows:

httpConfig.MessageHandlers.Add(new SwaggerAccessMessageHandler());

httpConfig.EnableSwagger(c =>
{
    ...
});
figuerres commented 8 years ago

thank you for the example and as soon as I can I will try it out in my setup and let you know if it works. much appreciated !

figuerres commented 8 years ago

just tried this change and there is an issue I have. to add the httpconfig inside the swaggerconfig.Register() method I need to pass in the httpconfiguration if this is to work like other .register() methods. this throws a runtime error for me.

checking to see how to solve or if I made an error.

figuerres commented 8 years ago

Ahhh, ok the sample should read like this: GlobalConfiguration.Configuration.MessageHandlers.Add(new SwaggerAccessMessageHandler()); not like this: httpConfig.MessageHandlers.Add(new SwaggerAccessMessageHandler());

reason: the default swagger nugget package uses the "GlobalConfiguration.Configuration" not "httpConfig"

I am now getting a 401 when I try to get the swagger folder. I am using Identity Server V3 so now I just have to see how to get it to have me authenticate and i'll be good to go.

interestingly the swashbuckler / swagger setup is using Identity Server to allow access to the actual api calls in the swagger pages... now I just need to have it do that before I get to the swagger page. may just need to setup a login page or something....

up2pixy commented 8 years ago

@figuerres , have you get it setup successfully? In my case, the Thread.CurrentPrincipal.Identity.IsAuthenticated always return false.. I call the swagger UI like this:

HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/swagger/ui/index" }, WsFederationAuthenticationDefaults.AuthenticationType);

I also tried adding following part in Global.asax.cs but still not working...

protected void Application_PostAuthenticateRequest()
        {
            if (Request.IsAuthenticated)
            {
                Thread.CurrentPrincipal = HttpContext.Current.User;
            }
        }

Please help @domaindrivendev

mdhalgara commented 8 years ago

@domaindrivendev - the DelegationHandler sample code you provided works for me. Thanks! I am using IdentityServer3 + Asp.Net Identity on a Web API 2 solution.

lolekjohn commented 8 years ago

I figured out the way to do this. Use the latest swashbuckle version and add the below div tag in the injected index.html

<div id='auth_container'></div>

This will show an Authorize button in the swagger UI which can be used for authentication and once Authenticated, for all the requests to the API, the JWT token will be passed from the swagger UI

chadwackerman commented 7 years ago

@domaindrivendev I reviewed the numerous issues here as well as posts on StackOverflow. The reason for the spotty "solutions" comes from the overly complicated ASP.NET pipeline and legacy crap lurking in web.configs.

You're adding HttpModules to an Web API project. This breaks the convention below. Like many others, I was surprised to see the /swagger endpoints magically ignore all attempts at securing them.

config.SuppressHostPrincipal();
config.Filters.Add(new AuthorizeAttribute());

The next problem comes from your code which you tested via Forms Authentication. This is outdated magic that happens at the front of the ASP.NET routing chain. Like the static files nonsense, here be dragons.

Similarly the DelegatingHandler and DocumentFilter code you wrote doesn't apply in many scenarios. These filters run before AuthorizationFilters so authorization hasn't happened and the Principal isn't filled in. (Forms Authentication hides this from you.)

And having spent about six hours figuring out these simple truths, I do not blame you one bit for not being aware of it. This whole thing (and especially the slightly different interfaces for MVC and Web API handlers that still linger) remain an utter disaster.

I don't know how you want to handle this architecturally. I'd be happy to just add the routes myself, setting whatever paths and authentication I desire, at which point you'd be at the right point of the chain. That may raise the issue that those controllers then appear in the docs, which I'm sure some people would like and some people would not.

figuerres commented 7 years ago

Seems like the best path should be owin / katana as that is what Web api uses and does not get into the old Web forms and isapi mess. . Just my thought.

chadwackerman commented 7 years ago

I understand why he used a HttpModule (it keeps stuff out of the Web API namespace). Besides, depending on what year they first created their project, who knows what web gunk people are running.

For authentication purposes, creating your own HttpModule would seem to solve it regardless of what legacy path is at play. (Though I wouldn't wager on it.)

To get started add the Hexasoft.BasicAuthentication package to get the warm fuzzy feeling of seeing a handler actually run ahead of the swagger endpoints.

Beyond that, you can swipe the code from the top of this routine and rig up what you need: https://github.com/hexasoftuk/Hexasoft.BasicAuthentication/blob/master/Hexasoft.BasicAuthentication/Hexasoft.BasicAuthentication/BasicAuthentication.cs

It's ugly but it works. @domaindrivendev please put this in the README at least?

mguinness commented 7 years ago

In .NET Core you use middleware, instead of a DelegatingHandler:

public class SwaggerAuthorizedMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        if (context.Request.Path.StartsWithSegments("/swagger")
            && !context.User.Identity.IsAuthenticated)
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            return;
        }

        await _next.Invoke(context);
    }
}

You will also need an extension method to help adding to pipeline:

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

Then add to Configure method in Startup.cs just before using Swagger:

app.UseSwaggerAuthorized();
app.UseSwagger();
app.UseSwaggerUi();
dbrennan commented 7 years ago

@chadwackerman, sure it works, but installing Hexasoft.BasicAuthentication applies Basic Authentication across my site. I tried creating a swagger subdirectory with a web.config to enable this module only for swagger, but IIS gets in the way and when it sees a swagger directory it no longer invokes the swagger module and gives the "listing access denied" page instead of the swagger documentation. Therefore this doesn't look like a great solution unless there is another way to enable basic auth only for the swagger path.

chadwackerman commented 7 years ago

@cptndave I posted it as a quick example of getting anything to run ahead of Swagger. There's probably a way to do it with web.config but I'd just modify the code to look at the request url instead.

Structed commented 7 years ago

Is there also a way to secure the API docs (eg /swagger) with BasicAuth, while the actual API requires JWT auth? We have the situation where we secure the application with JWT via IdentityServer4, but want the API Docs to be independently secured.

sashafencyk commented 7 years ago

@chadwackerman so, is there some right solution to protect subdirectory ? (with Basic Auth)

rwatjen commented 7 years ago

@mguinness Thanks for that solution.

Additionally, if the site uses OpenIdConnect authentication, this line in the SwaggerAuthorizedMiddleware class:

context.Response.StatusCode = StatusCodes.Status401Unauthorized;

can successfully be replaced with

await context.ChallengeAsync();

This works by invoking the DefaultChallengeScheme configured with services.AddAuthentication in Startup.cs, and will trigger the OpenIdConnect login flow.

mihaj commented 6 years ago

@Structed I also want that. Any solutions?

Structed commented 6 years ago

@mihaj No, not really. We ended up turning off swagger docs in prod for now, until we open up the API to customers. We'll probably go a different route from there and have a central API gateway instead.

mihaj commented 6 years ago

I only need swagger in development/staging, but still would like to password protect it with minimal effort. The above solution is ok, but I need to create manual HTML to prompt the user to login to Oauth provider.

jeffvella commented 6 years ago

I tried @mguinness solution but context.User.Identity.IsAuthenticated is always returning false for me :( (Core.All 2.05). Cookies are enabled, login is fine, other MVC pages show authenticated, token based requests authenticate. yeah. kinda lost.

mguinness commented 6 years ago

@imxzjv The order of middleware is important, check that app.UseAuthentication() occurs before your swagger config.

hengyiliu commented 6 years ago

We have a Web API project which is secured by JwtBearer auth. I tried @mguinness solution, and User.Identity.IsAuthenticated is always false because the web app doesn't have a way to login.

Is there a way to configure WebAPI project to use JwtBearer auth for everything, but AzureAD/OpenIDConnect auth for /swagger path? I tried the following, but couldn't get it work. The error "No IAuthenticationSignInHandler is configured to handle sign in for the scheme: Bearer"

services.AddAuthentication(options =>
{
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
 {
     options.Authority = "https://login.microsoftonline.com/...";
 })
.AddAzureAd(options =>
 {
     options.Instance = "https://login.microsoftonline.com/";
     // from https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore
 });

and

app.UseAuthentication();
app.UseSwaggerAuthorized();
app.UseSwagger();
Thwaitesy commented 6 years ago

I have enhanced @mguinness solution to use a very simple Basic Auth for only the swagger paths. Basically we wanted the swagger stuff to be hidden in prod, unless you enter a known/shared username/password. This solution does just that, it pops up asking for auth details, which if correct lets you view the swagger stuff. - It also skips the authentication locally for dev.

I've copied the basic auth code from here: https://www.johanbostrom.se/blog/adding-basic-auth-to-your-mvc-application-in-dotnet-core

Please note - I haven't tested it with oAuth authentication turned on for swagger... this most likely will overwrite the basic auth header and stop you accessing swagger... You could probably enhance it then to also check if the request is authenticated via oAuth.. etc.

Anyways, its simple and gets the job done.

public class SwaggerBasicAuthMiddleware
{
    private readonly RequestDelegate next;

    public SwaggerBasicAuthMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        //Make sure we are hitting the swagger path, and not doing it locally as it just gets annoying :-)
        if (context.Request.Path.StartsWithSegments("/swagger") && !this.IsLocalRequest(context))
        {
            string authHeader = context.Request.Headers["Authorization"];
            if (authHeader != null && authHeader.StartsWith("Basic "))
            {
                // Get the encoded username and password
                var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim();

                // Decode from Base64 to string
                var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword));

                // Split username and password
                var username = decodedUsernamePassword.Split(':', 2)[0];
                var password = decodedUsernamePassword.Split(':', 2)[1];

                // Check if login is correct
                if (IsAuthorized(username, password))
                {
                    await next.Invoke(context);
                    return;
                }
            }

            // Return authentication type (causes browser to show login dialog)
            context.Response.Headers["WWW-Authenticate"] = "Basic";

            // Return unauthorized
            context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
        }
        else
        {
            await next.Invoke(context);
        }
    }

    public bool IsAuthorized(string username, string password)
    {
        // Check that username and password are correct
        return username.Equals("SpecialUser", StringComparison.InvariantCultureIgnoreCase)
                && password.Equals("SpecialPassword1");
    }

    public bool IsLocalRequest(HttpContext context)
    {
        //Handle running using the Microsoft.AspNetCore.TestHost and the site being run entirely locally in memory without an actual TCP/IP connection
        if (context.Connection.RemoteIpAddress == null && context.Connection.LocalIpAddress == null)
        {
            return true;
        }
        if (context.Connection.RemoteIpAddress.Equals(context.Connection.LocalIpAddress))
        {
            return true;
        }
        if (IPAddress.IsLoopback(context.Connection.RemoteIpAddress))
        {
            return true;
        }
        return false;
    }
}
public static class SwaggerAuthorizeExtensions
{
    public static IApplicationBuilder UseSwaggerAuthorized(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<SwaggerBasicAuthMiddleware>();
    }
}

Startup.cs

app.UseAuthentication(); //Ensure this like is above the swagger stuff

app.UseSwaggerAuthorized();
app.UseSwagger();
app.UseSwaggerUI(
bcpi commented 6 years ago

@Thwaitesy, thanks for the code. It seems to only work on Firefox. Keep getting auth prompts on Safari, Chrome, and Edge. Any ideas why?

-- update: seems to have been an issue with IIS setup. now working. thx

Thwaitesy commented 6 years ago

@bcpi id start by debugging the auth header check.. if its coming through there then I have no idea why its not working.. But if it's not coming through there then something is striping the auth header out of the request... I've only tested this in chrome, but will try others and see what the results are..

jsantanders commented 6 years ago

Hi @Thwaitesy I tried your solution but I always get 401 Unauthorized.

Thwaitesy commented 6 years ago

@jsantanders if you give me some more details I might be able to help?

It's been working great for us in all browsers....

Have you debugged it to see if its getting into the check login part? and its successful?

Outside of this, its possible some other auth is affecting the outcome.

sbrown345 commented 5 years ago

As suggested - a DelegatingHandler is the easiest way to do this and should work with or without OWIN. See the example below which I've successfully tested with "Forms Authentication":

public class SwaggerAccessMessageHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (IsSwagger(request) && !Thread.CurrentPrincipal.Identity.IsAuthenticated)
        {
            var response = request.CreateResponse(HttpStatusCode.Unauthorized);
            return Task.FromResult(response);
        }
        else
        {
            return base.SendAsync(request, cancellationToken);
        }
    }

    private bool IsSwagger(HttpRequestMessage request)
    {
        return request.RequestUri.PathAndQuery.StartsWith("/swagger");
    }
}

Wire up the handler in your SwaggeConfig.cs just before enabling Swagger as follows:

httpConfig.MessageHandlers.Add(new SwaggerAccessMessageHandler());

httpConfig.EnableSwagger(c =>
{
    ...
});

I had to do: return request.RequestUri.PathAndQuery.StartsWith("/swagger", StringComparison.OrdinalIgnoreCase); instead because I could bypass it by going to /SWAGGER

ikshvakoo commented 5 years ago

@sbrown345 , I'm trying to accomplish the same thing for the swagger specification that I'm generating using Swashbuckle and I'm not on .Net core. I'm on .Net Framework 4.7.1

Your code above returns 401 - Unauthorized response.. Which is technically fine. How did you manage to have the user enter the necessary credentials? Did you manage to pop open a user credentials pop-up on the browser so that the user can enter the username and password?

Thanks, Vinay

shanchin2k commented 5 years ago

I have below code for protecting the API's by using Azure AD B2C

services.AddAuthentication(options =>
            {
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;                
            })
                    .AddJwtBearer(jwtOptions =>
                    {
                        jwtOptions.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
                        {
                            // Accept only those tokens where the audience of the token is equal to the client ID of this application
                            ValidAudience = Configuration["AzureAdB2C:ClientId"],
                            AuthenticationType = Configuration["AzureAdB2C:Policy"]
                        };                       
                        jwtOptions.Authority = $"https://login.microsoftonline.com/tfp/{Configuration["AzureAdB2C:Tenant"]}/{Configuration["AzureAdB2C:Policy"]}/v2.0/";
                        jwtOptions.Audience = Configuration["AzureAdB2C:ClientId"];
                        jwtOptions.Events = new JwtBearerEvents
                        {
                            OnAuthenticationFailed = AuthenticationFailed                           
                        };

                    });

With the SwaggerAuthorizedMiddleware as @rwatjen posted. The code inside the middleware is like below:

public async Task Invoke(HttpContext context)
        {
            if (context.Request.Path.StartsWithSegments("/swagger")
                && !context.User.Identity.IsAuthenticated)
            {                
                await context.ChallengeAsync("Bearer");

                return;
            }

            await _next.Invoke(context);
        }

The flow is not popping up the login page but always bringing 401 state. It hits the What am I missing? Should sign-in scheme causing issue? @Thwaitesy

gtaylor44 commented 5 years ago

@Thwaitesy provided an excellent answer for .NET core.

Here's an adapted solution for ASP.NET using DelegatingHandler

  public class SwaggerAccessMessageHandler : DelegatingHandler
  {
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
      if (IsSwagger(request) && !request.IsLocal())
      {
        IEnumerable<string> authHeaderValues = null;

        request.Headers.TryGetValues("Authorization", out authHeaderValues);
        var authHeader = authHeaderValues?.FirstOrDefault();

        if (authHeader != null && authHeader.StartsWith("Basic "))
        {
          // Get the encoded username and password
          var encodedUsernamePassword = authHeader.Split(' ')[1]?.Trim();

          // Decode from Base64 to string
          var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword));

          // Split username and password
          var username = decodedUsernamePassword.Split(':')[0];
          var password = decodedUsernamePassword.Split(':')[1];

          // Check if login is correct
          if (IsAuthorized(username, password))
          {
            return await base.SendAsync(request, cancellationToken);
          }
        }

        var response = request.CreateResponse(HttpStatusCode.Unauthorized);
        //response.Headers.Location = new Uri("http://www.google.com.au");
        response.Headers.Add("WWW-Authenticate", "Basic");

        return response;
      }
      else
      {
        return await base.SendAsync(request, cancellationToken);
      }
    }

    public bool IsAuthorized(string username, string password)
    {
      // Check that username and password are correct
      return username.Equals("SpecialUser", StringComparison.InvariantCultureIgnoreCase)
              && password.Equals("SpecialPassword1");
    }

    private bool IsSwagger(HttpRequestMessage request)
    {
      return request.RequestUri.PathAndQuery.StartsWith("/help");
    }
  }
jarikai commented 3 years ago

I made a small change to code to redirect in login page:

context.Response.StatusCode = StatusCodes.Status307TemporaryRedirect;
context.Response.Headers.Add("Location", "/Identity/Account/Login?returnUrl=/swagger");

I'm using .net5.0 preview with Identity.

jg11jg commented 3 years ago

see https://stackoverflow.com/a/65094653/6795110 for how I got it working using Swashbuckle and OpenIdConnect.

P47K0 commented 3 years ago

@Thwaitesy

Why don't do the check for development mode in the Startup.cs? Then you don't need the IsLocalRequest:

            app.UseAuthentication();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();                
            } else
            {
                app.UseSwaggerAuthorized();                
            }

            app.UseSwagger();
            app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "endpoint v1"));
rbiq commented 3 years ago

I came up with a super simple way to extend this using query string for those who dont wish to go the authentication route. Have confirmed this works with localhost as well as dns hostname

public bool IsLocalRequest(HttpContext context) { //Handle running using the Microsoft.AspNetCore.TestHost and the site being run entirely locally in memory without an actual TCP/IP connection if (context.Connection.RemoteIpAddress == null && context.Connection.LocalIpAddress == null) { return true; }

        if (context.Connection.RemoteIpAddress.Equals(context.Connection.LocalIpAddress))
        {
            return true;
        }

        if (IPAddress.IsLoopback(context.Connection.RemoteIpAddress))
        {
            return true;
        }

        if (context.Request.Query.ContainsKey("secure"))
        {
            if (context.Request.Query["dbg"] == "yes") //set whatever value you want here
            {
                return true;
            }
        }

        var files = new HashSet<string>
            {
                "swagger-ui-standalone-preset.js"
                ,"swagger-ui-bundle.js"
                ,"swagger-ui-bundle.js"
                ,"swagger-ui-standalone-preset.js"
                ,"swagger-ui.css"
                ,"favicon-32x32.png"
                ,"favicon-16x16.png"
                ,"swagger.json"
            };

        if (files.Any(f => context.Request.Path.Value.Contains(f)))
        {
            return true;
        }

        return false;
    }
idvlop commented 2 years ago

Hello, try this :)

public class SwaggerAuthorizedMiddleware
{
    private readonly RequestDelegate _next;
    public IAuthenticationSchemeProvider Schemes { get; set; }

    public SwaggerAuthorizedMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
        Schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Path.StartsWithSegments("/docs"))
        {
            context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
            {
                OriginalPath = context.Request.Path,
                OriginalPathBase = context.Request.PathBase
            });

            var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
            foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
            {
                var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
                if (handler != null && await handler.HandleRequestAsync())
                {
                    return;
                }
            }

            var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
            if (defaultAuthenticate != null)
            {
                var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
                if (result?.Principal != null)
                {
                    context.User = result.Principal;
                }
                else
                {
                    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    return;
                }
            }
        }

        await _next(context);
    }
}
janseris commented 2 years ago

How to customize this to accept boolean "allow unauthenticated access from local connection for production" and accept username and password as parameters in StartUp.cs/Program.cs? Edit: do it probably like this: https://stackoverflow.com/questions/40876333/how-do-i-usemiddlewaretype-and-pass-in-options#answer-49045033

mihaj commented 2 years ago

How to customize this to accept boolean "allow unauthenticated access from local connection for production" and accept username and password as parameters in StartUp.cs/Program.cs? Edit: do it probably like this: https://stackoverflow.com/questions/40876333/how-do-i-usemiddlewaretype-and-pass-in-options#answer-49045033

We are doing it like this. In a Startup.cs file under Configure method, you can have a section like the one below.

Show Swager UI only if DEV env (or any other). Additionally, protect it with Basic Authentication. You can also make basic auth optional.

if (env.IsDevelopment())
{
    app.MapWhen(context => context.Request.Path.ToString().Contains("/swagger"), swaggerApp =>
    {
        swaggerApp.UseMiddleware<BasicAuthMiddleware>(
            Configuration.GetValue<string>($"Swagger:Username"),
            Configuration.GetValue<string>($"Swagger:Password"));

        swaggerApp.UseSwagger();
        swaggerApp.UseSwaggerUI(c =>
        {
            foreach (var description in provider.ApiVersionDescriptions.OrderByDescending(o => o.GroupName))
            {
                c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",
                    description.GroupName.ToUpperInvariant());
                c.OAuthClientId(configuration["Swagger:ClientId"]);
            }
        });
    });
}
lixjia commented 2 days ago

Adding an enhancement to @mguinness solution by remove the cache from session after closing the browser and requiring reauthorization to login again to view swagger, it requires a few lines in the SwaggerBasicAuthMiddleware class.

public class SwaggerBasicAuthMiddleware
{
    private readonly RequestDelegate next;

    public SwaggerBasicAuthMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        //Make sure we are hitting the swagger path, and not doing it locally as it just gets annoying :-)
        if (context.Request.Path.StartsWithSegments("/swagger") && !this.IsLocalRequest(context))
        {
           ....
                // Split username and password
                var username = decodedUsernamePassword.Split(':', 2)[0];
                var password = decodedUsernamePassword.Split(':', 2)[1];

                //remove cache
                context.Request.Headers.Remove("Authorization");
                context.Response.GetTypedHeaders().CacheControl =
                new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
                {
                    Public = false,
                    MaxAge = TimeSpan.FromSeconds(0)
                };

                // Check if login is correct
                if (IsAuthorized(username, password))
                {
                    await next.Invoke(context);
                    return;
                }
            }

            ....
        }
}