If you've wired up Swashbuckle with AddSecurityDefinition and AddSecurityRequirement following the standard tutorial, you've probably hit this: every single endpoint in Swagger UI shows the padlock icon and demands the same auth scheme - even endpoints that don't need auth at all, or that need a different scheme entirely. This gets worse fast once you're running more than one JWT scheme in the same API.
Why this happens
The common tutorial snippet looks like this:
builder.Services.AddSwaggerGen(options =>
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
AddSecurityRequirement at this level applies globally - to every operation in the generated spec, regardless of whether the underlying endpoint actually requires that scheme, or requires a different one, or requires no auth at all. Swashbuckle has no way to know your intent unless you tell it per-operation.
The fix: an IOperationFilter
The right tool here is an IOperationFilter that inspects each operation's actual authorization metadata and applies the security requirement only where it's genuinely needed:
public class SecurityRequirementsOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var hasAuthorize = context.MethodInfo.DeclaringType!
.GetCustomAttributes(true)
.OfType<AuthorizeAttribute>()
.Any() ||
context.MethodInfo
.GetCustomAttributes(true)
.OfType<AuthorizeAttribute>()
.Any();
var hasAnonymous = context.MethodInfo
.GetCustomAttributes(true)
.OfType<AllowAnonymousAttribute>()
.Any();
if (!hasAuthorize || hasAnonymous)
{
return; // no auth required, no padlock
}
var authorizeAttributes = context.MethodInfo.DeclaringType!
.GetCustomAttributes(true)
.OfType<AuthorizeAttribute>()
.Concat(context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>());
var schemeNames = authorizeAttributes
.Select(a => a.AuthenticationSchemes)
.Where(s => !string.IsNullOrEmpty(s))
.SelectMany(s => s!.Split(','))
.Distinct()
.DefaultIfEmpty("Bearer") // fallback to your default scheme name
.ToList();
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = schemeNames.First() // scheme relevant to this operation
}
},
Array.Empty<string>()
}
}
};
}
}
Register it alongside your security definitions:
builder.Services.AddSwaggerGen(options =>
{
// ... AddSecurityDefinition calls for each scheme (e.g. "MainScheme", "UserTokenScheme")
options.OperationFilter<SecurityRequirementsOperationFilter>();
});
Now the generated spec - and Swagger UI's padlock icons - reflect reality: endpoints with [AllowAnonymous] show no padlock, endpoints with [Authorize] (no scheme specified) get your default, and endpoints with [Authorize(AuthenticationSchemes = "UserTokenScheme")] correctly show that they need the other token, not the main one.
Why this matters more with multiple schemes
If you're only running a single JWT scheme, the blanket AddSecurityRequirement approach is merely imprecise — annoying, but not actively misleading. Once you're running two schemes side by side (see my earlier post on dual JWT Bearer schemes), the global approach becomes actively wrong: it tells every consumer of your Swagger UI to authenticate with the wrong token on some endpoints, which is exactly the kind of thing that sends other developers down the wrong debugging path entirely.
Checklist
- [ ] Does Swagger UI show a padlock only on endpoints that actually require auth?
- [ ] If you have
[AllowAnonymous]endpoints, do they correctly show no lock? - [ ] If you're running multiple schemes, does each protected endpoint show the correct scheme, not just "a" scheme?
- [ ] Have you tested the Authorize button end-to-end — does the token you enter actually get attached to requests for the endpoint you're testing?
This is a small amount of extra setup for a much more honest Swagger UI — and it stops other developers (or future you) from trusting a spec that's quietly lying about what each endpoint needs.
Top comments (0)