New Livestream: How Banks Protect Their Apps with FAPI 2.0.

Register Now!

Swagger Authentication in .NET 10 with OAuth 2.0 and PKCE

Khalid Abuhakmeh
Two blue circles

This guide shows how to configure JWT bearer token validation for an ASP.NET Core API, describe an OAuth 2.0 authorization code flow in the generated OpenAPI document, and enable PKCE in Swagger UI so it can obtain and send access tokens automatically. The examples use Duende IdentityServer's public demo instance as the token service.

Note: This configuration protects API requests made to endpoints like /api/random. It does not restrict access to the Swagger UI page itself or to the /openapi/v1.json document; both remain publicly reachable unless you add a separate access gate in front of them.
  • Pasting an existing bearer token into Swagger UI's Authorize dialog lets you test a protected endpoint immediately, without triggering any OAuth flow.
  • Using the OAuth 2.0 authorization code flow with PKCE has Swagger UI redirect to your identity provider, obtain a token on your behalf, and attach it to requests automatically.
  • Gating access to the Swagger UI page or the OpenAPI document itself, so only authenticated developers can view the documentation, is a separate concern not covered by the JWT bearer or OAuth 2.0 configuration in this article.

Prerequisites and configuration checklist

  • .NET 10 SDK installed
  • The three NuGet packages used in this tutorial: Microsoft.AspNetCore.Authentication.JwtBearer, Microsoft.AspNetCore.OpenApi, and Swashbuckle.AspNetCore.SwaggerUI
  • A local HTTPS address that matches what you register with your identity provider
  • A redirect URI registered with your identity provider, typically https://<your-host>:<port>/swagger/oauth2-redirect.html (Swagger UI's default OAuth2 redirect route)
  • Your local origin allowed by the identity provider's token endpoint (CORS)
  • A client and the scopes you intend to use registered with your identity provider

This checklist assumes you're configuring your own identity provider. Use your own host, port, and allowed origins rather than assuming the demo instance's values apply to your setup.

Setting up the API project

Note: All library versions in this tutorial target .NET 10, so class names may differ in lower package versions, such as .NET 9.

We’ll start with a simple ASP.NET Core project using the Empty template. This template creates a Minimal API project with a single endpoint.

Csharp

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

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

app.Run();

So far, so good. Let’s add the Microsoft.AspNetCore.Authentication.JwtBearer package to our project. If you want a closer look at how AddJwtBearer works outside of Swagger, our step-by-step JWT authentication tutorial covers it in depth.

Bash

dotnet package add Microsoft.AspNetCore.Authentication.JwtBearer

We’ll now configure our JWT Bearer authentication options and add a brand-new API endpoint.

Csharp

using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication()
    .AddJwtBearer(options =>
    {
        options.Authority = "https://demo.duendesoftware.com";
        options.Audience = "api";
        options.TokenValidationParameters = new()
        {
            NameClaimType = "name",
            RoleClaimType = "role"
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");
app.MapGet("/api/random", (ClaimsPrincipal user) =>
        new
        {
            name = user.Identity?.Name,
            value = Random.Shared.Next(1, 100)
        })
    .RequireAuthorization();

app.Run();

Opening a browser and requesting a response from /api/random should return a 401 Unauthorized response. Let’s move on to adding our OpenAPI specification.

Adding OpenAPI Specifications

To use Microsoft’s OpenAPI specification, we will need to add the following package to our existing ASP.NET Core project.

Bash

dotnet package add Microsoft.AspNetCore.OpenApi

This library generates the OpenAPI specification by traversing all known endpoints in our project and adding them to a JSON endpoint. Let’s update our code with a few goals in mind.

  1. Add OpenAPI code to our services collection with options
  2. Map the OpenAPI JSON specification endpoint
  3. Exclude the “Hello, World” endpoint from the specification

Csharp

using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication()
    .AddJwtBearer(options =>
    {
        options.Authority = "https://demo.duendesoftware.com";
        options.Audience = "api";
        options.TokenValidationParameters = new()
        {
            NameClaimType = "name",
            RoleClaimType = "role"
        };
    });

builder.Services.AddAuthorization();
builder.Services.AddOpenApi(options =>
{
    // todo: add security definition
});

var app = builder.Build();

// maps to /openapi/v1.json
app.MapOpenApi();

app.MapGet("/", () => "Hello World!")
    // ignore this endpoint from OpenAPI document
    .ExcludeFromDescription();

app.MapGet("/api/random", (ClaimsPrincipal user) =>
        new
        {
            name = user.Identity?.Name,
            value = Random.Shared.Next(1, 100)
        })
    .RequireAuthorization();

app.Run();

Visiting the endpoint /openapi/v1.json in the browser should now return the ASP.NET Core application’s OpenAPI specification.

Json

{
  "openapi": "3.1.1",
  "info": {
    "title": "OpenApiSample | v1",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://localhost:5155/"
    }
  ],
  "paths": {
    "/api/random": {
      "get": {
        "tags": [
          "OpenApiSample"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnonymousTypeOfstringAndint"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AnonymousTypeOfstringAndint": {
        "required": [
          "name",
          "value"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": [
              "null",
              "string"
            ]
          },
          "value": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "format": "int32"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "OpenApiSample"
    }
  ]
}

You’ll notice there is no mention of security requirements in the JSON document, even if the API expects incoming requests to be authenticated. Let’s fix that next.

Adding an OpenAPI Security Requirement

The OpenAPI specification allows developers to add security requirements to all endpoints defined in the document. The Microsoft OpenAPI library exposes mutable functionality through the document transformer abstraction. We’ll be using a transformer to modify the OpenAPI document and add a global security requirement for all endpoints.

When it comes to web security, there are a few options, but we’re interested in using Duende IdentityServer to generate a JWT that our Swagger UI can use.

Before we get too far ahead of ourselves, let’s add the security requirement to our OpenAPI scheme.

Csharp

using System.Security.Claims;
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication()
    .AddJwtBearer(options =>
    {
        options.Authority = "https://demo.duendesoftware.com";
        options.Audience = "api";
        options.TokenValidationParameters = new()
        {
            NameClaimType = "name",
            RoleClaimType = "role"
        };
    });

builder.Services.AddAuthorization();
builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        // Ensure instances exist
        document.Components ??= new OpenApiComponents();
        document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();

        
        
        // Add OAuth2 security scheme (Authorization Code flow only)
        document.Components.SecuritySchemes.Add("oauth2", new OpenApiSecurityScheme
        {
            Type = SecuritySchemeType.OAuth2,
            Flows = new OpenApiOAuthFlows
            {
                AuthorizationCode = new OpenApiOAuthFlow
                {
                    AuthorizationUrl = new Uri("https://demo.duendesoftware.com/connect/authorize"),
                    TokenUrl = new Uri("https://demo.duendesoftware.com/connect/token"),
                    Scopes = new Dictionary<string, string>
                    {
                        { "api", "Access the Weather API" },
                        { "openid", "Access the OpenID Connect user profile" },
                        { "email", "Access the user's email address" },
                        { "profile", "Access the user's profile" }
                    }
                }
            }
        });

        // Apply security requirement globally
        document.Security = [
            new OpenApiSecurityRequirement
            {
                {
                    new OpenApiSecuritySchemeReference("oauth2"),
                    ["api", "profile", "email", "openid"]
                }
            }
        ];
        
        // Set the host document for all elements
        // including the security scheme references
        document.SetReferenceHostDocument();

        return Task.CompletedTask;
    });
});

var app = builder.Build();

// maps to /openapi/v1.json
app.MapOpenApi();

app.MapGet("/", () => "Hello World!")
    // ignore this endpoint from OpenAPI document
    .ExcludeFromDescription();

app.MapGet("/api/random", (ClaimsPrincipal user) =>
        new
        {
            name = user.Identity?.Name,
            value = Random.Shared.Next(1, 100)
        })
    .RequireAuthorization();

app.Run();

Essential to adding an OAuth security requirement is setting the authorization and token URLs to point to the demo instance of Duende IdentityServer at demo.duendesoftware.com. The demo instance will be our token service, but you may substitute your own. The scopes listed here describe what the OpenAPI document advertises as available for a client to request; they document intent for consumers of the specification.

Scopes and audiences serve different purposes. The api scope represents permission to access the API. The JWT bearer handler separately validates the token's audience against the configured value, also named api in this sample. openid, profile, and email are OpenID Connect identity scopes used to request authentication and user information. They do not enforce API permissions or guarantee that particular claims appear in an access token. Your identity provider's configuration determines the access token's audience and claims.

Declaring these scopes in the OpenAPI document tells Swagger UI what to request during the OAuth flow, but the ASP.NET Core API in this tutorial only calls RequireAuthorization(), which requires an authenticated user following successful JWT validation. It does not enforce a specific scope policy. If your API needs to require a particular scope, add an explicit authorization policy that checks for it; that isn't implemented in this tutorial.

Now, let’s get to the fun part. Putting it all together with the Swagger UI.

Adding the Swagger UI

Adding the Swagger UI is a personal choice: you can use an NPM package, host the files statically, or take another approach. For tutorial purposes, the most straightforward approach is to use the existing Swashbuckle package. In the same project, let’s add the package and set up our options.

Bash

dotnet package add Swashbuckle.AspNetCore.SwaggerUI

From here, we need to connect our specification and UI element. Let’s modify our code one last time. We have three tasks to accomplish in this step.

  1. Map the Swagger UI endpoint.
  2. Point swagger to our OpenAPI specification
  3. Enable Proof Key for Code Exchange (PKCE) for OAuth

Csharp

using System.Security.Claims;
using Microsoft.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication()
    .AddJwtBearer(options =>
    {
        options.Authority = "https://demo.duendesoftware.com";
        options.Audience = "api";
        options.TokenValidationParameters = new()
        {
            NameClaimType = "name",
            RoleClaimType = "role"
        };
    });

builder.Services.AddAuthorization();
builder.Services.AddOpenApi(options =>
{
    options.AddDocumentTransformer((document, context, cancellationToken) =>
    {
        // Ensure instances exist
        document.Components ??= new OpenApiComponents();
        document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();

        
        
        // Add OAuth2 security scheme (Authorization Code flow only)
        document.Components.SecuritySchemes.Add("oauth2", new OpenApiSecurityScheme
        {
            Type = SecuritySchemeType.OAuth2,
            Flows = new OpenApiOAuthFlows
            {
                AuthorizationCode = new OpenApiOAuthFlow
                {
                    AuthorizationUrl = new Uri("https://demo.duendesoftware.com/connect/authorize"),
                    TokenUrl = new Uri("https://demo.duendesoftware.com/connect/token"),
                    Scopes = new Dictionary<string, string>
                    {
                        { "api", "Access the Weather API" },
                        { "openid", "Access the OpenID Connect user profile" },
                        { "email", "Access the user's email address" },
                        { "profile", "Access the user's profile" }
                    }
                }
            }
        });

        // Apply security requirement globally
        document.Security = [
            new OpenApiSecurityRequirement
            {
                {
                    new OpenApiSecuritySchemeReference("oauth2"),
                    ["api", "profile", "email", "openid"]
                }
            }
        ];
        
        // Set the host document for all elements
        // including the security scheme references
        document.SetReferenceHostDocument();

        return Task.CompletedTask;
    });
});

var app = builder.Build();

// maps to /openapi/v1.json
app.MapOpenApi();

// add Swagger UI and point to the OpenAPI document
// also enable PKCE for OAuth2
app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/openapi/v1.json", "v1");
    options.OAuthUsePkce();
});

app.MapGet("/", () => "Hello World!")
    // ignore this endpoint from OpenAPI document
    .ExcludeFromDescription();

app.MapGet("/api/random", (ClaimsPrincipal user) =>
        new
        {
            name = user.Identity?.Name,
            value = Random.Shared.Next(1, 100)
        })
    .RequireAuthorization();

app.Run();

We must enable Proof Key for Code Exchange (PKCE), or authentication with our Duende IdentityServer instance will fail. Enabling PKCE is a current best practice, so we recommend enabling it in your identity provider.

That’s it, let’s test our Swagger UI against our secured API.

Testing Swagger with a Secure API

Since we’re using a secure endpoint, we need some information about our identity provider, mainly the following data points.

  1. Client ID: interactive.confidential
  2. Client Secret: secret
Note: This client secret (secret) is provided only for testing against Duende's public demo instance; it is not a production secret. Browser-based clients such as Swagger UI cannot keep a confidential secret private, so a production deployment should register a public client and use the authorization code flow with PKCE without a client secret.

Once we start our ASP.NET Core project, we can navigate to /swagger/index.html to view the Swagger user interface. Importantly, be sure to start your application on HTTPS.

Swagger UI in .NET 10 with OAuth 2.0

Clicking the green Authorize button displays a dialog box where you can enter the client_id and client_secret from above, and select all the scopes. Before clicking the Authorize button in the dialog, verify that the flow value is authorizationCode with PKCE. If not, you forgot to enable the feature in the Swagger options in your C# code.

Available authorizations

Clicking Authorize will redirect you to the Duende IdentityServer instance, where you can now log in using the username and password combination of bob and bob.

Login with IdentityServer in .NET 10 and Swagger UI

Once redirected back, you should see the following screen with clear Logout and Close buttons.

Authenticate IdentityServer.NET 10 and Swagger UI

Let’s close this dialog and test our /api/random endpoint. Clicking the Try it out button will now issue a secure request to our endpoint with a JSON response.

Swagger UI JWT authentication

Note that the curl command includes the Authorization Bearer header, which contains the JWT issued by Duende IdentityServer. You’ll also notice the value of “Bob Smith” in our JSON response, one of the claims found in the JWT, along with a random integer value generated on the server.

Troubleshooting

No Authorize button appears

Check that the security scheme and document transformer are registered before UseSwaggerUI, and that Swagger UI is pointed at the correct OpenAPI document.

Redirect URI mismatch after login

Confirm the exact registered redirect URI matches what Swagger UI uses, typically /swagger/oauth2-redirect.html, including scheme, host, and port.

CORS errors on the token request

Verify your identity provider allows the exact origin serving Swagger UI as an allowed CORS origin for the token endpoint.

401 Unauthorized after signing in

A successful sign-in in Swagger UI doesn't establish that the token is valid for your API. Check that the token's issuer, audience, and expiration match what the API's JWT bearer options expect.

403 Forbidden despite a valid token

This usually points to an authorization policy or scope requirement the token doesn't satisfy. Review any policies beyond RequireAuthorization() that check for specific scopes or claims.

Conclusion

In this article, you configured JWT bearer token validation for an ASP.NET Core API, added an OAuth 2.0 authorization code security requirement to the generated OpenAPI document, and enabled PKCE in Swagger UI so it can complete the flow against Duende IdentityServer. You also saw the difference between authenticating API requests and gating access to the documentation itself, and how the scopes declared in OpenAPI relate to the authorization actually enforced by RequireAuthorization().

If your architecture uses a Backend for Frontend instead of authenticating Swagger UI directly, see how we manage OpenAPI specifications with a Backend for Frontend host for a different approach to controlling documentation access.

Related Articles