
Introduction
Hey there! These past few days, I’ve had to add an additional authentication scheme to my API, and as usual, I had to roll up my sleeves and dive right in. You know how it goes: one day you’re peacefully sipping coffee and programming easy stuffs, and the next, you’re deep-diving into .NET authentication documentation like there’s no tomorrow. But you know what? It turned out to be quite an interesting experience, and I want to share it with you.
It turns out that implementing multiple authentication schemes in a .NET WebAPI is not only possible but can be really useful in many scenarios. So, grab your coffee (or tea, I don’t judge) and join me on this journey to make our API more flexible and secure.
The Scenario: WeatherForecast with Superpowers
We’re going to use the classic WebAPI example we all know: WeatherForecast. But we’ll give it a twist: we’ll implement two forms of authentication:
- JWT Bearer Token: For our regular users accessing through an application.
- API Key: For third-party integrations or programmatic access.
Step 1: Setting Up Authentication Settings
First, let’s create an AuthenticationSettings class to hold our authentication configuration:
internal sealed class AuthenticationSettings
{
public const string Section = "ApiAuthentication";
public string Secret { get; init; } = String.Empty;
public string Issuer { get; init; } = String.Empty;
public string Audience { get; init; } = String.Empty;
public string Authority { get; init; } = String.Empty;
public string ApiKey { get; init; } = String.Empty;
}
Next, we’ll add the corresponding section to our appsettings.json:
{
"ApiAuthentication": {
"Authority": "TBD",
"Audience": "weather",
"Issuer": "TBD",
"Secret": "TBD",
"ApiKey": "TBD"
}
}
Step 2: Creating Custom Authentication Extensions
Now, let’s create an AuthenticationExtensions class to encapsulate our authentication setup:
internal static class AuthenticationExtensions
{
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services,
IConfiguration configuration, IWebHostEnvironment environment)
{
services.Configure<AuthenticationSettings>(
configuration.GetSection(AuthenticationSettings.Section));
var authenticationOptions = new AuthenticationSettings();
configuration.GetSection(AuthenticationSettings.Section).Bind(authenticationOptions);
services.AddCustomAuthentication(authenticationOptions, environment);
return services;
}
private static void AddCustomAuthentication(this IServiceCollection services,
AuthenticationSettings authenticationSettings, IWebHostEnvironment environment)
{
IdentityModelEventSource.ShowPII = true;
services.AddAuthentication(options =>
{
options.DefaultScheme = "Authentication";
options.DefaultChallengeScheme = "Authentication";
})
.AddJwtBearer("Bearer", options =>
{
// This might vary depending on your specific case
options.RequireHttpsMetadata = true;
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuers =
[authenticationSettings.Authority, authenticationSettings.Issuer],
ValidateAudience = true,
ValidAudience = authenticationSettings.Audience,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
IssuerSigningKey =
new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(authenticationSettings.Secret)),
ClockSkew = TimeSpan.Zero
};
})
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(
"ApiKey", options => {options.ApiKey = authenticationSettings.ApiKey; })
.AddPolicyScheme("Authentication", "Bearer or ApiKey",
options =>
{
options.ForwardDefaultSelector = context =>
context.Request.Headers.ContainsKey("ApiKey") ? "ApiKey" : "Bearer";
});
if (environment.IsDevelopment())
{
services.AddAuthentication().AddJwtBearer("LocalAuthIssuer");
}
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme, "Authentication")
.Build();
});
}
}
Step 3: Implementing the ApiKeyAuthenticationHandler
Now, let’s create our authentication handler for the API Key:
public class ApiKeyAuthenticationHandler(
IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: AuthenticationHandler<ApiKeyAuthenticationOptions>(options, logger, encoder)
{
private const string ApiKeyHeaderName = "ApiKey";
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!this.Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyHeaderValues))
{
return Task.FromResult(AuthenticateResult.NoResult());
}
var providedApiKey = apiKeyHeaderValues.FirstOrDefault();
if (!this.ApiKeyIsValid(providedApiKey))
return Task.FromResult(AuthenticateResult.Fail("Invalid API Key"));
//
// This logic might vary depending on your specific case
//
var claims = new List<Claim>
{
new(ClaimTypes.Name, "System"),
new(ClaimTypes.Email, "System")
};
var identity = new ClaimsIdentity(claims, this.Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, this.Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
private bool ApiKeyIsValid(string? providedApiKey) =>
!String.IsNullOrEmpty(providedApiKey) && providedApiKey == this.Options.ApiKey;
}
Step 4: Updating WeatherForecastController
Now, let’s modify our controller to use authentication ([Authorize]):
[ApiController]
[Route("[controller]")]
[Authorize] // This will require authentication for all endpoints
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
Step 5: Configuring Swagger with Custom Extensions
To make our API documentation more useful and secure, we’ll create a custom Swagger extension. This will allow us to configure Swagger to support our two authentication methods and provide more detailed API information.
First, let’s create a SwaggerExtensions class:
internal static class SwaggerExtensions
{
public static IServiceCollection AddCustomSwaggerGen(this IServiceCollection services,
IConfiguration configuration)
{
var swaggerSettings =
configuration.GetSection(SwaggerSettings.Section).Get<SwaggerSettings>() ??
new SwaggerSettings();
var authenticationSettings =
configuration.GetSection(AuthenticationSettings.Section)
.Get<AuthenticationSettings>() ?? new AuthenticationSettings();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc(
"v1",
new OpenApiInfo { Title = swaggerSettings.Title, Version = "v1" });
c.OrderActionsBy((apiDesc) => $"{apiDesc.RelativePath}");
c.DescribeAllParametersInCamelCase();
c.CustomSchemaIds(type => type.ToString());
c.AddSecurityDefinition(
"Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Description =
$"Authorization bearer using {authenticationSettings.Issuer}/api/auth/token and AuthServer token",
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header
});
c.AddSecurityDefinition(
"ApiKey", new OpenApiSecurityScheme
{
Name = "ApiKey",
Description = "API Key Authentication",
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header,
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{ Type = ReferenceType.SecurityScheme, Id = "Bearer" }
},
new string[] { }
},
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{ Type = ReferenceType.SecurityScheme, Id = "ApiKey" }
},
new string[] { }
}
});
c.OperationFilter<AuthResponsesOperationFilter>();
});
return services;
}
}
Next, let’s implement the AuthResponsesOperationFilter class that we referenced in the Swagger configuration:
internal class AuthResponsesOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var authAttributes = context.MethodInfo.DeclaringType?.GetCustomAttributes(true)
.Union(context.MethodInfo.GetCustomAttributes(true))
.OfType<AuthorizeAttribute>();
var containsAuthAttributes = authAttributes == null || !authAttributes.Any();
if (!containsAuthAttributes &&
!context.ApiDescription.ActionDescriptor.EndpointMetadata.Any(m =>
m is AuthorizeAttribute))
{
return;
}
operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" });
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
},
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "ApiKey"
}
},
new string[] { }
}
}
};
}
}
Step 6: Configuring Program.cs
Finally, let’s update our Program.cs to use all these new components:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddCustomAuthentication(builder.Configuration, builder.Environment);
builder.Services.AddCustomSwaggerGen(builder.Configuration);
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Key Points to Note
- AddPolicyScheme: This allows us to dynamically select the authentication scheme based on the incoming request.
- ForwardDefaultSelector: This delegate determines which authentication scheme to use based on the presence of an «ApiKey» header.
- Custom Authentication Extensions: The
AddCustomAuthenticationmethod encapsulates all the authentication logic, making it easy to reuse and maintain. - Environment-specific configuration: We add an additional JWT bearer for local development environments.
- Custom Swagger Extension: The
AddCustomJobsSwaggerGenmethod encapsulates all the Swagger configuration logic, making it easy to reuse and maintain. - Multiple Security Schemes: We’ve defined both Bearer token and API Key authentication schemes in Swagger, allowing clients to understand and use both methods.
- Operation Filtering: The
AuthResponsesOperationFilterautomatically adds appropriate security requirements and response types to authenticated endpoints, improving the accuracy of our API documentation. - API Information: We’re using settings from configuration to populate API information like title and version, allowing for easy customization.
Potential Pitfalls
While implementing multiple authentication schemes can greatly enhance your API’s flexibility, there are some common issues you might encounter:
- 401 Unauthorized Errors: If you’re getting unexpected 401 errors, double-check that your
ForwardDefaultSelectoris correctly identifying the authentication scheme based on the request headers. - Swagger Not Including ApiKey: Ensure that your Swagger configuration correctly defines both authentication schemes and includes them in the security requirements.
- JWT Configuration Mismatch: If you’re having issues with JWT authentication, verify that the configuration in your application matches the JWT issuer’s settings, including issuer, audience, and signing key.
- Middleware Order: Remember that the order of middleware is crucial. Ensure that
UseAuthentication()comes beforeUseAuthorization()in your application pipeline. - Claims Not Propagating: If you’re not seeing the expected claims in your controller, make sure you’re creating and assigning them correctly in your custom authentication handler.
- Swagger UI Authentication: When testing your API through Swagger UI, make sure you’re providing the correct authentication information. Remember that you’ll need to prefix your JWT token with «Bearer » in the Authorization header.
- Conflicting Security Requirements: If you have endpoints that specifically require one type of authentication, you may need to create custom attributes or adjust the
AuthResponsesOperationFilterto handle these cases.
Conclusion
And there you have it: a WeatherForecast API with multiple authentication schemes. Now you can access your weather forecasts using either a JWT token or an API key. Isn’t it beautiful?
Implementing multiple authentication schemes might seem a bit daunting at first, but once you have it set up, you realize how flexible it makes your API. You can have different levels of access, support various types of clients, and all without having to reinvent the wheel each time.
Remember, API security is like a good umbrella on a rainy day: it’s better to have it and not need it than to need it and not have it. So go ahead and give your API that extra layer of security. Your users (and your future self) will thank you!
References
- ASP.NET Core Security Documentation
- JWT Authentication in ASP.NET Core
- Custom Authentication in ASP.NET Core
- Swagger/OpenAPI in ASP.NET Core
I hope you found this article helpful. And remember, in the world of development, there’s always something new to learn. Happy coding!