DEV Community

Virendra Vyas
Virendra Vyas

Posted on Edited on

Swagger UI Is Showing the Wrong Auth Requirement on Every Endpoint — Here's the Fix

Edit:Updated after a sharp comment from a reader — the original version used reflection over attributes, which misses RequireAuthorization() on minimal APIs and fallback policies, and collapsed multi-scheme requirements incorrectly. Fixed below.

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>()
        }
    });
});
Enter fullscreen mode Exit fullscreen mode

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 first attempt (and why it wasn't enough)

My first fix was an IOperationFilter that reflected over AuthorizeAttribute and AllowAnonymousAttribute on the controller/action. That works fine for typical MVC controllers — but a reader pointed out two real gaps:

Minimal APIs and fallback policies don't show up via reflection. RequireAuthorization() on a minimal API endpoint, or a global AuthorizationOptions.FallbackPolicy, protect the endpoint at runtime without any AuthorizeAttribute being declared. Reflection-based detection reports these as anonymous, which is wrong.
Multi-scheme requirements were being collapsed incorrectly. In OpenAPI, schemes inside one requirement object mean AND (all required together); separate requirement objects mean OR (any one satisfies it). The original code took schemeNames.First(), silently dropping any additional required schemes from the spec.
The fix: read runtime metadata, not attributes

The more reliable source is ApiDescription.ActionDescriptor.EndpointMetadata, which reflects what's actually enforced at runtime — including minimal API policies and fallback policies — not just what's declared via attributes:

public class SecurityRequirementsOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var endpointMetadata = context.ApiDescription.ActionDescriptor.EndpointMetadata;

        var authorizeData = endpointMetadata.OfType<IAuthorizeData>().ToList();
        var hasAnonymous = endpointMetadata.OfType<IAllowAnonymous>().Any();

        if (hasAnonymous)
        {
            return; // explicitly anonymous, no padlock
        }

        if (!authorizeData.Any())
        {
            // No explicit [Authorize] - check if a fallback policy applies
            // (inject IAuthorizationPolicyProvider or pass AuthorizationOptions
            // in to resolve FallbackPolicy here if one is configured)
            return;
        }

        // Group scheme names per AuthorizeData entry - each entry's schemes
        // are its own AND group; multiple entries are OR'd as separate
        // requirement objects, matching OpenAPI semantics.
        var requirementGroups = authorizeData
            .Select(a => (a.AuthenticationSchemes ?? "Bearer")
                .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
            .Where(schemes => schemes.Length > 0)
            .Distinct(new StringArrayComparer())
            .ToList();

        if (!requirementGroups.Any())
        {
            requirementGroups.Add(new[] { "Bearer" });
        }

        operation.Security = requirementGroups
            .Select(schemes =>
            {
                var requirement = new OpenApiSecurityRequirement();
                foreach (var scheme in schemes)
                {
                    requirement[new OpenApiSecurityScheme
                    {
                        Reference = new OpenApiReference
                        {
                            Type = ReferenceType.SecurityScheme,
                            Id = scheme
                        }
                    }] = Array.Empty<string>();
                }
                return requirement;
            })
            .ToList();
    }

    private class StringArrayComparer : IEqualityComparer<string[]>
    {
        public bool Equals(string[]? x, string[]? y) =>
            x != null && y != null && x.OrderBy(s => s).SequenceEqual(y.OrderBy(s => s));

        public int GetHashCode(string[] obj) =>
            obj.OrderBy(s => s).Aggregate(17, (hash, s) => hash * 31 + s.GetHashCode());
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it the same way as before:

builder.Services.AddSwaggerGen(options =>
{
    // ... AddSecurityDefinition calls for each scheme (e.g. "MainScheme", "UserTokenScheme")

    options.OperationFilter<SecurityRequirementsOperationFilter>();
});
Enter fullscreen mode Exit fullscreen mode

Now operation.Security correctly represents:

Anonymous endpoints ([AllowAnonymous] or no auth metadata at all) → no padlock
Single-scheme endpoints → one requirement object, one scheme
Multi-scheme AND ([Authorize(AuthenticationSchemes = "A,B")]) → one requirement object containing both schemes
Multi-scheme OR (stacked [Authorize] attributes with different schemes) → separate requirement objects, one per attribute

This is closer to the actual OpenAPI spec semantics, not just a padlock that happens to look right for the common case.

Fallback policies still need explicit handling

If you use a global FallbackPolicy (protects any endpoint without explicit authorization metadata), the filter above deliberately does nothing for those endpoints rather than guessing — you'll want to resolve the policy's scheme requirements yourself (via IAuthorizationPolicyProvider) and apply them the same way as an explicit [Authorize]. I've left this as a TODO above rather than baking in an assumption about your policy setup, since fallback policies vary a lot between projects.

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, or hides a required second token entirely, which is exactly the kind of thing that sends other developers down the wrong debugging path.

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(s) — not just "a" scheme?
Do multi-scheme AND requirements show as one requirement object with both schemes, not just the first one?
Do you have any minimal API endpoints or a fallback policy, and if so, are they handled explicitly rather than assumed anonymous?
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 (2)

Collapse
 
iqtechsolutions profile image
Ivan Rossouw

One edge case is authorization that isn’t expressed as controller/action attributes: RequireAuthorization() on minimal APIs and a FallbackPolicy can protect an endpoint while reflection over attributes says it is anonymous. Reading context.ApiDescription.ActionDescriptor.EndpointMetadata is closer to the runtime contract, and I’d add generated-spec tests for anonymous, default-scheme, and alternate-scheme endpoints. Multiple schemes also need care: schemes inside one OpenAPI requirement object mean AND, while separate objects mean OR, so taking only First() can silently misdocument the contract. How would you represent fallback-policy endpoints?

Collapse
 
virendravyas profile image
Virendra Vyas

You're right on the metadata point - reflecting on AuthorizeAttribute/AllowAnonymousAttributeonly tells you what's declared imperatively. Minimal APIs usingRequireAuthorization() or a global FallbackPolicywon't show up via reflection at all, so the filter would wrongly report them as anonymous. context.ApiDescription.ActionDescriptor.EndpointMetadata is the better source since it reflects what's actually enforced at runtime, not just what's declared via attributes - I'd pull IAuthorizeDataoff the endpoint metadata instead of walking attributes directly.

On the AND/OR issue - that's a real bug in the approach as written, not just an edge case. Taking schemeNames.First() collapses a multi-scheme [Authorize(AuthenticationSchemes = "A,B")] (which OpenAPI would represent as one requirement object containing both - AND) down to just scheme A, silently dropping B from the spec. And if an endpoint genuinely needs to represent "A OR B" that has to be two separate requirement objects in the operation.Security list, which the current code structurally can't produce since it only ever emits one.

For fallback-policy endpoints specifically, I'd check whether the endpoint has any explicit IAuthorizeData at all - if none, but a fallback policy is configured on AuthorizationOptions.FallbackPolicy, resolve that policy's scheme requirements and apply them the same way as an explicit [Authorize], rather than treating "no attribute" as "no auth."

I'll update the post with a corrected version that uses EndpointMetadata and properly separates AND vs OR requirement objects — appreciate you flagging it, this would've bitten people running fallback policies.