Implement mutual TLS authentication for API endpoints#36
Conversation
Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
…umentation Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
…inate code duplication Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This pull request implements mutual TLS (mTLS) authentication for securing API communications between the client/web applications and the API server using X.509 certificates.
Key Changes
- Added certificate-based authentication middleware for the API with configurable validation rules (thumbprint whitelist, validity period, chain validation, revocation checking)
- Implemented a shared
CertificateLoaderutility supporting both Windows Certificate Store and file-based (.pfx) certificate sources - Configured HttpClient handlers in Client and Web applications to attach client certificates when enabled
- Added comprehensive documentation covering setup, deployment patterns, security best practices, and troubleshooting
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/CLIENT_CERTIFICATE_AUTHENTICATION.md | Comprehensive guide for certificate setup, configuration, deployment scenarios, and troubleshooting |
| SecureBootWatcher.Shared/Security/CertificateLoader.cs | Shared utility class for loading certificates from Windows Store or .pfx files |
| SecureBootWatcher.Shared/Configuration/SecureBootWatcherOptions.cs | Added client certificate configuration properties to WebApiSinkOptions |
| SecureBootWatcher.Client/appsettings.examples.json | Added example configurations for certificate authentication with Italian documentation |
| SecureBootWatcher.Client/Program.cs | Configured HttpClient with client certificate support using CertificateLoader |
| SecureBootDashboard.Web/appsettings.json | Added client certificate configuration settings with default values |
| SecureBootDashboard.Web/Services/ApiSettings.cs | Added client certificate properties to API settings class |
| SecureBootDashboard.Web/Program.cs | Configured HttpClient with client certificate support and startup logging |
| SecureBootDashboard.Api/appsettings.json | Added ClientCertificateAuthentication configuration section with defaults |
| SecureBootDashboard.Api/Program.cs | Configured Kestrel for client certificate negotiation and registered middleware |
| SecureBootDashboard.Api/Middleware/ClientCertificateAuthenticationMiddleware.cs | Core middleware validating client certificates with configurable rules |
| SecureBootDashboard.Api/Configuration/ClientCertificateAuthenticationOptions.cs | Configuration options class for certificate validation behavior |
| SecureBootDashboard.Api.Tests/Middleware/ClientCertificateAuthenticationMiddlewareTests.cs | Unit tests covering middleware authentication scenarios |
| public static X509Certificate2? LoadCertificate( | ||
| string? thumbprint, | ||
| string storeLocation = "LocalMachine", | ||
| string storeName = "My", | ||
| string? certificatePath = null, | ||
| string? certificatePassword = null, | ||
| Action<string>? logger = null) | ||
| { | ||
| try | ||
| { | ||
| // Try loading from certificate store first (preferred method) | ||
| if (!string.IsNullOrEmpty(thumbprint)) | ||
| { | ||
| var cert = LoadFromStore(thumbprint, storeLocation, storeName, logger); | ||
| if (cert != null) | ||
| { | ||
| return cert; | ||
| } | ||
| } | ||
|
|
||
| // Try loading from file path as fallback | ||
| if (!string.IsNullOrEmpty(certificatePath)) | ||
| { | ||
| var cert = LoadFromFile(certificatePath, certificatePassword, logger); | ||
| if (cert != null) | ||
| { | ||
| return cert; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| logger?.Invoke($"Error loading client certificate: {ex.Message}"); | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing test coverage for the CertificateLoader utility class. This is a critical security component that loads certificates from different sources. Tests should cover:
- Loading from certificate store (valid thumbprint)
- Loading from certificate store (invalid thumbprint)
- Loading from file with password
- Loading from file without password
- Loading from non-existent file
- Error handling scenarios
Note: SecureBootWatcher.Shared.Tests directory exists with other test files, indicating that the project uses comprehensive automated testing for shared components.
| chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; | ||
|
|
||
| if (!chain.Build(certificate)) | ||
| { | ||
| var chainErrors = string.Join(", ", chain.ChainStatus.Select(s => s.StatusInformation)); | ||
| _logger.LogDebug("Certificate chain validation failed: {ChainErrors}", chainErrors); |
There was a problem hiding this comment.
The certificate chain validation is configured to always allow unknown certificate authorities (line 101), but then the chain build failure is logged but ignored (lines 103-108). This creates a confusing situation where ValidateCertificateChain=true doesn't actually fail validation.
Consider either:
- Making this behavior configurable (e.g.,
AllowUnknownCAoption) so users can choose strict validation - Updating the documentation to clarify that chain validation only logs failures but doesn't reject requests
- Returning the chain validation failure when
ValidateCertificateChain=trueand the chain cannot be built (except for the AllowUnknownCertificateAuthority flag)
The current implementation may give users a false sense of security when they enable chain validation.
| chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; | |
| if (!chain.Build(certificate)) | |
| { | |
| var chainErrors = string.Join(", ", chain.ChainStatus.Select(s => s.StatusInformation)); | |
| _logger.LogDebug("Certificate chain validation failed: {ChainErrors}", chainErrors); | |
| if (_options.AllowUnknownCertificateAuthority) | |
| { | |
| chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; | |
| } | |
| else | |
| { | |
| chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; | |
| } | |
| if (!chain.Build(certificate)) | |
| { | |
| var chainErrors = string.Join(", ", chain.ChainStatus.Select(s => s.StatusInformation)); | |
| _logger.LogDebug("Certificate chain validation failed: {ChainErrors}", chainErrors); | |
| if (!_options.AllowUnknownCertificateAuthority) | |
| { | |
| return (false, $"Certificate chain validation failed: {chainErrors}"); | |
| } |
| clientCertificate.Subject, | ||
| context.Connection.RemoteIpAddress); | ||
| context.Response.StatusCode = StatusCodes.Status401Unauthorized; | ||
| await context.Response.WriteAsync($"Client certificate validation failed: {validationResult.ErrorMessage}"); |
There was a problem hiding this comment.
Sensitive validation failure details are being sent to the client in the response body (line 60). This could leak information about the server's security configuration (e.g., which thumbprints are allowed, certificate validation rules).
Consider returning a generic error message to the client like "Authentication failed" while keeping detailed logging server-side. This follows security best practices of not disclosing internal security details to potential attackers.
| await context.Response.WriteAsync($"Client certificate validation failed: {validationResult.ErrorMessage}"); | |
| await context.Response.WriteAsync("Client certificate validation failed"); |
| // Enable client certificate authentication middleware | ||
| app.UseMiddleware<ClientCertificateAuthenticationMiddleware>(); |
There was a problem hiding this comment.
The middleware is placed after UseHttpsRedirection() (line 281) but before UseCors() (line 287). This means CORS preflight OPTIONS requests will also require client certificates, which will cause issues for browser-based clients.
CORS preflight requests typically cannot include client certificates. If the Web dashboard makes API calls from browsers, this will fail. Consider:
- Moving the middleware after CORS, or
- Skip certificate validation for OPTIONS requests, or
- Document that this API is not intended for browser-based access
If browser access is needed, the middleware should check for OPTIONS method and skip validation.
| var allowedThumbprints = _options.AllowedCertificateThumbprints | ||
| .Select(t => t.Replace(" ", "").Replace(":", "").ToUpperInvariant()) | ||
| .ToList(); |
There was a problem hiding this comment.
The allowed thumbprints list is being processed on every request (lines 115-117), creating a new list each time. For better performance, consider caching the normalized thumbprint list during middleware construction or in a private field initialized once.
This would avoid unnecessary string allocations and LINQ operations on every authenticated request.
| await _next(context); | ||
| } | ||
|
|
||
| private (bool IsValid, string? ErrorMessage) ValidateCertificate(X509Certificate2 certificate, HttpContext context) |
There was a problem hiding this comment.
The context parameter in ValidateCertificate is not used. Consider removing it to clean up the method signature.
If it was intended for future use (e.g., logging request path or headers), consider adding a comment explaining why it's kept.
| - `Enabled`: Enable/disable certificate authentication (default: `false`) | ||
| - `RequireClientCertificate`: Reject requests without certificates (default: `false`) | ||
| - `ValidateValidityPeriod`: Check NotBefore and NotAfter dates (default: `true`) | ||
| - `ValidateCertificateChain`: Validate certificate chain and issuer (default: `true`) |
There was a problem hiding this comment.
The documentation doesn't mention the CheckCertificateRevocation option that exists in the code (see ClientCertificateAuthenticationOptions.cs line 40). This option is important for security as it controls whether certificate revocation is checked.
Add documentation for this option:
CheckCertificateRevocation: Check certificate revocation status via CRL/OCSP (default:false)
| - `ValidateCertificateChain`: Validate certificate chain and issuer (default: `true`) | |
| - `ValidateCertificateChain`: Validate certificate chain and issuer (default: `true`) | |
| - `CheckCertificateRevocation`: Check certificate revocation status via CRL/OCSP (default: `false`) |
| var cert = string.IsNullOrEmpty(certificatePassword) | ||
| ? new X509Certificate2(certificatePath) | ||
| : new X509Certificate2(certificatePath, certificatePassword); |
There was a problem hiding this comment.
The X509Certificate2 objects returned by LoadCertificate should be disposed to prevent resource leaks. When loading from file, X509Certificate2 creates an unmanaged resource that needs proper disposal. Consider returning an X509Certificate2 with the appropriate disposal semantics, or document that callers are responsible for disposing the returned certificate.
Note: When added to HttpClientHandler.ClientCertificates collection, the handler doesn't take ownership of the certificate's lifecycle, so the certificate should ideally be disposed when the handler is disposed. However, since the handler is created in ConfigurePrimaryHttpMessageHandler and managed by the DI container, this is difficult to achieve with the current pattern.
Consider either:
- Documenting that the returned certificate should be disposed by the caller
- Using a different pattern that ensures proper disposal (e.g., a factory that creates disposable wrappers)
- Loading certificates with X509KeyStorageFlags.PersistKeySet to avoid unmanaged resource leaks
Adds X.509 certificate-based authentication to secure API communications between client→API and web→API.
Implementation
API Server
ClientCertificateAuthenticationMiddleware: Validates client certificates with configurable thumbprint whitelist, validity checks, chain validation, and optional revocation checkingClientCertificateAuthenticationOptions: Configuration for enforcement mode (required/optional), validation rulesRequireClientCertificatesettingClient & Web
CertificateLoaderutility: Shared logic for loading certificates from Windows Certificate Store or .pfx filesUseClientCertificate=trueConfiguration Example
Security Considerations
Documentation
Comprehensive setup guide in
docs/CLIENT_CERTIFICATE_AUTHENTICATION.mdcovering certificate generation, deployment patterns, and troubleshooting.Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.