.NET-5 Hide swagger endpoints to unauthorized users
Asked Answered
C

5

6

I have a .NET 5 API using OpenApi.

Is it possible to hide all API endpoints in swagger but the login one until user is authorized with a JWT Bearer Token?

This is the code I use in startup.cs

services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo { 
                Title = "API", Version = "v1",
                Description = "API (.NET 5.0)",
                Contact = new OpenApiContact()
                {
                    Name = "Contact",
                    Url = null,
                    Email = "[email protected]"
                }
            });
            c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
            {
                Description = @"Autorización JWT utilizando el esquema Bearer en header. <br />
                  Introducir el token JWT generado por AuthApi.",
                Name = "Authorization",
                In = ParameterLocation.Header,
                Type = SecuritySchemeType.Http,
                Scheme = "Bearer"
            });
            c.AddSecurityRequirement(new OpenApiSecurityRequirement()
  {
    {
      new OpenApiSecurityScheme
      {
        Reference = new OpenApiReference
          {
            Type = ReferenceType.SecurityScheme,
            Id = "Bearer"
          },
          Scheme = "oauth2",
          Name = "Bearer",
          In = ParameterLocation.Header,

        },
        new List<string>()
      }
    });
        });
Charleton answered 25/1, 2021 at 10:20 Comment(2)
Did you find a solution for this? I have the same question.Parochialism
@Parochialism Yes, I ended up doing it based on appsettings.json parameters, but I posted the code. Hope it helps youCharleton
E
3

I managed to hide swagger endpoints before authentication by hacking a middleware to remove endpoints from swagger.json file for unauthenticated users and using swagger request/response interceptors to persist the received token and refresh the page after user login to re-fetch the swagger.json file.

I wrote the solution down here: https://medium.com/@milad665/hide-endpoints-in-swagger-ui-for-unauthenticated-users-4054a4e15b89

Everywhere answered 14/2, 2022 at 10:23 Comment(2)
I tried this @milad, and it sort of worked. When I hit authorize it refreshes the page and shows the paths but clears the authentication I configured with swagger. So I hit authorize again and I can never get it to inject my bearer token into API requests. I'm using OAuth2 security definition with OpenApiOAuthFlow. Any idea why?Mir
@Mir Are you sure you implemented the interceptors correctly? You need to make sure the token is stored correctly in the browser's storage and then injected into all request headers afterwards. To do this, you should make sure swagger's request and response interceptors are configured correctlyEverywhere
O
3

First create and add a new DocumentFilter, that strips all information from your swagger.json for unauthorised users. You can be very specific what to remove or keep, but this example simply strips all Endpoints and Schemas but keeps the Auth information which are required for authorisation.

public class RequireAuthenticationDocumentFilter : IDocumentFilter
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public RequireAuthenticationDocumentFilter(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        bool isAuthenticated =
            _httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated ?? false;
        
        if (!isAuthenticated)
        {
            swaggerDoc.Paths.Clear();
            context.SchemaRepository.Schemas.Clear();
        }
    }
}

Then add the RequireAuthenticationDocumentFilter. Your should now see no Endpoints or Schema in your swagger.json and therefore SwaggerUI.

services.AddSwaggerGen(options =>
{
    options.DocumentFilter<RequireAuthenticationDocumentFilter>();
}

Next step is to configure SwaggerUI to persist the Auth Token between page reloads. The RequestInterceptor (a JavaScript function you can inject) then uses the persisted token when requesting the swagger.json.

app.UseSwaggerUI(options =>
{
    options.EnablePersistAuthorization();
    options.UseRequestInterceptor(
        "(request) => {" +
        // "  debugger;" +
        "  if (!request.url.endsWith('swagger.json')) return request;" +
        "  var json = window.localStorage?.authorized;" +
        "  if (!json) return request;" +
        "  var auth = JSON.parse(json);" +
        "  var token = auth?.oauth2?.token?.access_token;" +
        "  if (!token) return request;" +
        "  request.headers.Authorization = 'Bearer ' + token;" +
        "  return request;" +
        "}");
}

Note, that the swagger.json is requested on SwaggerUI page load. After authorising via SwaggerUI you need to manually reload the page in order to request your swagger.json again, but this time with the persisted authorisation information.

When experiencing problems while checking the authentication in the RequireAuthenticationDocumentFilter, make sure that authentication and authorisation takes place, before adding Swagger and SwaggerUI to your ASP.NET Core Middleware Pipeline.

...
app.UseAuthentication();
app.UseAuthorization();
app.UseSwagger();
app.UseSwaggerUI();
...
Otha answered 27/7, 2022 at 7:19 Comment(3)
Thanks for this! one small comment it should be ´if (!isAuthenticated)´Tailstock
What are the settings in app.UseSwaggerUI(...) registration? Where it come from?Buyer
@Buyer - Some leftover code, unnecessary for this small example. Removed it for more clarity.Otha
N
2

You will need to implement your own middleware and check the endpoint path. If it starts with "/swagger" then you should challenge the authentication.

Below code authored by someone else here

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using System;

/// <summary>
/// The extension methods that extends <see cref="IApplicationBuilder" /> for authentication purposes
/// </summary>
public static class ApplicationBuilderExtensions
{
    /// <summary>
    /// Requires authentication for paths that starts with <paramref name="pathPrefix" />
    /// </summary>
    /// <param name="app">The application builder</param>
    /// <param name="pathPrefix">The path prefix</param>
    /// <returns>The application builder</returns>
    public static IApplicationBuilder RequireAuthenticationOn(this IApplicationBuilder app, string pathPrefix)
    {
        return app.Use((context, next) =>
        {
            // First check if the current path is the swagger path
            if (context.Request.Path.HasValue && context.Request.Path.Value.StartsWith(pathPrefix, StringComparison.InvariantCultureIgnoreCase))
            {
                // Secondly check if the current user is authenticated
                if (!context.User.Identity.IsAuthenticated)
                {
                    return context.ChallengeAsync();
                }
            }

            return next();
        });
    }
}

And then in your startup.cs (below sequence matters)

app.RequireAuthenticationOn("/swagger");
app.UseSwagger();
app.UseSwaggerUI();
Nealon answered 26/11, 2021 at 9:16 Comment(0)
C
1

I finally ended up hidding swagger enpoints using appsettings.json parameters, not exactly what I was asking for, but I'll post the solution in case it helps someone as it may work to filter logged users:

There are some commented blocks and unused code that may be useful for you, as it came with the example I found on the web.

Swagger ignore filter class:

public class SwaggerIgnoreFilter : IDocumentFilter
{
    private IServiceProvider _provider;

    public SwaggerIgnoreFilter(IServiceProvider provider)
    {
        if (provider == null) throw new ArgumentNullException(nameof(provider));

        this._provider = provider;
    }
    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    {
        var allTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(i => i.GetTypes()).ToList();

        var http = this._provider.GetRequiredService<IHttpContextAccessor>();
        var authorizedIds = new[] { "00000000-1111-2222-1111-000000000000" };   // All the authorized user id's.
                                                                                // When using this in a real application, you should store these safely using appsettings or some other method.
        var userId = http.HttpContext.User.Claims.Where(x => x.Type == "jti").Select(x => x.Value).FirstOrDefault();
        var show = http.HttpContext.User.Identity.IsAuthenticated && authorizedIds.Contains(userId);
        //var Securitytoken = new JwtSecurityTokenHandler().CreateToken(tokenDescriptor);
        //var tokenstring = new JwtSecurityTokenHandler().WriteToken(Securitytoken);
        //var token = new JwtSecurityTokenHandler().ReadJwtToken(tokenstring);
        //var claim = token.Claims.First(c => c.Type == "email").Value;
        Parametros parametros = new Parametros();
        if (!show)
        {
            var descriptions = context.ApiDescriptions.ToList();

            foreach (var description in descriptions)
            {
                // Expose login so users can login through Swagger. 
                if (description.HttpMethod == "POST" && description.RelativePath == "denarioapi/v1/auth/login")
                    continue;

                var route = "/" + description.RelativePath.TrimEnd('/');
                OpenApiPathItem path;
                swaggerDoc.Paths.TryGetValue(route, out path);

                switch(route)
                {
                    case string s when s.Contains("/Contabilidad"):
                        if (parametros.contabilidadApi != "1")
                        {
                            swaggerDoc.Paths.Remove(route);
                        }
                        break;
                    case string s when s.Contains("/Identificativos"):
                        if (parametros.identificativosApi != "1")
                        {
                            swaggerDoc.Paths.Remove(route);
                        }
                        break;
                    case string s when s.Contains("/Centros"):
                        if (parametros.centrosApi != "1")
                        {
                            swaggerDoc.Paths.Remove(route);
                        }
                        break;
                    case string s when s.Contains("/Contratos"):
                        if (parametros.contratosApi != "1")
                        {
                            swaggerDoc.Paths.Remove(route);
                        }
                        break;
                    
                    case string s when s.Contains("/Planificacion"):
                        if (parametros.planificacionApi != "1")
                        {
                            swaggerDoc.Paths.Remove(route);
                        }
                        break;
                    case string s when s.Contains("/Puestotrabajo"):
                        if (parametros.puestotrabajoApi != "1")
                        {
                            swaggerDoc.Paths.Remove(route);
                        }
                        break;
                    
                    case string s when s.Contains("/Usuarios"):
                        if (parametros.usuariosApi != "1")
                        {
                            swaggerDoc.Paths.Remove(route);
                        }
                        break;
                    
                    default:
                        break;
                }

                // remove method or entire path (if there are no more methods in this path)
                //switch (description.HttpMethod)
                //{
                    //case "DELETE": path. = null; break;
                    //case "GET": path.Get = null; break;
                    //case "HEAD": path.Head = null; break;
                    //case "OPTIONS": path.Options = null; break;
                    //case "PATCH": path.Patch = null; break;
                    //case "POST": path.Post = null; break;
                    //case "PUT": path.Put = null; break;
                    //default: throw new ArgumentOutOfRangeException("Method name not mapped to operation");
                //}

                //if (path.Delete == null && path.Get == null &&
                //    path.Head == null && path.Options == null &&
                //    path.Patch == null && path.Post == null && path.Put == null)
                //swaggerDoc.Paths.Remove(route);
            }

        }




        foreach (var definition in swaggerDoc.Components.Schemas)
        {
            var type = allTypes.FirstOrDefault(x => x.Name == definition.Key);
            if (type != null)
            {
                var properties = type.GetProperties();
                foreach (var prop in properties.ToList())
                {
                    var ignoreAttribute = prop.GetCustomAttribute(typeof(OpenApiIgnoreAttribute), false);

                    if (ignoreAttribute != null)
                    {
                        definition.Value.Properties.Remove(prop.Name);
                    }
                }
            }
        }
    }
}

Startup.cs ConfigureServices:

services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo
            {
                Title = "API",
                Version = "v1",
                Description = "API (.NET 5.0)",
                Contact = new OpenApiContact()
                {
                    Name = "Contact name",
                    Url = null,
                    Email = "[email protected]"
                }
            });
            c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
            {
                Description = @"Description",
                Name = "Authorization",
                In = ParameterLocation.Header,
                Type = SecuritySchemeType.Http,
                Scheme = "Bearer"
            });
            c.DocumentFilter<SwaggerIgnoreFilter>();
            c.AddSecurityRequirement(new OpenApiSecurityRequirement()
  {
        {
          new OpenApiSecurityScheme
          {
            Reference = new OpenApiReference
              {
                Type = ReferenceType.SecurityScheme,
                Id = "Bearer"
              },
              Scheme = "oauth2",
              Name = "Bearer",
              In = ParameterLocation.Header,

            },
            new List<string>()
          }
    });
        });
Charleton answered 11/6, 2021 at 7:7 Comment(0)
W
0

My implementation:

    string controlJs = "(response) => { " +
"if (!window.timerMostrar) { " +
"   window.timerMostrar = setInterval( " +
"    function() {" +
"        const lista = document.querySelectorAll('.opblock-tag-section'); " +
"        const listaArray = [...lista]; " +
"        if (document.querySelector('.authorize').classList.contains('unlocked')) " +
"        { " +
"           listaArray.forEach(title => { " +
"                if (title.querySelectorAll('#operations-tag-Login').length == 0) { " +
"                    title.style.display = 'none'; " +
"                } else { " +
"                    title.style.display = ''; " +
"                } " +
"            }); " +
"            document.querySelector('.models').style.display = 'none';  "+
"        } else { " +
"            listaArray.forEach(title => { " +
"                title.style.display = ''; " +
"            }); " +
"            document.querySelector('.models').style.display = '';  " +
"        } " +
"    }, 1000); " +
"} " +
"return response; }";

 app.UseSwaggerUI(c =>
 {
    ...
    c.UseResponseInterceptor(controlJs);
 })

You must change the word "Login" with the name of your Login controller.

Wendt answered 14/2 at 12:0 Comment(1)
Thank you for contributing to the Stack Overflow community. This may be a correct answer, but it’d be really useful to provide additional explanation of your code so developers can understand your reasoning. This is especially useful for new developers who aren’t as familiar with the syntax or struggling to understand the concepts. Would you kindly edit your answer to include additional details for the benefit of the community?Kugler

© 2022 - 2024 — McMap. All rights reserved.