20 minlesson

OpenAPI Documentation & API Key Authentication

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:

bash
1dotnet add src/ParcelTracking.Api package Microsoft.AspNetCore.OpenApi
2dotnet 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:

csharp
1builder.Services.AddOpenApi(options =>
2{
3 options.AddDocumentTransformer((document, context, ct) =>
4 {
5 document.Info = new OpenApiInfo
6 {
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:

csharp
1var app = builder.Build();
2
3if (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:

csharp
1using System.Security.Claims;
2using System.Text.Encodings.Web;
3using Microsoft.AspNetCore.Authentication;
4using Microsoft.Extensions.Options;
5
6namespace ParcelTracking.Api.Authentication;
7
8public class ApiKeyAuthenticationHandler
9 : AuthenticationHandler<AuthenticationSchemeOptions>
10{
11 private const string ApiKeyHeaderName = "X-Api-Key";
12
13 public ApiKeyAuthenticationHandler(
14 IOptionsMonitor<AuthenticationSchemeOptions> options,
15 ILoggerFactory logger,
16 UrlEncoder encoder)
17 : base(options, logger, encoder)
18 {
19 }
20
21 protected override Task<AuthenticateResult> HandleAuthenticateAsync()
22 {
23 if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyValues))
24 {
25 return Task.FromResult(AuthenticateResult.NoResult());
26 }
27
28 var providedKey = apiKeyValues.ToString();
29 var expectedKey = Context.RequestServices
30 .GetRequiredService<IConfiguration>()
31 .GetValue<string>("Authentication:ApiKey");
32
33 if (string.IsNullOrEmpty(expectedKey)
34 || !string.Equals(providedKey, expectedKey, StringComparison.Ordinal))
35 {
36 return Task.FromResult(
37 AuthenticateResult.Fail("Invalid API key."));
38 }
39
40 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);
44
45 return Task.FromResult(AuthenticateResult.Success(ticket));
46 }
47}

The handler has three possible outcomes:

Return ValueMeaningEffect
NoResult()No X-Api-Key header presentDefers to authorization policy — [Authorize] will return 401, [AllowAnonymous] will allow access
Fail(message)Header present but invalidAlways returns 401, even on [AllowAnonymous] endpoints
Success(ticket)Valid API keyRequest 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:

csharp
1builder.Services.AddAuthentication("ApiKey")
2 .AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(
3 "ApiKey", options => { });
4
5builder.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:

csharp
1var app = builder.Build();
2
3if (app.Environment.IsDevelopment())
4{
5 app.MapOpenApi();
6 app.MapScalarApiReference();
7}
8
9app.UseAuthentication();
10app.UseAuthorization();
11
12app.MapControllers();
13
14app.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:

json
1{
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:

csharp
1[ApiController]
2[Route("api/[controller]")]
3[Authorize]
4public class ParcelsController : ControllerBase
5{
6 // All actions require a valid API key by default
7
8 [HttpPost]
9 public IActionResult RegisterParcel(/* ... */)
10 {
11 // Requires API key (inherits [Authorize] from controller)
12 return Ok();
13 }
14
15 [HttpGet("{trackingNumber}/tracking")]
16 [AllowAnonymous]
17 public IActionResult GetTracking(string trackingNumber)
18 {
19 // Public endpoint — no API key needed
20 // Customers can track their parcel without credentials
21 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:

csharp
1builder.Services.AddOpenApi(options =>
2{
3 options.AddDocumentTransformer((document, context, ct) =>
4 {
5 document.Info = new OpenApiInfo
6 {
7 Title = "Parcel Tracking API",
8 Version = "v1",
9 Description = "A carrier-style parcel tracking REST API"
10 };
11 return Task.CompletedTask;
12 });
13
14 options.AddDocumentTransformer((document, context, ct) =>
15 {
16 document.Components ??= new OpenApiComponents();
17 document.Components.SecuritySchemes["ApiKey"] =
18 new OpenApiSecurityScheme
19 {
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 };
25
26 document.SecurityRequirements.Add(
27 new OpenApiSecurityRequirement
28 {
29 {
30 new OpenApiSecurityScheme
31 {
32 Reference = new OpenApiReference
33 {
34 Type = ReferenceType.SecurityScheme,
35 Id = "ApiKey"
36 }
37 },
38 Array.Empty<string>()
39 }
40 });
41
42 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:

csharp
1using 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;
8
9var builder = WebApplication.CreateBuilder(args);
10
11// EF Core
12builder.Services.AddDbContext<ParcelTrackingDbContext>(options =>
13 options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
14
15// Authentication
16builder.Services.AddAuthentication("ApiKey")
17 .AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(
18 "ApiKey", options => { });
19
20builder.Services.AddAuthorization();
21
22// OpenAPI
23builder.Services.AddOpenApi(options =>
24{
25 options.AddDocumentTransformer((document, context, ct) =>
26 {
27 document.Info = new OpenApiInfo
28 {
29 Title = "Parcel Tracking API",
30 Version = "v1",
31 Description = "A carrier-style parcel tracking REST API"
32 };
33 return Task.CompletedTask;
34 });
35
36 options.AddDocumentTransformer((document, context, ct) =>
37 {
38 document.Components ??= new OpenApiComponents();
39 document.Components.SecuritySchemes["ApiKey"] =
40 new OpenApiSecurityScheme
41 {
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 };
47
48 document.SecurityRequirements.Add(
49 new OpenApiSecurityRequirement
50 {
51 {
52 new OpenApiSecurityScheme
53 {
54 Reference = new OpenApiReference
55 {
56 Type = ReferenceType.SecurityScheme,
57 Id = "ApiKey"
58 }
59 },
60 Array.Empty<string>()
61 }
62 });
63
64 return Task.CompletedTask;
65 });
66});
67
68// Controllers
69builder.Services.AddControllers();
70
71var app = builder.Build();
72
73if (app.Environment.IsDevelopment())
74{
75 app.MapOpenApi();
76 app.MapScalarApiReference();
77}
78
79app.UseAuthentication();
80app.UseAuthorization();
81
82app.MapControllers();
83
84app.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() and MapOpenApi()
  • Serve interactive API documentation with Scalar at /scalar/v1
  • Implement API Key authentication using a custom AuthenticationHandler
  • Understand the three authentication results: NoResult, Fail, and Success
  • 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.