Client Certificate
Authenticate requests using client TLS certificates (mTLS).
Full source
File: pwsh/tutorial/examples/8.8-Client-Certificate-Authentication.ps1
<#
Sample: Client Certificate Authentication
Purpose: Demonstrates client certificate (mTLS) authentication with X.509 certificate validation.
File: 8.8-Client-Certificate-Authentication.ps1
Notes: Uses self-signed certificates for testing. For production, use CA-signed certificates.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
param(
[int]$Port = 5000,
[IPAddress]$IPAddress = [IPAddress]::Loopback
)
# 1. (Optional) Logging pipeline
New-KrLogger |
Set-KrLoggerLevel -Value Debug |
Add-KrSinkConsole |
Register-KrLogger -Name 'console' -SetAsDefault | Out-Null
# 2. Create server host
New-KrServer -Name 'Client Certificate Auth Demo'
# Set certs directory
$certPath = Join-Path -Path $PSScriptRoot -ChildPath 'certs'
$basename = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
# 3. Create or load server certificate for HTTPS
$serverCertPath = Join-Path -Path $certPath -ChildPath "$basename-server-cert.pfx"
if (Test-Path $serverCertPath) {
$serverCert = Import-KrCertificate -FilePath $serverCertPath -Password (ConvertTo-SecureString -String 'test' -AsPlainText -Force)
} else {
if (-not (Test-Path -Path $certPath )) {
New-Item -ItemType Directory -Path $certPath | Out-Null
}
# Create server certificate for localhost
$serverCert = New-KrSelfSignedCertificate -DnsNames 'localhost' -Exportable
# Export for reuse
Export-KrCertificate -Certificate $serverCert -FilePath $serverCertPath `
-Format pfx -IncludePrivateKey -Password (ConvertTo-SecureString -String 'test' -AsPlainText -Force)
Write-Host "Created server certificate: $serverCertPath"
}
# 4. Create or load client certificate for testing
$clientCertPath = Join-Path -Path $certPath -ChildPath "$basename-client-cert.pfx"
if (-not (Test-Path $clientCertPath)) {
# Create a self-signed client certificate for testing
# Note: In production, client certificates should be issued by a trusted CA
$clientCert = New-KrSelfSignedCertificate -DnsNames 'test-client' -Exportable
# Export for testing
Export-KrCertificate -Certificate $clientCert -FilePath $clientCertPath `
-Format pfx -IncludePrivateKey -Password (ConvertTo-SecureString -String 'test' -AsPlainText -Force)
Write-Host 'Created test certificates:'
Write-Host " Server: $serverCertPath"
Write-Host " Client: $clientCertPath"
Write-Host ' Password: test'
Write-Host ''
Write-Host 'Note: Using self-signed certificates for testing.'
Write-Host ' In production, use certificates from a trusted CA.'
Write-Host ''
}
# Import the client certificate so we can trust it for authentication.
# (The client will present this cert during TLS; the authentication layer still validates an X509Chain.)
$clientCertForTrust = Import-KrCertificate -FilePath $clientCertPath -Password (ConvertTo-SecureString -String 'test' -AsPlainText -Force)
# 5. Configure HTTPS to require client certificates
# NOTE: This callback is invoked on Kestrel threadpool threads during TLS handshake.
# It must be pure .NET (not a PowerShell scriptblock), otherwise you'll get
# "There is no Runspace available" errors.
#
# This example demonstrates compiling a C# snippet with Roslyn into the TLS callback delegate.
$clientCertValidationCode = @'
// certificate: X509Certificate2
// chain: X509Chain
// sslPolicyErrors: SslPolicyErrors
// AllowCertificate mode: permit connections without a client certificate.
if (certificate is null)
{
return true;
}
// Accept valid chains.
if (sslPolicyErrors == SslPolicyErrors.None)
{
return true;
}
// Dev-friendly: allow self-signed / untrusted chains.
return sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors;
'@
Set-KrServerHttpsOptions -ClientCertificateMode AllowCertificate `
-ClientCertificateValidationLanguage CSharp `
-ClientCertificateValidationCode $clientCertValidationCode
# Note: You can also use a built-in .NET callback instead of compiling code:
# Set-KrServerHttpsOptions -ClientCertificateMode AllowCertificate -ClientCertificateValidation ([Kestrun.Certificates.ClientCertificateValidationCallbacks]::AllowMissingOrSelfSignedForDevelopment)
#
# Tip: If you prefer storing the snippet in a file, use -ClientCertificateValidationCodePath with a .cs/.csx or .vb file.
# 6. Add HTTPS endpoint
Add-KrEndpoint -Port $Port -IPAddress $IPAddress -X509Certificate $serverCert
# 7. Configure client certificate authentication
# Note: For self-signed client certificates, we need to allow all certificate types
# and disable some validation that requires a proper certificate chain
$authOptions = [Kestrun.Authentication.ClientCertificateAuthenticationOptions]::new()
$authOptions.AllowedCertificateTypes = [Microsoft.AspNetCore.Authentication.Certificate.CertificateTypes]::All
$authOptions.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
# Trust the self-signed client certificate as a root for chain building.
$authOptions.CustomTrustStore = [System.Security.Cryptography.X509Certificates.X509Certificate2Collection]::new()
$null = $authOptions.CustomTrustStore.Add($clientCertForTrust)
Add-KrClientCertificateAuthentication -AuthenticationScheme 'Certificate' -Options $authOptions
# 8. Finalize configuration (build internal pipeline)
Enable-KrConfiguration
# 9. Map secured route group using certificate authentication
Add-KrMapRoute -Verbs Get -Pattern '/secure/cert/hello' -AuthorizationScheme 'Certificate' -ScriptBlock {
$cert = $Context.Connection.ClientCertificate
if ($null -eq $cert) {
Write-KrJsonResponse @{
error = 'No client certificate provided'
} -StatusCode 401
return
}
Write-KrJsonResponse @{
message = 'Hello from client certificate authentication'
subject = $cert.Subject
issuer = $cert.Issuer
thumbprint = $cert.Thumbprint
validFrom = $cert.NotBefore.ToString('o')
validTo = $cert.NotAfter.ToString('o')
}
}
Add-KrMapRoute -Verbs Get -Pattern '/secure/cert/info' -AuthorizationScheme 'Certificate' -ScriptBlock {
$cert = $Context.Connection.ClientCertificate
if ($null -eq $cert) {
Write-KrJsonResponse @{
error = 'No client certificate provided'
} -StatusCode 401
return
}
$claims = $Context.User.Claims | ForEach-Object {
@{
type = $_.Type
value = $_.Value
}
}
Write-KrJsonResponse @{
authenticationType = $Context.User.Identity.AuthenticationType
isAuthenticated = $Context.User.Identity.IsAuthenticated
name = $Context.User.Identity.Name
claims = $claims
certificate = @{
subject = $cert.Subject
issuer = $cert.Issuer
thumbprint = $cert.Thumbprint
serialNumber = $cert.SerialNumber
}
}
}
# 10. Add a public info endpoint
Add-KrMapRoute -Verbs Get -Pattern '/info' -ScriptBlock {
Write-KrJsonResponse @{
message = 'Client Certificate Authentication Demo'
endpoints = @(
@{
path = '/secure/cert/hello'
method = 'GET'
description = 'Returns certificate information'
requiresAuth = $true
}
@{
path = '/secure/cert/info'
method = 'GET'
description = 'Returns detailed authentication and claims information'
requiresAuth = $true
}
)
testInstructions = @{
clientCertPath = 'client-cert.pfx (in same directory as script)'
password = 'test'
example = "Invoke-RestMethod -Uri 'https://localhost:$Port/secure/cert/hello' -Certificate `$cert -SkipCertificateCheck"
}
}
}
# 11. Start server (Ctrl+C to stop)
Write-Host "`nServer starting on https://localhost:$Port"
Write-Host 'Test the secured endpoint with:'
Write-Host " `$cert = Import-KrCertificate -FilePath '$clientCertPath' -Password (ConvertTo-SecureString 'test' -AsPlainText -Force)"
Write-Host " Invoke-RestMethod -Uri 'https://localhost:$Port/secure/cert/hello' -Certificate `$cert -SkipCertificateCheck"
Write-Host "`nPress Ctrl+C to stop"
Start-KrServer -CloseLogsOnExit
Step-by-step
- Logging: Register a console logger at Debug.
- Server: Create a server instance with New-KrServer.
- Server cert: Create/load an HTTPS server certificate (self-signed for local testing).
- Client cert: Create/load a client certificate (self-signed for local testing).
-
TLS validation (Kestrel): Configure HTTPS to accept dev certificates using Set-KrServerHttpsOptions.
- Use
-ClientCertificateMode AllowCertificateso/info(and probes) can work without a client certificate. - Provide a TLS callback using either:
-ClientCertificateValidation(built-in .NET delegate), or-ClientCertificateValidationCode/-ClientCertificateValidationCodePath(compile C#/VB.NET with Roslyn).
- Use
- Listener: Bind HTTPS using Add-KrEndpoint.
- Authentication: Register certificate auth with Add-KrClientCertificateAuthentication.
- Finalize: Apply all staged configuration using Enable-KrConfiguration.
- Routes: Map
/secure/cert/*routes that require theCertificatescheme via Add-KrMapRoute. - Start: Run the server with Start-KrServer.
TLS vs authentication (important)
Client certificate authentication runs at the ASP.NET Core authentication layer. Kestrel must first accept the client certificate during the TLS handshake, otherwise the request never reaches Kestrun.
Try it
Assumptions:
- Server is listening on
https://127.0.0.1:5000. - Certificates are self-signed for local development.
Run the sample from the repository root:
pwsh .\docs\_includes\examples\pwsh\8.8-Client-Certificate-Authentication.ps1
Public route:
curl -k https://127.0.0.1:5000/info
Without a client certificate, the protected route should fail:
try {
Invoke-RestMethod https://127.0.0.1:5000/secure/cert/hello -SkipCertificateCheck -ErrorAction Stop
} catch {
$_.Exception.Message
}
With the generated client certificate:
$clientCertPath = '.\docs\_includes\examples\pwsh\client-cert.pfx'
$password = ConvertTo-SecureString -String 'test' -AsPlainText -Force
$cert = Import-KrCertificate -FilePath $clientCertPath -Password $password
Invoke-RestMethod https://127.0.0.1:5000/secure/cert/hello -Certificate $cert -SkipCertificateCheck
Invoke-RestMethod https://127.0.0.1:5000/secure/cert/info -Certificate $cert -SkipCertificateCheck
Troubleshooting
| Symptom | Cause / Details | Fix / Guidance |
|---|---|---|
| Client says: “The decryption operation failed” | TLS handshake rejected the client certificate (common with self-signed certs) | Configure Kestrel TLS callback via Set-KrServerHttpsOptions |
| Server logs mention RemoteCertificate… | Certificate rejected by TLS validation callback | Use AllowCertificate + dev-friendly validation (-ClientCertificateValidation*) |
| “There is no Runspace available” | TLS callback is a PowerShell script block running on Kestrel TLS threads | Use a pure .NET delegate or compile code via -ClientCertificateValidationCode* |
/info fails unless client cert provided | TLS mode is RequireCertificate | Prefer -ClientCertificateMode AllowCertificate and enforce auth per-route |
| Protected routes return 401 with a cert | Cert auth not registered or scheme mismatch | Ensure Add-KrClientCertificateAuthentication and route uses -AuthorizationScheme 'Certificate' |
References
- New-KrServer
- New-KrSelfSignedCertificate
- Export-KrCertificate
- Import-KrCertificate
- Set-KrServerHttpsOptions
- Add-KrEndpoint
- Add-KrClientCertificateAuthentication
- Enable-KrConfiguration
- Add-KrMapRoute
- Write-KrJsonResponse
- Start-KrServer
Previous / Next
Previous: Claims & Policies Next: OpenID Connect (Okta)