Friday, June 8, 2018

Swashbuckle for Swagger (Markdown in XML Comments and Protecting the Documentation)

I just introduced Swashbuckle to a project I've been working on. If you're not familiar with Swashbuckle and/or Swagger UI, you can check them out here and here respectively. They work together to provide really simple out of the box documentation, which you can configure in a number of ways.

In my case I wanted to allow markdown in my XML comments that would then be displayed to my user. I also wanted to be able to control access to my API documentation. That may seem counter-intuitive (why document it if you're going to restrict access?) so you'll just have to trust me that it was necessary.

Both of these requirements proved to be pretty simple once I found the right posts and put them all together. I figured I'd centralize them so next time I have to do this I have all the information in one place.

Before we get into configuration and tweaking, you'll need to actually install Swagger, which can be done by adding the Swashbuckle.AspNetCore package from NuGet. If you want to follow these instructions, you'll also need to install the Microsoft.Extensions.PlatformAbstractions.

For the markdown in my XML, I had to take the following steps:
  1. Configure my project to generate XML comment documentation
  2. Configure Swagger UI to use the generated XML document
  3. Create an operation filter to format comments as markdown
  4. Set Swagger UI to use the new filter
Configuring the project to generate XML comment documentation is pretty easy. Right-click on your project and choose Properties. On the Build tab, check "XML documentation file" and in the textbox enter the name and path you want your file to be generated into. For this post I used "bin\Debug\net461\my-awesome-comments.xml" (without the quotes of course).

Assuming you go with the out-of-the-box, most-simple usage of Swagger UI to get started, your startup.cs includes something like this:

   1:  public void ConfigureServices(IServiceCollection services)
   2:  {
   3:      ...snip...
   4:      services.AddSwaggerGen(c =>
   5:      {
   6:          c.SwaggerDoc("v1", new Info {Title = "Identity Server", Version = "v1"});
   7:      });
   8:      ...snip...
   9:  }

 To configure Swagger UI to use the generated XML document you have to two lines to your AddSwaggerGen invocation. After our changes we have this:
   1:  public void ConfigureServices(IServiceCollection services)
   2:  {
   3:      ...snip...
   4:      services.AddSwaggerGen(c =>
   5:      {
   6:          c.SwaggerDoc("v1", new Info {Title = "Identity Server", Version = "v1"});
   7:  
   8:          var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "my-awesome-comments.xml");
   9:          c.IncludeXmlComments(filePath);
  10:      });
  11:      ...snip...
  12:  }

That's it. Our Swagger UI now uses our XML comments. We're not able to use markdown yet, but we're getting there.

I'm not super up to speed on operation filters, but I found this solution in an answer to an issue someone opened on the Github repo for Swashbuckle. Scroll down to the answer on April 23, 2015 from user geokaps. I like having everything separated in folder structures, but you don't necessarily have to do it that way. In my case, I created a folder called Filters and created a new file in that folder called FormatXmlCommentSwaggerFilter.cs. Here are the contents of that file:
   1:  public class FormatXmlCommentSwaggerFilter : IOperationFilter
   2:  {
   3:      public void Apply(Operation operation, OperationFilterContext context)
   4:      {
   5:          operation.Description = Formatted(operation.Description);
   6:          operation.Summary = Formatted(operation.Summary);
   7:      }
   8:  
   9:      private string Formatted(string text)
  10:      {
  11:          if (text == null) return null;
  12:  
  13:          // Strip out the whitespace that messes up the markdown in the xml comments.
  14:          // but don't touch the whitespace in <code> blocks. Those get fixed below.
  15:          string resultString = Regex.Replace(text, @"(^[ \t]+)(?![^<]*>|[^>]*<\/)", "", RegexOptions.Multiline);
  16:          resultString = Regex.Replace(resultString, @"<code[^>]*>", "<pre>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Multiline);
  17:          resultString = Regex.Replace(resultString, @"</code[^>]*>", "</pre>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Multiline);
  18:          resultString = Regex.Replace(resultString, @"<!--", "", RegexOptions.Multiline);
  19:          resultString = Regex.Replace(resultString, @"-->", "", RegexOptions.Multiline);
  20:  
  21:          try
  22:          {
  23:              string pattern = @"<pre\b[^>]*>(.*?)</pre>";
  24:  
  25:              foreach (Match match in Regex.Matches(resultString, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Multiline))
  26:              {
  27:                  var formattedPreBlock = FormatPreBlock(match.Value);
  28:                  resultString = resultString.Replace(match.Value, formattedPreBlock);
  29:              }
  30:              return resultString;
  31:          }
  32:          catch
  33:          {
  34:              // Something went wrong so just return the original resultString
  35:              return resultString;
  36:          }
  37:      }
  38:  
  39:      private string FormatPreBlock(string preBlock)
  40:      {
  41:          // Split the <pre> block into multiple lines
  42:          var linesArray = preBlock.Split('\n');
  43:          if (linesArray.Length < 2)
  44:          {
  45:              return preBlock;
  46:          }
  47:          else
  48:          {
  49:              // Get the 1st line after the <pre>
  50:              string line = linesArray[1];
  51:              int lineLength = line.Length;
  52:              string formattedLine = line.TrimStart(' ', '\t');
  53:              int paddingLength = lineLength - formattedLine.Length;
  54:  
  55:              // Remove the padding from all of the lines in the <pre> block
  56:              for (int i = 1; i < linesArray.Length - 1; i++)
  57:              {
  58:                  linesArray[i] = linesArray[i].Substring(paddingLength);
  59:              }
  60:  
  61:              var formattedPreBlock = string.Join("", linesArray);
  62:              return formattedPreBlock;
  63:          }
  64:      }
  65:  }

Once I had that filter created I just had to modify my Swagger UI configuration to use it. After these changes here's my whole Swagger UI configuration in startup.cs:
   1:  public void ConfigureServices(IServiceCollection services)
   2:  {
   3:      ...snip...
   4:      services.AddSwaggerGen(c =>
   5:      {
   6:          c.SwaggerDoc("v1", new Info {Title = "Identity Server", Version = "v1"});
   7:  
   8:          var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "my-awesome-comments.xml");
   9:          c.IncludeXmlComments(filePath);
  10:          c.OperationFilter<FormatXmlCommentSwaggerFilter>();
  11:      });
  12:      ...snip...
  13:  }

That's all I had to do to enable markdown in my XML comments and it works pretty well I have to say. The next part was a little bit trickier because I wanted to lock down my entire Swagger instance so that only authorized users (with a particular permission) would be able to access it. To do this, I took the following steps:

  1. Create Swagger authorization middleware
  2. Create an extension method to use the middleware
  3. Use extension method to wire up the middleware
In my particular scenario I'm using Swagger to document our Identity Server API so I already had the ability to secure my other endpoints. On the surface it should have been just as straightforward to secure my Swagger endpoints. But I didn't want just anyone to be able to authenticate with our Identity Server and then view my APIs. I wanted to keep those private so only certain people (developers in my organization) could see the APIs after they're authenticated. To do that I created the following middleware to validate that the user is authenticated (logged in) and also should be able to access Swagger:
   1:  public class SwaggerAuthorizedMiddleware
   2:  {
   3:      private readonly RequestDelegate _next;
   4:  
   5:      public SwaggerAuthorizedMiddleware(RequestDelegate next)
   6:      {
   7:          _next = next;
   8:      }
   9:  
  10:      public async Task Invoke(HttpContext context)
  11:      {
  12:          // if the request is for the /swagger check for authentication
  13:          if (context.Request.Path.Equals("/swagger/index.html")
  14:              && (!context.User.Identity.IsAuthenticated || context.User.Claims.All(c => c.Type != "can_access_swagger") ||
  15:                  context.User.Claims.First(c => c.Type == "can_access_swagger").Value != "true"))
  16:          {
  17:              // the user is trying to access /swagger.index.html, but is either not authenticated (logged in) or
  18:              // is authenticated, but does not have access to Swagger, so redirect them to the login page
  19:              await context.ChallengeAsync();
  20:              return;
  21:          }
  22:  
  23:          // either the user was not trying to access /swagger/index.html or the user is authenticated and allowed
  24:          // so carry on to the next middleware process
  25:          await _next.Invoke(context);
  26:      }
  27:  }

That's all the middleware has to do. Now, keep in mind that we already had our authentication setup and I'm just plugging into that existing authentication and checking whether the user is authenticated and has access. If you don't already have that setup (i.e. your API is not already secured) then securing your Swagger UI is going to be more complicated. Even if that's the case, hopefully this helps steer you in the right direction.
Now that I have the middleware, I want to create a really simple extension method so I can use the middleware in my startup class
   1:  public static class SwaggerAuthorizeExtensions
   2:  {
   3:      public static IApplicationBuilder UseSwaggerAuthorized(this IApplicationBuilder builder)
   4:      {
   5:          return builder.UseMiddleware<SwaggerAuthorizedMiddleware>();
   6:      }
   7:  }

Then once I have that extension method it's just a matter of wiring it up. In startup.cs I have a Configure method. In there, after the UseMvc invocation, I want to add UseSwaggerAuthorized:
app.UseSwaggerAuthorized();


That's it! My Swagger UI is now protected with my pre-existing authentication process with an added check for whether the user should be able to access my Swagger documentation. Hope this helps someone!

1 comment:

  1. Answers I Couldn'T Find Anywhere Else: Swashbuckle For Swagger (Markdown In Xml Comments And Protecting The Documentation) >>>>> Download Now

    >>>>> Download Full

    Answers I Couldn'T Find Anywhere Else: Swashbuckle For Swagger (Markdown In Xml Comments And Protecting The Documentation) >>>>> Download LINK

    >>>>> Download Now

    Answers I Couldn'T Find Anywhere Else: Swashbuckle For Swagger (Markdown In Xml Comments And Protecting The Documentation) >>>>> Download Full

    >>>>> Download LINK xm

    ReplyDelete