diff --git a/examples/web-auth-clientcert.ps1 b/examples/web-auth-clientcert.ps1 new file mode 100644 index 000000000..d8349beee --- /dev/null +++ b/examples/web-auth-clientcert.ps1 @@ -0,0 +1,48 @@ +$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) +Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop + +# or just: +# Import-Module Pode + +# create a server, flagged to generate a self-signed cert for dev/testing, but allow client certs for auth +Start-PodeServer { + + # bind to ip/port and set as https with self-signed cert + Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -SelfSigned -AllowClientCertificate + + # set view engine for web pages + Set-PodeViewEngine -Type Pode + + # setup client cert auth + New-PodeAuthScheme -ClientCertificate | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { + param($cert, $errors) + + # validate the thumbprint - here you would check a real cert store, or database + if ($cert.Thumbprint -ieq '2561B2BD3CF292FF55F72692FB252E6B3D9879C2') { + return @{ + User = @{ + ID ='M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + } + } + + # an invalid cert + return @{ Message = 'Invalid certificate supplied' } + } + + # GET request for web page at "/" + Add-PodeRoute -Method Get -Path '/' -Authentication 'Validate' -ScriptBlock { + param($e) + #$e.Request.ClientCertificate | out-default + Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } + } + + # GET request throws fake "500" server error status code + Add-PodeRoute -Method Get -Path '/error' -Authentication 'Validate' -ScriptBlock { + param($e) + Set-PodeResponseStatus -Code 500 + } + +} diff --git a/examples/web-pages-https.ps1 b/examples/web-pages-https.ps1 index cdcc0ad1d..6221dc75a 100644 --- a/examples/web-pages-https.ps1 +++ b/examples/web-pages-https.ps1 @@ -21,13 +21,13 @@ Start-PodeServer { # GET request for web page at "/" Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - param($session) + param($e) Write-PodeViewResponse -Path 'simple' -Data @{ 'numbers' = @(1, 2, 3); } } # GET request throws fake "500" server error status code Add-PodeRoute -Method Get -Path '/error' -ScriptBlock { - param($session) + param($e) Set-PodeResponseStatus -Code 500 } diff --git a/src/Listener/PodeContext.cs b/src/Listener/PodeContext.cs index ad329bf67..ec08e8392 100644 --- a/src/Listener/PodeContext.cs +++ b/src/Listener/PodeContext.cs @@ -112,7 +112,7 @@ private void NewRequest() // attempt to open the request stream try { - Request.Open(PodeSocket.Certificate, PodeSocket.Protocols); + Request.Open(PodeSocket.Certificate, PodeSocket.Protocols, PodeSocket.AllowClientCertificate); State = PodeContextState.Open; } catch diff --git a/src/Listener/PodeRequest.cs b/src/Listener/PodeRequest.cs index 3efb38a73..b10e9c6ac 100644 --- a/src/Listener/PodeRequest.cs +++ b/src/Listener/PodeRequest.cs @@ -19,6 +19,8 @@ public class PodeRequest : IDisposable public virtual bool CloseImmediately { get => false; } public Stream InputStream { get; private set; } + public X509Certificate2 ClientCertificate { get; private set; } + public SslPolicyErrors ClientCertificateErrors { get; private set; } public HttpRequestException Error { get; private set; } private Socket Socket; @@ -42,7 +44,7 @@ public PodeRequest(PodeRequest request) Context = request.Context; } - public void Open(X509Certificate certificate, SslProtocols protocols) + public void Open(X509Certificate certificate, SslProtocols protocols, bool allowClientCertificate) { // ssl or not? IsSsl = (certificate != default(X509Certificate)); @@ -57,18 +59,19 @@ public void Open(X509Certificate certificate, SslProtocols protocols) // otherwise, convert the stream to an ssl stream var ssl = new SslStream(InputStream, false, new RemoteCertificateValidationCallback(ValidateCertificateCallback)); - ssl.AuthenticateAsServerAsync(certificate, false, protocols, false).Wait(Context.Listener.CancellationToken); + ssl.AuthenticateAsServerAsync(certificate, allowClientCertificate, protocols, false).Wait(Context.Listener.CancellationToken); InputStream = ssl; } private bool ValidateCertificateCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { - if (certificate == default(X509Certificate)) - { - return true; - } + ClientCertificateErrors = sslPolicyErrors; + + ClientCertificate = certificate == default(X509Certificate) + ? default(X509Certificate2) + : new X509Certificate2(certificate); - return (sslPolicyErrors != SslPolicyErrors.None); + return true; } public void Receive() diff --git a/src/Listener/PodeSocket.cs b/src/Listener/PodeSocket.cs index 88a069ffc..ddc691341 100644 --- a/src/Listener/PodeSocket.cs +++ b/src/Listener/PodeSocket.cs @@ -15,6 +15,7 @@ public class PodeSocket : IDisposable public int Port { get; private set; } public IPEndPoint Endpoint { get; private set; } public X509Certificate Certificate { get; private set; } + public bool AllowClientCertificate { get; private set; } public SslProtocols Protocols { get; private set; } public Socket Socket { get; private set; } @@ -35,11 +36,12 @@ public int ReceiveTimeout set => Socket.ReceiveTimeout = value; } - public PodeSocket(IPAddress ipAddress, int port, SslProtocols protocols, X509Certificate certificate = null) + public PodeSocket(IPAddress ipAddress, int port, SslProtocols protocols, X509Certificate certificate = null, bool allowClientCertificate = false) { IPAddress = ipAddress; Port = port; Certificate = certificate; + AllowClientCertificate = allowClientCertificate; Protocols = protocols; Endpoint = new IPEndPoint(ipAddress, port); diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index 507f01d31..93662e960 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -59,6 +59,42 @@ function Get-PodeAuthBasicType } } +function Get-PodeAuthClientCertificateType +{ + return { + param($e, $options) + $cert = $e.Request.ClientCertificate + + # ensure we have a client cert + if ($null -eq $cert) { + return @{ + Message = 'No client certificate supplied' + Code = 401 + } + } + + # ensure the cert has a thumbprint + if ([string]::IsNullOrWhiteSpace($cert.Thumbprint)) { + return @{ + Message = 'Invalid client certificate supplied' + Code = 401 + } + } + + # ensure the cert hasn't expired, or has it even started + $now = [datetime]::Now + if (($cert.NotAfter -lt $now) -or ($cert.NotBefore -gt $now)) { + return @{ + Message = 'Invalid client certificate supplied' + Code = 401 + } + } + + # return data for calling validator + return @($cert, $e.Request.ClientCertificateErrors) + } +} + function Get-PodeAuthBearerType { return { diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 4ae024dc6..c6bf2c121 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -33,6 +33,7 @@ function Start-PodeWebServer Address = $_ip Port = $_.Port Certificate = $_.Certificate.Raw + AllowClientCertificate = $_.Certificate.AllowClientCertificate HostName = $_.Url } } @@ -45,7 +46,7 @@ function Start-PodeWebServer { # register endpoints on the listener $endpoints | ForEach-Object { - $socket = [PodeSocket]::new($_.Address, $_.Port, $PodeContext.Server.Sockets.Ssl.Protocols, $_.Certificate) + $socket = [PodeSocket]::new($_.Address, $_.Port, $PodeContext.Server.Sockets.Ssl.Protocols, $_.Certificate, $_.AllowClientCertificate) $socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout $listener.Add($socket) } diff --git a/src/Private/SignalServer.ps1 b/src/Private/SignalServer.ps1 index dcd9d7bca..4fd151408 100644 --- a/src/Private/SignalServer.ps1 +++ b/src/Private/SignalServer.ps1 @@ -15,6 +15,7 @@ function Start-PodeSignalServer Address = $_ip Port = $_.Port Certificate = $_.Certificate.Raw + AllowClientCertificate = $_.Certificate.AllowClientCertificate HostName = $_.Url } } @@ -27,7 +28,7 @@ function Start-PodeSignalServer { # register endpoints on the listener $endpoints | ForEach-Object { - $socket = [PodeSocket]::new($_.Address, $_.Port, $PodeContext.Server.Sockets.Ssl.Protocols, $_.Certificate) + $socket = [PodeSocket]::new($_.Address, $_.Port, $PodeContext.Server.Sockets.Ssl.Protocols, $_.Certificate, $_.AllowClientCertificate) $socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout $listener.Add($socket) } diff --git a/src/Public/Authentication.ps1 b/src/Public/Authentication.ps1 index 572e24b39..e634809c1 100644 --- a/src/Public/Authentication.ps1 +++ b/src/Public/Authentication.ps1 @@ -50,6 +50,9 @@ If supplied, will use the inbuilt Digest Authentication credentials retriever. .PARAMETER Bearer If supplied, will use the inbuilt Bearer Authentication token retriever. +.PARAMETER ClientCertificate +If supplied, will use the inbuilt Client Certificate Authentication validation. + .PARAMETER Scope An optional array of Scopes for Bearer Authentication. (These are case-sensitive) @@ -135,6 +138,10 @@ function New-PodeAuthScheme [switch] $Bearer, + [Parameter(ParameterSetName='ClientCertificate')] + [switch] + $ClientCertificate, + [Parameter(ParameterSetName='Bearer')] [string[]] $Scope @@ -162,6 +169,20 @@ function New-PodeAuthScheme } } + 'clientcertificate' { + return @{ + Name = 'Mutual' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthClientCertificateType) + UsingVariables = $null + } + PostValidator = $null + Scheme = 'http' + Arguments = @{} + } + } + 'digest' { return @{ Name = 'Digest' diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 26877fe03..55b03162a 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -651,6 +651,9 @@ Ignore Adminstrator checks for non-localhost endpoints. .PARAMETER SelfSigned Create and bind a self-signed certifcate for HTTPS endpoints. +.PARAMETER AllowClientCertificate +Allow for client certificates to be sent on requests. + .EXAMPLE Add-PodeEndpoint -Address localhost -Port 8090 -Protocol Http @@ -718,7 +721,10 @@ function Add-PodeEndpoint [Parameter(ParameterSetName='CertSelf')] [switch] - $SelfSigned + $SelfSigned, + + [switch] + $AllowClientCertificate ) # error if serverless @@ -771,6 +777,7 @@ function Add-PodeEndpoint Certificate = @{ Raw = $X509Certificate SelfSigned = $SelfSigned + AllowClientCertificate = $AllowClientCertificate } }