Swagger Security in .NET 8 API
Enhancing Security with Basic Authentication
If you create a .NET 8 API project in Visual Studio, Swagger is available by default. When using Swagger, you will consider about security. You can set the Swagger documentation to be visible only in the internal network. However, anyone who knows the domain internally can view the Swagger documentation through the /swagger path. You can handle this issue by using basic authentication to enhance security and control API exposure.
Senario
Here is a Swagger document with APIs called Product and User.
- I will create
SwaggerAuthMiddleware.csand secure it with Basic Authentication. - In the sample code, I will use only two accounts: admin and user.
And when accessing by user account, Swagger will hide the Product and only expose the User.
*The sample source code hard-codes the data for convenience.
Source Code
SwaggerAuthMiddleware.cs
public class SwaggerAuthMiddleware
{
private readonly RequestDelegate _next;
public SwaggerAuthMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
// Check the swagger document path
if (!context.Request.Path.StartsWithSegments("/swagger"))
{
await _next(context);
return;
}
// Authentication and role verification
string? role = GetUserRole(context);
if (string.IsNullOrEmpty(role))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.Headers["WWW-Authenticate"] = "Basic realm=\"Swagger UI\"";
return;
}
// Call FilterSwaggerJsonResponse if role is not an admin
if (IsSwaggerJsonRequest(context) && !role.Equals("admin", StringComparison.OrdinalIgnoreCase))
{
await FilterSwaggerJsonResponse(context, role);
return;
}
await _next(context);
}
/// <summary>
/// Basic Authentication and Assigning Roles to Accounts
/// </summary>
private string? GetUserRole(HttpContext context)
{
var authHeader = context.Request.Headers["Authorization"].ToString();
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Basic "))
{
var encodedCredentials = authHeader.Substring("Basic ".Length).Trim();
try
{
var decodedBytes = Convert.FromBase64String(encodedCredentials);
var decodedCredentials = Encoding.UTF8.GetString(decodedBytes);
var credential = decodedCredentials.Split(':', 2);
if (credential.Length == 2)
{
var username = credential[0];
var password = credential[1];
// Verify account and grant roles
if (username == "admin" && password == "0000")
{
return "admin";
}
else if (username == "user" && password == "1234")
{
return "user";
}
}
}
catch
{
Console.WriteLine("error!");
}
}
return null;
}
/// <summary>
/// Check SwaggerJson Request
/// </summary>
private bool IsSwaggerJsonRequest(HttpContext context)
{
return context.Request.Path.Value?.Contains("swagger.json", StringComparison.OrdinalIgnoreCase) == true;
}
/// <summary>
/// Return a Swagger JSON response filtered by role
/// </summary>
private async Task FilterSwaggerJsonResponse(HttpContext context, string role)
{
var allowedPrefixes = GetAllowedPathForRole(role);
if (allowedPrefixes == null || !allowedPrefixes.Any())
{
await _next(context);
return;
}
var originalBodyStream = context.Response.Body;
using (var memStream = new MemoryStream())
{
context.Response.Body = memStream;
await _next(context);
memStream.Position = 0;
string json = await new StreamReader(memStream).ReadToEndAsync();
var swaggerDoc = JsonSerializer.Deserialize<Dictionary<string, object>>(json);
// Verify that the parsed Swagger document is valid and contains a "paths" key
if (swaggerDoc != null && swaggerDoc.ContainsKey("paths"))
{
var pathsJson = swaggerDoc["paths"].ToString();
var paths = JsonSerializer.Deserialize<Dictionary<string, object>>(pathsJson);
if (paths != null)
{
// Filter paths based on allowed path
var filteredPaths = paths
.Where(p => allowedPrefixes.Any(prefix => p.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
.ToDictionary(p => p.Key, p => p.Value);
// Reassign filtered paths to "paths" in the Swagger document
swaggerDoc["paths"] = filteredPaths;
json = JsonSerializer.Serialize(swaggerDoc);
}
}
context.Response.Body = originalBodyStream;
context.Response.ContentLength = Encoding.UTF8.GetByteCount(json);
await context.Response.WriteAsync(json);
}
}
/// <summary>
/// Get role's allowed API paths
/// </summary>
private List<string> GetAllowedPathForRole(string role)
{
var allowedPaths = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase)
{
{ "user", new List<string> { "/api/v1/user" } }
};
return allowedPaths.ContainsKey(role) ? allowedPaths[role] : [];
}
}Invoke(HttpContext context)
1. Check Swagger path
2. If role does not exist, return 401 Unauthorized response
3. Check if request is swagger.json request
4. If role is not admin, process path filtering
Get bcal’s stories in your inbox
Join Medium for free to get updates from this writer.
GetUserRole(HttpContext context)
1. Get authentication information from Authorization header.
2. Check if it is Basic authentication
3. Decode authentication information ( username:password )
4. Check username and password and return role
IsSwaggerJsonRequest(HttpContext context)
1. Check if the request path contains swagger.json
FilterSwaggerJsonResponse(HttpContext context, string role)
1. Get the allowed API paths based on the role
2. Read the saved JSON data and parse it into a Swagger document
3. Filter only the paths starting with the allowed path prefix
4. Serialize the filtered paths and reassign filtered paths to the response and return.
GetAllowedPathForRole(string role)
1. Returns the paths allowed by role
Program.cs
...
var app = builder.Build();
// Place the Middleware before the UseSwagger() method.
app.UseMiddleware<SwaggerAuthMiddleware>();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
...Execution
When accessing the Swagger document, a form for entering ID/PW appears.
Entered user account information.
User account is filtered and only User API is exposed in Swagger.
Conclusion
If you want to enhance Swagger security, try to use Basic Authentication and Swagger Path filters. It can be used more usefully than you might think.











