Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
gabriel-a opened this issue Jun 10, 2015 · 37 comments
Closed

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

gabriel-a opened this issue Jun 10, 2015 · 37 comments

Comments

@gabriel-a
Copy link

@gabriel-a gabriel-a commented Jun 10, 2015

Greetings everyone,

I was wondering if someone found a way to restrict access to swagger/* folder, I tried DelegatingHandler as mentioned in #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
Copy link
Author

@gabriel-a gabriel-a commented Jun 12, 2015

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
Copy link

@JohnGalt1717 JohnGalt1717 commented Dec 18, 2015

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
Copy link

@jptrueblood jptrueblood commented Dec 22, 2015

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
Copy link

@jptrueblood jptrueblood commented Dec 22, 2015

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
Copy link

@mvarblow mvarblow commented Apr 1, 2016

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
Copy link
Owner

@domaindrivendev domaindrivendev commented Jul 7, 2016

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
Copy link

@figuerres figuerres commented Jul 8, 2016

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
Copy link

@figuerres figuerres commented Jul 11, 2016

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
Copy link

@figuerres figuerres commented Jul 11, 2016

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
Copy link

@up2pixy up2pixy commented Jul 25, 2016

@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
Copy link

@mdhalgara mdhalgara commented Sep 26, 2016

@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
Copy link

@lolekjohn lolekjohn commented Oct 7, 2016

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
Copy link

@chadwackerman chadwackerman commented Nov 27, 2016

@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
Copy link

@figuerres figuerres commented Nov 27, 2016

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
Copy link

@chadwackerman chadwackerman commented Nov 27, 2016

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
Copy link

@mguinness mguinness commented Jan 12, 2017

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
Copy link

@dbrennan dbrennan commented Apr 17, 2017

@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
Copy link

@chadwackerman chadwackerman commented Apr 19, 2017

@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
Copy link

@Structed Structed commented Aug 22, 2017

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
Copy link

@sashafencyk sashafencyk commented Aug 29, 2017

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

@rwatjen
Copy link

@rwatjen rwatjen commented Oct 3, 2017

@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
Copy link

@mihaj mihaj commented Mar 22, 2018

@Structed I also want that. Any solutions?

@Structed
Copy link

@Structed Structed commented Mar 22, 2018

@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
Copy link

@mihaj mihaj commented Mar 24, 2018

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
Copy link

@jeffvella jeffvella commented Mar 27, 2018

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
Copy link

@mguinness mguinness commented Mar 27, 2018

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

@hengyiliu
Copy link

@hengyiliu hengyiliu commented Apr 25, 2018

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
Copy link

@Thwaitesy Thwaitesy commented Aug 3, 2018

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
Copy link

@bcpi bcpi commented Aug 6, 2018

@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
Copy link

@Thwaitesy Thwaitesy commented Aug 7, 2018

@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
Copy link

@jsantanders jsantanders commented Aug 11, 2018

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

@Thwaitesy
Copy link

@Thwaitesy Thwaitesy commented Aug 11, 2018

@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
Copy link

@sbrown345 sbrown345 commented Apr 28, 2019

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
Copy link

@ikshvakoo ikshvakoo commented May 13, 2019

@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
Copy link

@shanchin2k shanchin2k commented Aug 22, 2019

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
Copy link

@gtaylor44 gtaylor44 commented Aug 30, 2019

@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
Copy link

@jarikai jarikai commented Nov 5, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.