Skip to content

Implement mutual TLS authentication for API endpoints - #36

Open
robgrame with Copilot wants to merge 4 commits into
mainfrom
copilot/implement-api-client-authentication
Open

Implement mutual TLS authentication for API endpoints#36
robgrame with Copilot wants to merge 4 commits into
mainfrom
copilot/implement-api-client-authentication

Conversation

Copilot AI commented Dec 3, 2025

Copy link
Copy Markdown
Contributor

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 checking
  • ClientCertificateAuthenticationOptions: Configuration for enforcement mode (required/optional), validation rules
  • Kestrel configured to negotiate client certificates based on RequireClientCertificate setting

Client & Web

  • CertificateLoader utility: Shared logic for loading certificates from Windows Certificate Store or .pfx files
  • HttpClient handlers configured to attach certificates when UseClientCertificate=true
  • Supports both thumbprint-based (store) and file-based (.pfx) certificate sources

Configuration Example

// API - Enable validation
{
  "ClientCertificateAuthentication": {
    "Enabled": true,
    "RequireClientCertificate": true,
    "AllowedCertificateThumbprints": ["ABC123..."],
    "CheckCertificateRevocation": false
  }
}

// Client/Web - Send certificate
{
  "Sinks": {
    "WebApi": {
      "UseClientCertificate": true,
      "ClientCertificateThumbprint": "ABC123...",
      "ClientCertificateStoreLocation": "LocalMachine",
      "ClientCertificateStoreName": "My"
    }
  }
}

Security Considerations

  • Default: authentication disabled for backward compatibility
  • Revocation checking disabled by default (incompatible with self-signed certs and air-gapped deployments)
  • Chain validation allows unknown CAs (enterprise scenarios with self-signed roots)
  • Empty thumbprint whitelist accepts any valid certificate

Documentation

Comprehensive setup guide in docs/CLIENT_CERTIFICATE_AUTHENTICATION.md covering certificate generation, deployment patterns, and troubleshooting.

Original prompt

This section details on the original issue you should resolve

<issue_title>Implement API client based authentication</issue_title>
<issue_description>Allo scopo di rendere più sicura la soluzione é necessario fare in modo che le chiamate ai servizio API siano autenticate con certificato sia lato client sia lato app nel momento in cui eseguano una richiesta al servizio API</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 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.

Copilot AI and others added 3 commits December 3, 2025 22:55
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>
Copilot AI changed the title [WIP] Add API client based authentication for API requests Implement mutual TLS authentication for API endpoints Dec 3, 2025
Copilot AI requested a review from robgrame December 3, 2025 23:15
@robgrame
robgrame marked this pull request as ready for review December 3, 2025 23:23
@robgrame
robgrame requested a review from Copilot December 3, 2025 23:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 CertificateLoader utility 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

Comment on lines +22 to +59
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;
}
}

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +106
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);

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Making this behavior configurable (e.g., AllowUnknownCA option) so users can choose strict validation
  2. Updating the documentation to clarify that chain validation only logs failures but doesn't reject requests
  3. Returning the chain validation failure when ValidateCertificateChain=true and 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.

Suggested change
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}");
}

Copilot uses AI. Check for mistakes.
clientCertificate.Subject,
context.Connection.RemoteIpAddress);
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync($"Client certificate validation failed: {validationResult.ErrorMessage}");

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
await context.Response.WriteAsync($"Client certificate validation failed: {validationResult.ErrorMessage}");
await context.Response.WriteAsync("Client certificate validation failed");

Copilot uses AI. Check for mistakes.
Comment on lines +283 to +284
// Enable client certificate authentication middleware
app.UseMiddleware<ClientCertificateAuthenticationMiddleware>();

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Moving the middleware after CORS, or
  2. Skip certificate validation for OPTIONS requests, or
  3. 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.

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +117
var allowedThumbprints = _options.AllowedCertificateThumbprints
.Select(t => t.Replace(" ", "").Replace(":", "").ToUpperInvariant())
.ToList();

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
await _next(context);
}

private (bool IsValid, string? ErrorMessage) ValidateCertificate(X509Certificate2 certificate, HttpContext context)

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
- `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`)

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Suggested change
- `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`)

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +110
var cert = string.IsNullOrEmpty(certificatePassword)
? new X509Certificate2(certificatePath)
: new X509Certificate2(certificatePath, certificatePassword);

Copilot AI Dec 3, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Documenting that the returned certificate should be disposed by the caller
  2. Using a different pattern that ensures proper disposal (e.g., a factory that creates disposable wrappers)
  3. Loading certificates with X509KeyStorageFlags.PersistKeySet to avoid unmanaged resource leaks

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement API client based authentication

3 participants