Swagger OpenApi Doc Generation

The Swagger tooling provided by Swashbuckle in ASP.NET Core can be used to auto-generate OpenAPI docs and a test UI for exercising your API through the browser without external tooling. The basic setup only requires a few lines of code and is currently optionally included in many of the standard dotnet templates that produce APIs. A simple setup includes adding two services and two middleware registrations:

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

...

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

Using a minimal API with a single simple endpoint this is all that’s needed to produce an OpenAPI json doc and a full documentation and testing UI.

API Code

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapGet("/hello", () => "Hello World");
app.Run();

OpenAPI spec

{
  "openapi": "3.0.1",
  "info": {
    "title": "SwaggerApi",
    "version": "1.0"
  },
  "paths": {
    "/hello": {
      "get": {
        "tags": [
          "SwaggerApi"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": { }
}

Swagger UI

Swagger UI test for unsecured minimal API

Configuring for Bearer Token Authentication

The basic settings are sufficient for an unsecured API, but don’t reflect authorization requirements in either the generated doc or the UI. Executing a test against a secured API using the generated UI will only result in 401 failures because no Authorization header is included. For an API using standard Bearer auth some changes are needed.

The additional configuration is made up of two parts: a security definition and a security requirement referencing the defined scheme. The definition is added to the AddSwaggerGen call and specifies the type of authentication and how it is expected to be applied to requests. JWT Bearer authentication is used here.

services.AddSwaggerGen(c =>
{
    c.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = "bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "JWT Authorization header using the Bearer scheme."
    });
});

Using this by itself adds a new securitySchemes entry in the API definition and also enables a new “Authorize” button in the UI which accepts a token value to be applied to the Authorization header of test requests.

Swagger UI Authorization Dialog

If I test this against a secured endpoint with my current configuration I still get a 401 failure.

Unauthorized API request result

Looking at the details of the request, the problem is clear. Even though I provided a token, there is no Authorization header being added to the request. This is because I haven’t declared this API endpoint (or any currently) as having a security requirement in my Swagger configuration. This prevents the UI from working but also leaves out this information from my generated API definition, causing it to misrepresent the authentication requirements of my secured endpoints.

To add security requirements I can update the configuration either at the individual endpoint level or globally across the whole API. The global requirement configuration goes into the same place as the definition. I need to make sure that the reference Id matches that on the declared definition.

builder.Services.AddSwaggerGen(c =>
{
    c.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = "bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "JWT Authorization header using the Bearer scheme."
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "bearerAuth"
                }
            },
            new string[] { }
        }
    });
});

Adding this now allows the request to complete in the test UI and we can see the added Authorization: Bearer header.

Authorized API request result

In this case there’s still a minor problem present. Because I’ve declared the security requirement at a global level the /hello endpoint seen above is showing a lock icon and will be tested with the added header even though it allows anonymous access. This is also reflected in the generated definition, which may be a bigger problem depending on how I rely on it elsewhere in my system.

app.MapGet("/hello", () => "Hello World");

app.MapGet("/secure", () => "Hello Secure World")
    .RequireAuthorization();

Representing Mixed Endpoint Security

In many cases it’s probably a better idea architecturally to try to separate anonymous endpoints, but if that isn’t the case security definitions can also be applied at the individual endpoint level (or with groups of endpoints depending on how your endpoint routes are defined). I first need to remove the global requirement I added previously and then I can apply the same thing at a lower level. Here I’m using a local function to simplify reuse on multiple endpoints.

OpenApiOperation AddBearerAuth(OpenApiOperation op)
{
    op.Security.Add(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "bearerAuth"
                }
            },
            new string[] { }
        }
    });
    return op;
}

app.MapGet("/hello", () => "Hello World");

app.MapGet("/secure", () => "Hello Secure World")
    .RequireAuthorization()
    .WithOpenApi(AddBearerAuth);

app.MapGet("/users", async () => await DataSource.GetUsersListAsync())
    .WithName("GetUsers")
    .RequireAuthorization()
    .WithOpenApi(AddBearerAuth);

Now the UI shows lock icons only on the secured endpoints and behaves appropriately while testing.

Mix of secured and anonymous endpoints

The OpenAPI definition similarly reflects the actual auth requirements.

{
  "openapi": "3.0.1",
  "info": {
    "title": "SwaggerApi",
    "version": "1.0"
  },
  "paths": {
    "/hello": {
      "get": {
        "tags": [
          "SwaggerApi"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/secure": {
      "get": {
        "tags": [
          "SwaggerApi"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": [ ]
          }
        ]
      }
    },
    "/users": {
      "get": {
        "tags": [
          "SwaggerApi"
        ],
        "operationId": "GetUsers",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/User"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": [ ]
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "User": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true
          },
          "first": {
            "type": "string",
            "nullable": true
          },
          "last": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "username": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "description": "JWT Authorization header using the Bearer scheme.",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  }
}

In a real API I would want to add further configuration to represent data types, possible returned status codes, and other details but this configuration provides what’s needed for API testing in the browser using the built-in tooling.

Code Versions

Example code is using .NET 7.0 with these library versions:

  • Microsoft.AspNetCore.Authentication.JwtBearer 7.0.9
  • Microsoft.AspNetCore.OpenApi 7.0.9
  • Swashbuckle.AspNetCore 6.5.0