DEV Community

Cover image for C# Minimal API: Multiple Authentication Schemes with Swagger Support
Daniel Balcarek
Daniel Balcarek

Posted on

C# Minimal API: Multiple Authentication Schemes with Swagger Support

Minimal APIs make it easy to get started quickly, but production APIs almost always require authentication and authorization. In real-world systems, it’s also common to support multiple authentication methods for example, JWT for public clients and Basic authentication for internal tooling or service to service access.

Swagger is invaluable for local development and quick endpoint testing, but once multiple authentication schemes are involved, it requires explicit configuration to work correctly.

This article walks through a practical setup for configuring authentication in ASP.NET Core Minimal APIs, including JWT and a custom Basic authentication handler and preparing the API for proper authorization and Swagger integration.

Note: For detailed background about authentication and authorization in Minimal APIs, see the official documentation: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/security

Authentication

To enable authentication in a Minimal API application, authentication services must be registered using AddAuthentication. This registers the authentication infrastructure and allows endpoints to declare authentication requirements.

In this example, JWT Bearer authentication is configured as the default scheme. To use it, you need to install the following NuGet package:

A minimal authentication setup looks like this:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication().AddJwtBearer();

var app = builder.Build();

app.MapGet("/", () => "Our Auth Example");
app.Run();
Enter fullscreen mode Exit fullscreen mode

Note: While WebApplication can automatically inject authentication middleware in Minimal API applications, I recommend explicitly calling UseAuthentication() in Program.cs when middleware ordering matters or when clarity is important in larger applications. The same applies to UseAuthorization().

Explicit registration gives you full control over pipeline order—for example, ensuring authentication runs after a global exception-handling middleware so that exceptions thrown by custom authentication handlers or token validation logic are properly captured. The same considerations apply to authorization middleware.

JWT configuration

A basic JWT configuration can be provided via appsettings.json:

  "Authentication": {
    "Schemes": {
      "Bearer": {
        "ValidAudiences": [
          "https://localhost:4200",
          "http://localhost:4200"
        ],
        "ValidIssuer": "secret-issuer"
      }
    }
  },
Enter fullscreen mode Exit fullscreen mode

Note: This example shows audience and issuer validation. For production use, you must also configure token signing key validation. See JWT authentication in ASP.NET Core for complete configuration details.

Custom authentication method: Basic Authentication

In some scenarios, more than one authentication method is required. A common example is exposing internal or administrative endpoints protected by Basic authentication, while public endpoints use JWT.

For demonstration purposes, this example adds a custom Basic authentication scheme.

First, extend appsettings.json with a Basic authentication section:

  "Authentication": {
    "Schemes": {
      "Bearer": {
        "ValidAudiences": [
          "https://localhost:4200",
          "http://localhost:4200"
        ],
        "ValidIssuer": "secret-issuer"
      },
      "Basic": {
        "UserName": "admin",
        "Password": "admin"
      }
    }
  },
Enter fullscreen mode Exit fullscreen mode

Then register the scheme in Program.cs, but don’t forget to also set the DefaultScheme, DefaultChallengeScheme, DefaultAuthenticateScheme properties. This ensures JWT is used as the default for authentication and challenges, while Basic authentication is only applied when explicitly requested via authorization policies.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(options =>
{
     options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
     options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
     options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer()
.AddScheme<BasicAuthOptions, BasicAuthenticationHandler>("Basic", (options) =>
{
     var userName = configuration["Authentication:Schemes:Basic:UserName"];
     var password = configuration["Authentication:Schemes:Basic:Password"];

     if (string.IsNullOrWhiteSpace(userName))
     {
          throw new InvalidOperationException("Basic authentication username is not configured.");
     }

     if (string.IsNullOrWhiteSpace(password))
     {
          throw new InvalidOperationException("Basic authentication password is not configured.");
     }

     options.UserName = userName;
     options.Password = password;
});

var app = builder.Build();

app.MapGet("/", () => "Our Auth Example");
app.Run();
Enter fullscreen mode Exit fullscreen mode

Here, AddScheme<TOptions, THandler> registers a custom authentication scheme backed by a custom authentication handler. The options are bound from configuration and passed to the handler at runtime.

Create AuthenticationConfiguration

As we are always trying to keep our Program.cs file as clean as possible, let’s create an extension method that configures authentication:

public static class BasicSchemeDefaults
{
    public static readonly string AuthenticationScheme = "Basic";
}

public static class AuthenticationConfiguration
{
    public static IServiceCollection ConfigureAuthentication(this IServiceCollection serviceCollection, IConfiguration configuration)
    {
        serviceCollection.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer().AddScheme<BasicAuthOptions, BasicAuthenticationHandler>(BasicSchemeDefaults.AuthenticationScheme, (options) =>
        {
            var userName = configuration["Authentication:Schemes:Basic:UserName"];
            var password = configuration["Authentication:Schemes:Basic:Password"];

            if (string.IsNullOrWhiteSpace(userName))
            {
                throw new InvalidOperationException(
                    "Basic authentication username is not configured.");
            }

            if (string.IsNullOrWhiteSpace(password))
            {
                throw new InvalidOperationException(
                    "Basic authentication password is not configured.");
            }

            options.UserName = userName;
            options.Password = password;
        });

        return serviceCollection;
    }
}
Enter fullscreen mode Exit fullscreen mode

Implementing BasicAuthenticationHandler

To create a custom authentication handler, the handler must implement IAuthenticationHandler. In practice, this is usually done by inheriting from AuthenticationHandler<TOptions>.

The handler’s responsibility is to extract credentials from the request, validate them, and produce an AuthenticateResult.

public sealed class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthOptions>
{
    public BasicAuthenticationHandler(
        IOptionsMonitor<BasicAuthOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue("Authorization", out var authorizationHeader))
        {
            return AuthenticateResult.Fail("Missing Authorization header");
        }

        if (!AuthenticationHeaderValue.TryParse(authorizationHeader.ToString(), out var authHeader))
        {
            return AuthenticateResult.Fail("Invalid Authorization header");
        }

        if (string.IsNullOrEmpty(authHeader?.Parameter))
        {
            return AuthenticateResult.Fail("Missing Authorization Header parameter");
        }

        Span<byte> bytesBuffer = stackalloc byte[authHeader!.Parameter!.Length];
        if (!Convert.TryFromBase64String(authHeader.Parameter, bytesBuffer, out var bytesWritten))
        {
            return AuthenticateResult.Fail("Invalid Base64 string");
        }

        var credentials = Encoding.UTF8.GetString(bytesBuffer[..bytesWritten]).Split(':', 2);

        if (credentials.Length != 2)
        {
            return AuthenticateResult.Fail("Invalid credential format");
        }

        if (credentials[0] != this.Options.UserName || credentials[1] != this.Options.Password)
        {
            return AuthenticateResult.Fail("Invalid credentials");
        }

        var claims = new List<Claim>()
        {
            new(ClaimTypes.Name, credentials[0]),
            new(ClaimTypes.Role, AuthRoles.Administrator)
        };

        var claimsIdentity = new ClaimsIdentity(claims, Scheme.Name);
        var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

        return AuthenticateResult.Success(
            new AuthenticationTicket(
                claimsPrincipal,
                Scheme.Name));
    }
}
Enter fullscreen mode Exit fullscreen mode

Authentication options

The options class used by the handler must inherit from AuthenticationSchemeOptions:

public sealed class BasicAuthOptions : AuthenticationSchemeOptions
{
   public const string SectionName = "Basic";
   public string UserName { get; set; } = string.Empty;
   public string Password { get; set; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode

Important This Basic authentication implementation is intended for demonstration. Credentials must never be stored in appsettings.json in production. Use secure storage such as environment variables, secret managers, or Azure Key Vault.
Always use Basic authentication over HTTPS. Without HTTPS, credentials are exposed to anyone listening on the network, making your API extremely vulnerable.

Authorization

Authentication answers who the user is. Authorization answers what the user is allowed to do. In most real-world APIs, authenticated users do not have the same permissions, which means authorization policies are required.

ASP.NET Core authorization is policy-based. Policies can be scoped to:

  • Specific authentication schemes
  • Required roles or claims
  • Custom requirements

In this example, the API supports two authentication schemes:

  • JWT Bearer authentication
  • Basic authentication

Each scheme requires its own authorization rules.

Authorization configuration

To keep authorization setup explicit and reusable, policies are defined in a dedicated configuration class:

public static class AuthorizationConfiguration
{
    public static readonly string BasicPolicyName = "basicPolicy";
    public static readonly string UserPolicyName = "userPolicy";
    public static readonly string DeveloperPolicyName = "developerPolicy";
    public static readonly string AdministratorPolicyName = "administratorPolicy";

    public static IServiceCollection ConfigureAuthorization(this IServiceCollection serviceCollection)
    {
        serviceCollection.AddAuthorization(options =>
        {
            options.AddBearerPolicy(UserPolicyName, AuthRoles.AllRoles);
            options.AddBearerPolicy(DeveloperPolicyName, [AuthRoles.Developer, AuthRoles.Administrator]);
            options.AddBearerPolicy(AdministratorPolicyName, [AuthRoles.Administrator]);
            options.AddPolicy(BasicPolicyName, options =>
            {
                options.RequireAuthenticatedUser();
                options.AddAuthenticationSchemes(BasicSchemeDefaults.AuthenticationScheme);
            });
        });

        return serviceCollection;
    }

    public static void AddBearerPolicy(this AuthorizationOptions authorizationOptions, string policyName, IEnumerable<string> roles)
    {
        authorizationOptions.AddPolicy(policyName, policy =>
        {
          policy.RequireAuthenticatedUser();
          policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
          policy.RequireRole(roles);

        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Why AddAuthenticationSchemes matters

When multiple authentication schemes are registered, authorization policies must explicitly specify which scheme they apply to.

Without AddAuthenticationSchemes:

  • ASP.NET Core may attempt to authenticate using the default scheme, leading to unexpected failures when multiple schemes are registered.
  • Requests may fail even with valid credentials
  • Swagger testing becomes unreliable

In this configuration:

  • JWT-based policies explicitly require the Bearer scheme
  • The Basic policy explicitly requires the Basic scheme

This guarantees predictable behavior when both authentication methods coexist.

Role-based authorization for JWT

JWT policies use role-based authorization:

policy.RequireAuthenticatedUser();
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireRole(roles);
Enter fullscreen mode Exit fullscreen mode

This assumes roles are:

  • Embedded in the JWT token
  • Mapped to ClaimTypes.Role (or configured accordingly)

Each policy restricts access to a specific role set:

  • User policy → any authenticated user
  • Developer policy → developer or administrator
  • Administrator policy → administrator only

Basic authentication authorization

The Basic authentication policy is intentionally simple:

policy.RequireAuthenticatedUser();
policy.AddAuthenticationSchemes(BasicSchemeDefaults.AuthenticationScheme);
Enter fullscreen mode Exit fullscreen mode

This ensures:

  • Only authenticated Basic credentials are accepted
  • Authorization remains explicit and isolated

Applying authorization to endpoints

To use it in our endpoints we have to call RequireAuthorization method with specific policy name:

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureAuthentication(builder.Configuration);
builder.Services.ConfigureAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/jwt", () => "Our jwt Auth Example")
.RequireAuthorization(AuthorizationConfiguration.UserPolicyName);
app.MapGet("/basic", () => "Our basic Auth Example")
.RequireAuthorization(AuthorizationConfiguration.BasicPolicyName);
app.Run();
Enter fullscreen mode Exit fullscreen mode

Each endpoint clearly declares:

  • Which policy it requires
  • Which authentication scheme is expected
  • Which roles (if any) are allowed

Swagger

And finally swagger configuration. Let's start from basics. Extension method below configures Swagger and adds it to your dependency injection container. And with SwaggerDoc creates the main Swagger document with version "v1".

public static IServiceCollection ConfigureSwagger(this IServiceCollection serviceCollection)
{
    return serviceCollection.AddSwaggerGen(options =>
    {
        options.SwaggerDoc(_version, new OpenApiInfo()
        {
            Version = _version,
        });
    });
}
Enter fullscreen mode Exit fullscreen mode

JWT Bearer Authentication Setup

options.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, CreateScheme());

options.AddSecurityRequirement(document => new OpenApiSecurityRequirement
{
    [new OpenApiSecuritySchemeReference(JwtBearerDefaults.AuthenticationScheme, document)] = []
});
Enter fullscreen mode Exit fullscreen mode

What this does:

  • AddSecurityDefinition - Tells Swagger "this API uses JWT Bearer tokens for authentication"
  • AddSecurityRequirement - Tells Swagger "users must provide a JWT token to test endpoints"

The CreateScheme() method defines how JWT authentication works:

private static OpenApiSecurityScheme CreateScheme()
{
    return new OpenApiSecurityScheme()
    {
        Name = "JWT Bearer token",          // Display name in Swagger UI
        Type = SecuritySchemeType.Http,     // Uses HTTP authentication
        Scheme = JwtBearerDefaults.AuthenticationScheme,  // "Bearer - Scheme name"
        BearerFormat = "JWT",               // Token format
        Description = "JWT Bearer token Authorization"
    };
}
Enter fullscreen mode Exit fullscreen mode

Basic Authentication Setup

options.AddSecurityDefinition(BasicSchemeDefaults.AuthenticationScheme, CreateBasicScheme());

options.AddSecurityRequirement(document => new OpenApiSecurityRequirement
{
    [new OpenApiSecuritySchemeReference(BasicSchemeDefaults.AuthenticationScheme, document)] = []
});
Enter fullscreen mode Exit fullscreen mode

Same pattern as JWT, but for Basic authentication (username + password).

The CreateBasicScheme() method:

private static OpenApiSecurityScheme CreateBasicScheme()
{
    return new OpenApiSecurityScheme()
    {
        Name = "Basic Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = BasicSchemeDefaults.AuthenticationScheme,  // "Basic" Scheme name
        In = ParameterLocation.Header,      // Sent in HTTP headers
        Description = "Enter your username and password."
    };
}
Enter fullscreen mode Exit fullscreen mode

Let's put it together in one final Swagger UI extension method:

public static class SwaggerConfiguration
{
    private static readonly string _version = "v1";
    public static IServiceCollection ConfigureSwagger(this IServiceCollection serviceCollection)
    {
        return serviceCollection.AddSwaggerGen(options =>
        {
            options.SwaggerDoc(_version, CreateInfo());
            options.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, CreateScheme());
            options.AddSecurityRequirement(document => new OpenApiSecurityRequirement
            {
                [new OpenApiSecuritySchemeReference(JwtBearerDefaults.AuthenticationScheme, document)] = []
            });
            options.AddSecurityDefinition(BasicSchemeDefaults.AuthenticationScheme, CreateBasicScheme());
            options.AddSecurityRequirement(document => new OpenApiSecurityRequirement
            {
                [new OpenApiSecuritySchemeReference(BasicSchemeDefaults.AuthenticationScheme, document)] = []
            });
        });
    }

    private static OpenApiSecurityScheme CreateScheme()
    {
        return new OpenApiSecurityScheme()
        {
            Name = "JWT Bearer token",
            Type = SecuritySchemeType.Http,
            Scheme = JwtBearerDefaults.AuthenticationScheme,
            BearerFormat = "JWT",
            Description = "JWT Bearer token Authorization",
        };
    }

    private static OpenApiSecurityScheme CreateBasicScheme()
    {
        return new OpenApiSecurityScheme()
        {
            Name = "Basic Authorization",
            Type = SecuritySchemeType.Http,
            Scheme = BasicSchemeDefaults.AuthenticationScheme,
            In = ParameterLocation.Header,
            Description = "Enter your username and password.",
        };
    }

    private static OpenApiInfo CreateInfo()
    {
        return new OpenApiInfo()
        {
            Version = _version,
        };
    }

Enter fullscreen mode Exit fullscreen mode

Note: Swagger security definitions are declared at the API level and describe which authentication mechanisms are supported globally. Actual access control such as JWT or Basic policies is enforced per endpoint through your authorization policies.

And program.cs will look like this:

var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureSwagger();
builder.Services.ConfigureAuthentication(builder.Configuration);
builder.Services.ConfigureAuthorization();

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/jwt", () => "Our jwt Auth Example").RequireAuthorization(AuthorizationConfiguration.UserPolicyName);
app.MapGet("/basic", () => "Our basic Auth Example").RequireAuthorization(AuthorizationConfiguration.BasicPolicyName);
app.Run();
Enter fullscreen mode Exit fullscreen mode

When you open Swagger UI you'll see:

  1. 🔒 Authorize Button - Click to authenticate
  2. Two authentication options:
    • Bearer - Paste your JWT token
    • Basic - Enter username/password

Note Usually UseSwagger and UseSwaggerUI methods are under if statement because we use them in development environments:

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
Enter fullscreen mode Exit fullscreen mode

Solution Examples

The following example project demonstrates multiple authentication schemes with swagger support: AuthApi

Conclusion

Securing Minimal APIs requires deliberate design and careful implementation. By combining multiple authentication schemes, clearly defined authorization policies, and a properly configured Swagger setup, you ensure your endpoints remain predictable, secure, and maintainable, even as your API scales.

Top comments (0)