{"slug": "adding-a-jwt-authorize-button-to-swagger-ui", "title": "Adding a JWT Authorize Button to Swagger UI", "summary": "A Softinbit Technology developer guide details how to add a JWT Authorize button to Swagger UI and Scalar in ASP.NET Core by declaring a bearer security scheme in the OpenAPI document and applying it only to operations marked with [Authorize]. The guide covers Swashbuckle 6 through 9 (pinned at 9.0.6), Swashbuckle 10 on Microsoft.OpenApi 2.x, and the built-in Microsoft.AspNetCore.OpenApi package in .NET 9 and 10, warning that Microsoft.AspNetCore.OpenApi 8.x and 9.x depend on Microsoft.OpenApi 1.x while 10.x depends on 2.x, so the packages must agree or the build or first document request fails. It also recommends keeping documentation endpoints limited to the Development environment.", "body_md": "Add a JWT Authorize button to Swagger UI or Scalar in ASP.NET Core: bearer scheme, locks only on [Authorize] endpoints, Swashbuckle 10 and .NET 10 code.\n\nNote: this article was drafted with AI assistance and reviewed by Softinbit Technology before publishing. It is part of the series The .NET Implementation Playbook.\n\nYour API issues JSON Web Tokens (JWT), and every endpoint with [Authorize] returns 401 when you try it from Swagger UI. The UI is not broken. It simply does not know that your API expects an Authorization: Bearer <token> header, because nothing in the OpenAPI document says so.\n\nIn this guide you describe the bearer scheme in the OpenAPI document, mark only the protected operations with a lock, and keep the documentation UI out of production. You get the same result on Swashbuckle 6 to 9, on Swashbuckle 10, and on the built-in Microsoft.AspNetCore.OpenApi package of .NET 9 and 10.\n\nWhat you will build\n\nAn Authorize button in Swagger UI that takes a raw access token and sends Authorization: Bearer <token> on protected calls.\n\nA lock icon only on operations that carry [Authorize], with 401 and 403 responses documented. [AllowAnonymous] operations stay open.\n\nWorking code for Swashbuckle 6 to 9, for Swashbuckle 10 (Microsoft.OpenApi 2.x), and for the built-in OpenAPI document with Scalar or Swagger UI.\n\nA token that survives a page reload while you develop.\n\nDocumentation endpoints that exist only in the Development environment.\n\nBefore you start\n\nA Store.Api Web API with controllers on .NET 8, 9, or 10.\n\nJWT bearer authentication from Issuing and Validating JWT Access Tokens in ASP.NET Core, including an anonymous POST /api/auth/login that returns { \"accessToken\": \"...\", \"expiresAt\": \"...\" }. The authentication scheme is the default JwtBearerDefaults.AuthenticationScheme, which is \"Bearer\".\n\nPick one path from this table.\n\n.NET 8 or 9 that already uses Swashbuckle, or references Microsoft.AspNetCore.OpenApi 8.x or 9.x: Path: A: Swashbuckle 9.x, pinned; Steps: 1, 3, 4, 7\n\n.NET 10, or a project with no other OpenAPI package: Path: A2: Swashbuckle 10; Steps: 2, 3, 4, 7\n\n.NET 9 or 10 that uses the built-in AddOpenApi(): Path: B: document transformer; Steps: 4, 5, 6, 7\n\nWhy pin? Swashbuckle 10 moved to Microsoft.OpenApi 2.x, which renamed namespaces and replaced how references work. Microsoft.AspNetCore.OpenApi 8.x and 9.x depend on Microsoft.OpenApi 1.x, while 10.x depends on 2.x. A project can load only one Microsoft.OpenApi version, so the two packages must agree, or the build or the first document request fails. The .NET 9 and .NET 10 webapi templates both add Microsoft.AspNetCore.OpenApi by default.\n\n```\n# Path A: Swashbuckle 6 to 9 (pin the last 9.x release)dotnet add package Swashbuckle.AspNetCore --version 9.0.6\n# Path A2: Swashbuckle 10 (Microsoft.OpenApi 2.x)dotnet add package Swashbuckle.AspNetCore\n# Path B: built-in OpenAPI (.NET 9+) plus one UIdotnet add package Microsoft.AspNetCore.OpenApidotnet add package Scalar.AspNetCoredotnet add package Swashbuckle.AspNetCore.SwaggerUI\n```\n\nSwashbuckle.AspNetCore.SwaggerUI only serves the UI files and has no Microsoft.OpenApi dependency, so it is safe next to any version of the built-in package.\n\nHow it works\n\nThe OpenAPI document gets a components.securitySchemes entry named Bearer of type http with scheme bearer. Swagger UI and Scalar read it and show an Authorize control.\n\nYou paste the token. The UI keeps it in memory, or in browser storage when persistence is on.\n\nBefore each call, the UI checks the operation’s security list. Only operations that reference Bearer get the header.\n\nThat reference comes either from a global requirement (every operation) or from a per-operation filter that reads [Authorize] and [AllowAnonymous] metadata.\n\nThe JWT bearer handler validates the header exactly as it does for any other client.\n\n``` php\nOpenAPI document  components.securitySchemes.Bearer  -> Authorize button  paths./api/me.get.security: Bearer -> lock icon, header sent  paths./api/me/ping.get             -> no lock, no header\n```\n\nThe document only tells the UI what to send. It never protects anything. [Authorize] and the authentication middleware still do that.\n\nStep 1: Describe the bearer scheme with Swashbuckle 6 to 9\n\n```\n// File: OpenApi/SwaggerServiceExtensions.csusing Microsoft.OpenApi.Models;\nnamespace Store.Api.OpenApi;\njs\npublic static class SwaggerServiceExtensions{    public const string BearerSchemeId = \"Bearer\";\npublic static IServiceCollection AddSwaggerWithJwt(this IServiceCollection services)    {        services.AddEndpointsApiExplorer();        services.AddSwaggerGen(options =>        {            options.SwaggerDoc(\"v1\", new OpenApiInfo { Title = \"Store API\", Version = \"v1\" });\noptions.AddSecurityDefinition(BearerSchemeId, new OpenApiSecurityScheme            {                Type = SecuritySchemeType.Http,                Scheme = \"bearer\",                BearerFormat = \"JWT\",                In = ParameterLocation.Header,                Name = \"Authorization\",                Description = \"Paste the access token only. Swagger UI adds the 'Bearer ' prefix.\"            });\noptions.AddSecurityRequirement(new OpenApiSecurityRequirement            {                [new OpenApiSecurityScheme                {                    Reference = new OpenApiReference                    {                        Type = ReferenceType.SecurityScheme,                        Id = BearerSchemeId                    }                }] = Array.Empty<string>()            });        });\nreturn services;    }}\n```\n\nType = SecuritySchemeType.Http with Scheme = \"bearer\" tells the UI to build the header itself as Bearer <value>. That is why the description says to paste the token alone.\n\nIn and Name are ignored for the Http type (the Microsoft.OpenApi serializer drops them from the JSON). They matter only for SecuritySchemeType.ApiKey, where the UI sends the pasted value unchanged.\n\nAddSecurityRequirement with an OpenApiReference applies the scheme to every operation. It is the quickest way to get a working button. Step 3 replaces it with a per-operation version.\n\nThe empty array is the list of scopes. Only OAuth2 schemes use scopes.\n\nStep 2: Use the Swashbuckle 10 syntax (Microsoft.OpenApi 2.x)\n\nSkip this step on Path A. On Path A2, use this version of the same file.\n\n```\n// File: OpenApi/SwaggerServiceExtensions.csusing Microsoft.OpenApi;\nnamespace Store.Api.OpenApi;\njs\npublic static class SwaggerServiceExtensions{    public const string BearerSchemeId = \"Bearer\";\npublic static IServiceCollection AddSwaggerWithJwt(this IServiceCollection services)    {        services.AddEndpointsApiExplorer();        services.AddSwaggerGen(options =>        {            options.SwaggerDoc(\"v1\", new OpenApiInfo { Title = \"Store API\", Version = \"v1\" });\noptions.AddSecurityDefinition(BearerSchemeId, new OpenApiSecurityScheme            {                Type = SecuritySchemeType.Http,                Scheme = \"bearer\",                BearerFormat = \"JWT\",                Description = \"Paste the access token only. Swagger UI adds the 'Bearer ' prefix.\"            });\njs\n            options.AddSecurityRequirement(document => new OpenApiSecurityRequirement            {                [new OpenApiSecuritySchemeReference(BearerSchemeId, document)] = []            });        });\nreturn services;    }}\n```\n\nThe types moved from Microsoft.OpenApi.Models to Microsoft.OpenApi. Old tutorials fail to compile on Swashbuckle 10 for this reason alone.\n\nAddSecurityRequirement now takes a Func<OpenApiDocument, OpenApiSecurityRequirement>, as the Swashbuckle migration guide describes.\n\nOpenApiSecuritySchemeReference replaces the Reference property. Passing document lets the reference resolve to the scheme you defined. This matches the bearer sample in the Swashbuckle documentation.\n\nStep 3: Show the lock only on protected endpoints\n\nA global requirement puts a lock on POST /api/auth/login too, which is misleading. This filter adds the requirement only where authorization metadata says so.\n\n```\n// File: OpenApi/AuthorizeOperationFilter.csusing Microsoft.AspNetCore.Authorization;using Microsoft.OpenApi.Models;using Swashbuckle.AspNetCore.SwaggerGen;\nnamespace Store.Api.OpenApi;\npublic sealed class AuthorizeOperationFilter : IOperationFilter{    public void Apply(OpenApiOperation operation, OperationFilterContext context)    {        var metadata = context.ApiDescription.ActionDescriptor.EndpointMetadata;\njs\n        var requiresAuth = metadata.OfType<IAuthorizeData>().Any();        var allowsAnonymous = metadata.OfType<IAllowAnonymous>().Any();\nif (!requiresAuth || allowsAnonymous)        {            return;        }\noperation.Responses.TryAdd(\"401\", new OpenApiResponse { Description = \"Unauthorized\" });        operation.Responses.TryAdd(\"403\", new OpenApiResponse { Description = \"Forbidden\" });\noperation.Security = new List<OpenApiSecurityRequirement>        {            new()            {                [new OpenApiSecurityScheme                {                    Reference = new OpenApiReference                    {                        Type = ReferenceType.SecurityScheme,                        Id = SwaggerServiceExtensions.BearerSchemeId                    }                }] = Array.Empty<string>()            }        };    }}\n```\n\nThen, in SwaggerServiceExtensions, replace the whole options.AddSecurityRequirement(...) call with:\n\n```\n// File: OpenApi/SwaggerServiceExtensions.cs (inside AddSwaggerGen)options.OperationFilter<AuthorizeOperationFilter>();\n```\n\nEndpointMetadata contains attributes from both the controller and the action, so a class-level [Authorize] with one [AllowAnonymous] action is handled correctly.\n\nChecking the IAuthorizeData and IAllowAnonymous interfaces also catches [Authorize(Roles = \"Admin\")] and custom attributes that implement them.\n\nTryAdd avoids a duplicate key exception when an action already declares [ProducesResponseType(401)].\n\nOn Swashbuckle 10, change the using to Microsoft.OpenApi and replace the last two statements with:\n\n```\n// File: OpenApi/AuthorizeOperationFilter.cs (Swashbuckle 10 version of the end of Apply)operation.Responses ??= new OpenApiResponses();operation.Responses.TryAdd(\"401\", new OpenApiResponse { Description = \"Unauthorized\" });operation.Responses.TryAdd(\"403\", new OpenApiResponse { Description = \"Forbidden\" });\noperation.Security =[    new OpenApiSecurityRequirement    {        [new OpenApiSecuritySchemeReference(SwaggerServiceExtensions.BearerSchemeId, context.Document)] = []    }];\n```\n\ncontext.Document is the document being generated, which plays the same role as the document argument in Step 2.\n\nStep 4: Add a protected endpoint to test with\n\n```\n// File: Auth/MeController.csusing Microsoft.AspNetCore.Authorization;using Microsoft.AspNetCore.Mvc;\nnamespace Store.Api.Auth;\n[ApiController][Route(\"api/me\")][Authorize]public sealed class MeController : ControllerBase{    [HttpGet]    public IActionResult Get() => Ok(new    {        userId = User.FindFirst(\"sub\")?.Value,        email = User.FindFirst(\"email\")?.Value,        roles = User.FindAll(\"role\").Select(c => c.Value)    });\njs\n    [HttpGet(\"ping\")]    [AllowAnonymous]    public IActionResult Ping() => Ok(new { status = \"ok\" });}\n```\n\nThis controller gives you one locked and one open operation. The AuthController from Issuing and Validating JWT Access Tokens in ASP.NET Core already has such a pair (GET /api/auth/me and POST /api/auth/login), so skip this step if you prefer to test with those. The claim names sub, email, and role match the series convention (MapInboundClaims = false).\n\nStep 5: Add the bearer scheme to the built-in document (Path B)\n\nOn .NET 9 and later, AddOpenApi() generates the document and transformers let you edit it. This is the .NET 10 version (Microsoft.OpenApi 2.x).\n\n```\n// File: OpenApi/BearerSecuritySchemeTransformer.csusing Microsoft.AspNetCore.OpenApi;using Microsoft.OpenApi;\nnamespace Store.Api.OpenApi;\npublic sealed class BearerSecuritySchemeTransformer : IOpenApiDocumentTransformer{    public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)    {        document.Components ??= new OpenApiComponents();        document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();        document.Components.SecuritySchemes[\"Bearer\"] = new OpenApiSecurityScheme        {            Type = SecuritySchemeType.Http,            Scheme = \"bearer\",            BearerFormat = \"JWT\",            Description = \"Paste the access token only.\"        };        return Task.CompletedTask;    }}\n```\n\nOn .NET 9 (Microsoft.OpenApi 1.x), use using Microsoft.OpenApi.Models; and delete the SecuritySchemes ??= line, because that dictionary is already created and typed IDictionary<string, OpenApiSecurityScheme>.\n\nStep 6: Mark protected operations in the built-in document\n\n```\n// File: OpenApi/AuthorizeOperationTransformer.csusing Microsoft.AspNetCore.Authorization;using Microsoft.AspNetCore.OpenApi;using Microsoft.OpenApi;\nnamespace Store.Api.OpenApi;\npublic sealed class AuthorizeOperationTransformer : IOpenApiOperationTransformer{    public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)    {        var metadata = context.Description.ActionDescriptor.EndpointMetadata;        if (!metadata.OfType<IAuthorizeData>().Any() || metadata.OfType<IAllowAnonymous>().Any())        {            return Task.CompletedTask;        }\noperation.Security ??= [];        operation.Security.Add(new OpenApiSecurityRequirement        {            [new OpenApiSecuritySchemeReference(\"Bearer\", context.Document)] = []        });        return Task.CompletedTask;    }}\n```\n\nOperation transformers run before document transformers, so the scheme does not exist yet when this code runs. ASP.NET Core 10 registers the document’s components after all transformers finish, so the reference resolves when the JSON is written.\n\ncontext.Document exists only on .NET 10. On .NET 9, use using Microsoft.OpenApi.Models;, initialize with new List<OpenApiSecurityRequirement>(), and use new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = \"Bearer\" } } as the key with Array.Empty<string>().\n\nStep 7: Register everything in Program.cs\n\nAdd using Store.Api.OpenApi; at the top of Program.cs (plus using Scalar.AspNetCore; if you choose Scalar). Then, for Path A or A2, before builder.Build():\n\n```\n// File: Program.cs (before builder.Build())builder.Services.AddSwaggerWithJwt();\n```\n\nPath A or A2, in the pipeline after builder.Build():\n\n```\n// File: Program.cs (after builder.Build(), before app.MapControllers())if (app.Environment.IsDevelopment()){    app.UseSwagger();    app.UseSwaggerUI(options =>    {        options.SwaggerEndpoint(\"/swagger/v1/swagger.json\", \"Store API v1\");        options.EnablePersistAuthorization();    });}\n```\n\nPath B, before builder.Build():\n\n```\n// File: Program.cs (before builder.Build())builder.Services.AddOpenApi(options =>{    options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();    options.AddOperationTransformer<AuthorizeOperationTransformer>();});\n```\n\nPath B, after builder.Build(), with Scalar at /scalar or Swagger UI at /swagger:\n\n```\n// File: Program.cs (after builder.Build(), before app.MapControllers())if (app.Environment.IsDevelopment()){    app.MapOpenApi();    app.MapScalarApiReference(options => options        .AddPreferredSecuritySchemes(\"Bearer\")        .EnablePersistentAuthentication());\njs\n    app.UseSwaggerUI(options =>    {        options.SwaggerEndpoint(\"/openapi/v1.json\", \"Store API v1\");        options.EnablePersistAuthorization();    });}\n```\n\nIsDevelopment() keeps the document and both UIs out of production. A browser that opens /swagger cannot attach your JWT, so if you must expose docs in staging, protect them at the network level (VPN or an IP allow list on the reverse proxy).\n\nEnablePersistAuthorization() (Swashbuckle 6.2 and later) and Scalar's EnablePersistentAuthentication() store the token in browser storage so a reload keeps it. Keep them out of shared environments.\n\nKeep only the UI you want. Both are shown so you can compare.\n\nTry it\n\nRun the API and open https://localhost:7001/swagger (or /scalar).\n\nExpand POST /api/auth/login, click Try it out, send your test user's credentials, and copy accessToken from the response.\n\nClick Authorize, paste the token without any prefix, click Authorize, then Close.\n\nRun GET /api/me. The request shows -H 'Authorization: Bearer eyJ...' and returns 200.\n\nThe same flow as an .http file:\n\n```\n@accessToken = paste-the-token-here\n### Open endpoint: no lock, no token neededGET https://localhost:7001/api/me/ping\n### Protected endpoint without a token: expect 401GET https://localhost:7001/api/me\n### Protected endpoint with the token: expect 200GET https://localhost:7001/api/meAuthorization: Bearer {{accessToken}}\n### The double prefix mistake: expect 401GET https://localhost:7001/api/meAuthorization: Bearer Bearer {{accessToken}}\n```\n\nExpected results:\n\n``` php\nGET /api/me/ping                 -> 200 {\"status\":\"ok\"}GET /api/me (no header)          -> 401, WWW-Authenticate: BearerGET /api/me (token)              -> 200 {\"userId\":\"...\",\"email\":\"ada@example.com\",\"roles\":[\"Customer\"]}GET /api/me (\"Bearer Bearer ...\") -> 401, WWW-Authenticate: Bearer error=\"invalid_token\"\n```\n\nThen open the raw document (/swagger/v1/swagger.json or /openapi/v1.json). /api/me has \"security\": [ { \"Bearer\": [ ] } ] and /api/me/ping has no security key.\n\nProduction checklist\n\nMap the OpenAPI document and UI only in Development, or protect them at the network edge.\n\nNever put a real token in a scheme description or in Scalar’s prefilled authentication.\n\nKeep persisted authorization out of shared environments, because any script on that origin can read browser storage.\n\nKeep exactly one Microsoft.OpenApi major version in the project, and pin the Swashbuckle major in the .csproj.\n\nRead the Swashbuckle release notes before you move from 9.x to 10.x, since your filters must change too.\n\nMake the lock match reality: if you add a fallback authorization policy, update the filter so operations without [AllowAnonymous] get the lock.\n\nKeep access tokens short-lived (the series uses 15 minutes) and re-authorize in the UI when calls start returning 401.\n\nCommon pitfalls\n\nPasting Bearer eyJ... into an Http scheme. The UI sends Bearer Bearer eyJ... and the API returns 401 invalid_token. Paste the token alone, or switch to ApiKey and type the prefix yourself (not recommended).\n\nCopying a Swashbuckle 6 sample into a Swashbuckle 10 project. Microsoft.OpenApi.Models no longer exists. Use Step 2, or pin 9.0.6 with --version.\n\nMixing Microsoft.OpenApi 1.x and 2.x. Swashbuckle 10 next to Microsoft.AspNetCore.OpenApi 9.x builds fine, then every document request returns 500 with TypeLoadException: Could not load type 'Microsoft.OpenApi.Models.OpenApiDocument'. Align the versions as the table shows.\n\nA scheme with no requirement. The Authorize button appears, but no operation references the scheme, so the UI never sends the header. Add Step 1’s requirement or Step 3’s filter.\n\nMismatched scheme ids. AddSecurityDefinition(\"Bearer\", ...) with a reference to \"bearer\" does not resolve. Microsoft.OpenApi 2.x writes an empty security object, and 1.x points at a scheme that does not exist. Either way, no header is sent. Use one constant.\n\nGlobal authorization the filter cannot see. A fallback policy or a global AuthorizeFilter adds no endpoint metadata, so the filter shows no locks. Invert the check to lock everything without IAllowAnonymous.", "url": "https://wpnews.pro/news/adding-a-jwt-authorize-button-to-swagger-ui", "canonical_source": "https://blog.devgenius.io/adding-a-jwt-authorize-button-to-swagger-ui-71c63a7bd133?source=rss----4e2c1156667e---4", "published_at": "2026-11-10 09:32:45+00:00", "updated_at": "2026-11-10 09:50:09.761154+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Softinbit Technology", "Swagger UI", "Scalar", "Swashbuckle", "Microsoft.OpenApi", "Microsoft.AspNetCore.OpenApi", "ASP.NET Core", ".NET 10"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/adding-a-jwt-authorize-button-to-swagger-ui", "markdown": "https://wpnews.pro/news/adding-a-jwt-authorize-button-to-swagger-ui.md", "text": "https://wpnews.pro/news/adding-a-jwt-authorize-button-to-swagger-ui.txt", "jsonld": "https://wpnews.pro/news/adding-a-jwt-authorize-button-to-swagger-ui.jsonld"}}