.NET-5 Hide swagger endpoints to unauthorized users

Viewed 1343

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@email.com"
                }
            });
            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>()
      }
    });
        });
4 Answers

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();

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@email.com"
                }
            });
            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>()
          }
    });
        });

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();
    if (settings.RequireAuthentication)
    {
        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();
...
Related