OpenAPI Documentation & API Key Authentication
Real carrier APIs like FedEx and UPS require an API key for every request and provide interactive documentation for developers. In this presentation, we set up both from the start so you can test every endpoint interactively as you build it through the rest of the course.
Why Documentation and Security from Day One
If you have ever integrated with the FedEx Tracking API or UPS OAuth flow, you know the first step is always: get your API credentials, then open the interactive docs to make your first test call. Our API follows the same pattern.
Setting these up now means:
- Every endpoint you build is immediately testable through Scalar's interactive explorer
- Authentication is built into the pipeline so controllers can use
[Authorize]and[AllowAnonymous]naturally - You learn the modern approach: ASP.NET Core ships built-in OpenAPI support, replacing the need for Swashbuckle
In the prerequisite course, you used Swashbuckle to generate Swagger documentation. ASP.NET Core now includes first-party OpenAPI support via Microsoft.AspNetCore.OpenApi, which integrates more tightly with the framework and is the recommended approach going forward.
Installing Packages
Add the OpenAPI and Scalar packages to the project:
bash1dotnet add src/ParcelTracking.Api package Microsoft.AspNetCore.OpenApi2dotnet add src/ParcelTracking.Api package Scalar.AspNetCore
Microsoft.AspNetCore.OpenApi provides the AddOpenApi() and MapOpenApi() methods that generate an OpenAPI document from your controllers at runtime. Scalar.AspNetCore renders that document as an interactive API explorer — similar to Swagger UI but with a modern interface.
Registering OpenAPI Services
In Program.cs, register the OpenAPI services:
csharp1builder.Services.AddOpenApi(options =>2{3 options.AddDocumentTransformer((document, context, ct) =>4 {5 document.Info = new OpenApiInfo6 {7 Title = "Parcel Tracking API",8 Version = "v1",9 Description = "A carrier-style parcel tracking REST API"10 };11 return Task.CompletedTask;12 });13});
AddOpenApi() registers the services that generate the OpenAPI document. The document transformer customizes the metadata (title, version, description) that appears at the top of the documentation page.
Mapping OpenAPI Endpoints
After building the app, map the OpenAPI document endpoint and the Scalar UI:
csharp1var app = builder.Build();23if (app.Environment.IsDevelopment())4{5 app.MapOpenApi();6 app.MapScalarApiReference();7}
MapOpenApi() exposes the raw OpenAPI JSON document at /openapi/v1.json. MapScalarApiReference() serves the interactive explorer at /scalar/v1. Both are mapped inside the development environment check so they are not exposed in production.
After starting the application, navigate to https://localhost:{port}/scalar/v1 to see the interactive documentation. You can expand any endpoint, fill in parameters, and click Send to make a real request — no external tool needed.
Why API Key Authentication
Our API models a carrier service. Real carrier APIs authenticate client applications (not individual users) using API keys. This pattern suits our domain:
- API keys identify client applications, not users. A warehouse management system or e-commerce platform gets its own key.
- No login flow required. The client includes the key in every request header.
- Simple to implement and test. Pass the header in Scalar or curl, and you are authenticated.
We implement this using ASP.NET Core's authentication pipeline so that controllers can use the standard [Authorize] and [AllowAnonymous] attributes.
The Authentication Handler
Create an ApiKeyAuthenticationHandler that reads the X-Api-Key header and validates it against a configured value:
csharp1using System.Security.Claims;2using System.Text.Encodings.Web;3using Microsoft.AspNetCore.Authentication;4using Microsoft.Extensions.Options;56namespace ParcelTracking.Api.Authentication;78public class ApiKeyAuthenticationHandler9 : AuthenticationHandler<AuthenticationSchemeOptions>10{11 private const string ApiKeyHeaderName = "X-Api-Key";1213 public ApiKeyAuthenticationHandler(14 IOptionsMonitor<AuthenticationSchemeOptions> options,15 ILoggerFactory logger,16 UrlEncoder encoder)17 : base(options, logger, encoder)18 {19 }2021 protected override Task<AuthenticateResult> HandleAuthenticateAsync()22 {23 if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyValues))24 {25 return Task.FromResult(AuthenticateResult.NoResult());26 }2728 var providedKey = apiKeyValues.ToString();29 var expectedKey = Context.RequestServices30 .GetRequiredService<IConfiguration>()31 .GetValue<string>("Authentication:ApiKey");3233 if (string.IsNullOrEmpty(expectedKey)34 || !string.Equals(providedKey, expectedKey, StringComparison.Ordinal))35 {36 return Task.FromResult(37 AuthenticateResult.Fail("Invalid API key."));38 }3940 var claims = new[] { new Claim(ClaimTypes.Name, "ApiClient") };41 var identity = new ClaimsIdentity(claims, Scheme.Name);42 var principal = new ClaimsPrincipal(identity);43 var ticket = new AuthenticationTicket(principal, Scheme.Name);4445 return Task.FromResult(AuthenticateResult.Success(ticket));46 }47}
The handler has three possible outcomes:
| Return Value | Meaning | Effect |
|---|---|---|
NoResult() | No X-Api-Key header present | Defers to authorization policy — [Authorize] will return 401, [AllowAnonymous] will allow access |
Fail(message) | Header present but invalid | Always returns 401, even on [AllowAnonymous] endpoints |
Success(ticket) | Valid API key | Request is authenticated with a ClaimsPrincipal |
The distinction between NoResult() and Fail() is critical. NoResult() means "I cannot authenticate this request, but another handler might." Since we only have one scheme, the [Authorize] attribute will reject the request. But [AllowAnonymous] endpoints still work because no handler explicitly failed.
Registering Authentication in Program.cs
Register the authentication scheme and authorization services:
csharp1builder.Services.AddAuthentication("ApiKey")2 .AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(3 "ApiKey", options => { });45builder.Services.AddAuthorization();
The string "ApiKey" is the default scheme name. AddScheme registers our handler for that scheme. AddAuthorization() enables the [Authorize] and [AllowAnonymous] attributes.
Middleware Order
The order of middleware in Program.cs matters. Authentication and authorization must come before the controller endpoints:
csharp1var app = builder.Build();23if (app.Environment.IsDevelopment())4{5 app.MapOpenApi();6 app.MapScalarApiReference();7}89app.UseAuthentication();10app.UseAuthorization();1112app.MapControllers();1314app.Run();
UseAuthentication() runs the handler to identify the caller. UseAuthorization() checks whether the identified caller is allowed to access the endpoint. MapControllers() routes the request to the matching controller action. If you swap the order, authentication or authorization checks will not run.
Configuring the API Key
Store the API key in appsettings.Development.json:
json1{2 "ConnectionStrings": {3 "DefaultConnection": "Host=localhost;Port=5432;Database=parceltracking;Username=parcel;Password=parcel123"4 },5 "Authentication": {6 "ApiKey": "dev-test-key-12345"7 }8}
In development, a simple static key is fine. In production, you would use environment variables or a secrets manager. The handler reads Authentication:ApiKey from IConfiguration, which merges values from all configuration sources.
Using [Authorize] and [AllowAnonymous]
With authentication registered, controllers use standard attributes to control access:
csharp1[ApiController]2[Route("api/[controller]")]3[Authorize]4public class ParcelsController : ControllerBase5{6 // All actions require a valid API key by default78 [HttpPost]9 public IActionResult RegisterParcel(/* ... */)10 {11 // Requires API key (inherits [Authorize] from controller)12 return Ok();13 }1415 [HttpGet("{trackingNumber}/tracking")]16 [AllowAnonymous]17 public IActionResult GetTracking(string trackingNumber)18 {19 // Public endpoint — no API key needed20 // Customers can track their parcel without credentials21 return Ok();22 }23}
Placing [Authorize] on the controller means every action requires authentication by default. Individual actions can opt out with [AllowAnonymous]. This matches the carrier API pattern: most operations require credentials, but tracking lookup is public.
As you build endpoints in Topics 2 through 11, apply [Authorize] at the controller level and [AllowAnonymous] on specific public actions.
Adding API Key to the OpenAPI Document
For the Scalar explorer to send the API key with test requests, the OpenAPI document needs a security scheme. Add a second document transformer:
csharp1builder.Services.AddOpenApi(options =>2{3 options.AddDocumentTransformer((document, context, ct) =>4 {5 document.Info = new OpenApiInfo6 {7 Title = "Parcel Tracking API",8 Version = "v1",9 Description = "A carrier-style parcel tracking REST API"10 };11 return Task.CompletedTask;12 });1314 options.AddDocumentTransformer((document, context, ct) =>15 {16 document.Components ??= new OpenApiComponents();17 document.Components.SecuritySchemes["ApiKey"] =18 new OpenApiSecurityScheme19 {20 Type = SecuritySchemeType.ApiKey,21 Name = "X-Api-Key",22 In = ParameterLocation.Header,23 Description = "API key passed in the X-Api-Key header"24 };2526 document.SecurityRequirements.Add(27 new OpenApiSecurityRequirement28 {29 {30 new OpenApiSecurityScheme31 {32 Reference = new OpenApiReference33 {34 Type = ReferenceType.SecurityScheme,35 Id = "ApiKey"36 }37 },38 Array.Empty<string>()39 }40 });4142 return Task.CompletedTask;43 });44});
This tells Scalar to show an Authorize input where you enter your API key. Once set, Scalar includes the X-Api-Key header in every test request automatically.
Complete Program.cs
Here is the consolidated Program.cs combining EF Core, OpenAPI, authentication, and controllers:
csharp1using Microsoft.AspNetCore.Authentication;2using Microsoft.AspNetCore.OpenApi;3using Microsoft.EntityFrameworkCore;4using Microsoft.OpenApi.Models;5using ParcelTracking.Api.Authentication;6using ParcelTracking.Infrastructure.Data;7using Scalar.AspNetCore;89var builder = WebApplication.CreateBuilder(args);1011// EF Core12builder.Services.AddDbContext<ParcelTrackingDbContext>(options =>13 options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));1415// Authentication16builder.Services.AddAuthentication("ApiKey")17 .AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(18 "ApiKey", options => { });1920builder.Services.AddAuthorization();2122// OpenAPI23builder.Services.AddOpenApi(options =>24{25 options.AddDocumentTransformer((document, context, ct) =>26 {27 document.Info = new OpenApiInfo28 {29 Title = "Parcel Tracking API",30 Version = "v1",31 Description = "A carrier-style parcel tracking REST API"32 };33 return Task.CompletedTask;34 });3536 options.AddDocumentTransformer((document, context, ct) =>37 {38 document.Components ??= new OpenApiComponents();39 document.Components.SecuritySchemes["ApiKey"] =40 new OpenApiSecurityScheme41 {42 Type = SecuritySchemeType.ApiKey,43 Name = "X-Api-Key",44 In = ParameterLocation.Header,45 Description = "API key passed in the X-Api-Key header"46 };4748 document.SecurityRequirements.Add(49 new OpenApiSecurityRequirement50 {51 {52 new OpenApiSecurityScheme53 {54 Reference = new OpenApiReference55 {56 Type = ReferenceType.SecurityScheme,57 Id = "ApiKey"58 }59 },60 Array.Empty<string>()61 }62 });6364 return Task.CompletedTask;65 });66});6768// Controllers69builder.Services.AddControllers();7071var app = builder.Build();7273if (app.Environment.IsDevelopment())74{75 app.MapOpenApi();76 app.MapScalarApiReference();77}7879app.UseAuthentication();80app.UseAuthorization();8182app.MapControllers();8384app.Run();
This is the foundation you build on for the rest of the course. Each topic adds controllers and services, but this Program.cs structure remains stable.
Summary
In this presentation, you learned how to:
- Replace Swashbuckle with ASP.NET Core's built-in
AddOpenApi()andMapOpenApi() - Serve interactive API documentation with Scalar at
/scalar/v1 - Implement API Key authentication using a custom
AuthenticationHandler - Understand the three authentication results:
NoResult,Fail, andSuccess - Register authentication and authorization in the correct middleware order
- Use
[Authorize]at the controller level and[AllowAnonymous]for public endpoints - Add an API Key security scheme to the OpenAPI document so Scalar can send authenticated requests
Next, we will test your understanding of the complete Topic 1 material with a quiz covering project setup, domain design, EF Core configuration, and the OpenAPI and authentication setup you just learned.