diff --git a/docs/Getting-Started/Migrating/1X-to-2X.md b/docs/Getting-Started/Migrating/1X-to-2X.md index c571cc770..0a2facbd2 100644 --- a/docs/Getting-Started/Migrating/1X-to-2X.md +++ b/docs/Getting-Started/Migrating/1X-to-2X.md @@ -59,6 +59,8 @@ There's also a new [`Add-PodeAuthMiddleware`](../../../Functions/Authentication/ Furthermore, the OpenAPI functions for `Set-PodeOAAuth` and `Set-PodeOAGlobalAuth` have been removed. The new [`Add-PodeAuthMiddleware`](../../../Functions/Authentication/Add-PodeAuthMiddleware) function and `-Authentication` parameter on [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute) set these up for you automatically in OpenAPI. +On `Add-PodeAuth`, `Add-PodeAuthWindowsAd`, and `Add-PodeAuthUserFile` the `-Type` parameter has been renamed to `-Scheme`. If you have always piped `New-PodeAuthScheme` (formally `New-PodeAuthType`) into them, then this won't affect you. + ### Endpoint and Protocol On the following functions: diff --git a/docs/Tutorials/Authentication/Methods/ClientCertificate.md b/docs/Tutorials/Authentication/Methods/ClientCertificate.md new file mode 100644 index 000000000..177365230 --- /dev/null +++ b/docs/Tutorials/Authentication/Methods/ClientCertificate.md @@ -0,0 +1,88 @@ +# Client Certificate + +Client Certificate Authentication is when the server requires the client to supply a certificate on the request, to verify themselves with the server. This only works over HTTPS connections. + +If at any point to you need to access the client's certificate outside of this validator, then it can be found on the web event object at `Request.ClientCertificate`. + +## Setup + +To setup and start using Client Certificate Authentication in Pode you use the `New-PodeAuthScheme -ClientCertificate` function, and then pipe this into the [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function. The [`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth) function's ScriptBlock is supplied the client's certificate, and any SSL errors that may have occurred (like chain issues, etc). + +You will also need to supply `-AllowClientCertificate` to [`Add-PodeEndpoint`], and ensure the `-Protocol` is HTTPS: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -SelfSigned -AllowClientCertificate + + New-PodeAuthScheme -ClientCertificate | Add-PodeAuth -Name 'Login' -Sessionless -ScriptBlock { + param($cert, $errors) + + # check if the client's cert is valid + + return @{ User = $user } + } +} +``` + +By default, Pode will ensure a certificate was supplied, and also ensure the certificate's Before/After dates are valid - if not, a 401 response will be returned. + +## Middleware + +Once configured you can start using Client Certificate Authentication to validate incoming Requests. You can either configure the validation to happen on every Route as global Middleware, or as custom Route Middleware. + +The following will use Client Certificate Authentication to validate every request on every Route: + +```powershell +Start-PodeServer { + Add-PodeAuthMiddleware -Name 'GlobalAuthValidation' -Authentication 'Login' +} +``` + +Whereas the following example will use Client Certificate authentication to only validate requests on specific a Route: + +```powershell +Start-PodeServer { + Add-PodeRoute -Method Get -Path '/info' -Authentication 'Login' -ScriptBlock { + # logic + } +} +``` + +## Full Example + +The following full example of Basic authentication will setup and configure authentication, validate that a users username/password is valid, and then validate on a specific Route: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -SelfSigned -AllowClientCertificate + + # setup client cert authentication to validate a user + New-PodeAuthScheme -ClientCertificate | Add-PodeAuth -Name 'Login' -Sessionless -ScriptBlock { + param($cert, $errors) + + # validate the thumbprint - here you would check a real cert store, or database + if ($cert.Thumbprint -ieq '3571B3BE3CA202FA56F73691FC258E653D0874C1') { + return @{ + User = @{ + ID ='M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + } + } + + # an invalid cert + return @{ Message = 'Invalid certificate supplied' } + } + + # check the request on this route against the authentication + Add-PodeRoute -Method Get -Path '/cpu' -Authentication 'Login' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'cpu' = 82 } + } + + # this route will not be validated against the authentication + Add-PodeRoute -Method Get -Path '/memory' -ScriptBlock { + Write-PodeJsonResponse -Value @{ 'memory' = 14 } + } +} +``` diff --git a/docs/Tutorials/Authentication/Methods/Custom.md b/docs/Tutorials/Authentication/Methods/Custom.md index 6e7a1b6b1..03399d433 100644 --- a/docs/Tutorials/Authentication/Methods/Custom.md +++ b/docs/Tutorials/Authentication/Methods/Custom.md @@ -1,6 +1,6 @@ # Custom -Custom authentication works much like the inbuilt types (Basic/Form/etc), but allows you to specify your own parsing logic, as well as any custom options that might be required. +Custom authentication works much like the inbuilt schemes (Basic/Form/etc), but allows you to specify your own parsing logic, as well as any custom options that might be required. ## Setup and Parsing @@ -12,8 +12,8 @@ The `-ScriptBlock` on [`New-PodeAuthScheme`](../../../../Functions/Authenticatio ```powershell Start-PodeServer { - # define a new custom authentication type - $custom_type = New-PodeAuthScheme -Custom -ScriptBlock { + # define a new custom authentication scheme + $custom_scheme = New-PodeAuthScheme -Custom -ScriptBlock { param($e, $opts) # get client/user/password field names @@ -30,8 +30,8 @@ Start-PodeServer { return @($client, $username, $password) } - # now, add a new custom authentication method using the type you created above - $custom_type | Add-PodeAuth -Name 'Login' -ScriptBlock { + # now, add a new custom authentication validator using the scheme you created above + $custom_scheme | Add-PodeAuth -Name 'Login' -ScriptBlock { param($client, $username, $password) # check if the client is valid in some database @@ -47,9 +47,9 @@ Start-PodeServer { ## Post Validation -The typical setup of authentication is that you create some type to parse the request ([`New-PodeAuthScheme`](../../../../Functions/Authentication/New-PodeAuthScheme)), and then you pipe this into a validator/method to validate the parsed user's credentials ([`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth)). +The typical setup of authentication is that you create some scheme to parse the request ([`New-PodeAuthScheme`](../../../../Functions/Authentication/New-PodeAuthScheme)), and then you pipe this into a validator to validate the parsed user's credentials ([`Add-PodeAuth`](../../../../Functions/Authentication/Add-PodeAuth)). -There is however also an optional `-PostValidator` ScriptBlock that can be passed to your Custom Authentication type on the [`New-PodeAuthScheme`](../../../../Functions/Authentication/New-PodeAuthScheme) function. This `-PostValidator` script runs after normal user validation, and is supplied the current [web event](../../../WebEvent), the original splatted array returned from the [`New-PodeAuthScheme`](../../../../Functions/Authentication/New-PodeAuthScheme) ScriptBlock, the result HashTable from the user validator from `Add-PodeAuth`, and the `-ArgumentList` HashTable from `New-PodeAuthScheme`. You can use this script to re-generate any hashes for further validation, but if successful you *must* return the User object again (ie: re-return the last parameter which is the original validation result). +There is however also an optional `-PostValidator` ScriptBlock that can be passed to your Custom Authentication scheme on the [`New-PodeAuthScheme`](../../../../Functions/Authentication/New-PodeAuthScheme) function. This `-PostValidator` script runs after normal user validation, and is supplied the current [web event](../../../WebEvent), the original splatted array returned from the [`New-PodeAuthScheme`](../../../../Functions/Authentication/New-PodeAuthScheme) ScriptBlock, the result HashTable from the user validator from `Add-PodeAuth`, and the `-ArgumentList` HashTable from `New-PodeAuthScheme`. You can use this script to re-generate any hashes for further validation, but if successful you *must* return the User object again (ie: re-return the last parameter which is the original validation result). For example, if you have a post validator script for the above Client Custom Authentication, then it would be supplied the following parameters: @@ -59,14 +59,14 @@ For example, if you have a post validator script for the above Client Custom Aut * Password * ClientName * Validation Result -* Type ArgumentsList +* Scheme ArgumentsList For example: ```powershell Start-PodeServer { - # define a new custom authentication type - $custom_type = New-PodeAuthScheme -Custom -ScriptBlock { + # define a new custom authentication scheme + $custom_scheme = New-PodeAuthScheme -Custom -ScriptBlock { param($e, $opts) # get client/user/password field names @@ -91,8 +91,8 @@ Start-PodeServer { return $result } - # now, add a new custom authentication method using the type you created above - $custom_type | Add-PodeAuth -Name 'Login' -ScriptBlock { + # now, add a new custom authentication method using the scheme you created above + $custom_scheme | Add-PodeAuth -Name 'Login' -ScriptBlock { param($client, $username, $password) # check if the client is valid in some database @@ -133,8 +133,8 @@ The following full example of Custom authentication will setup and configure aut Start-PodeServer { Add-PodeEndpoint -Address * -Port 8080 -Protocol Http - # define a new custom authentication type - $custom_type = New-PodeAuthScheme -Custom -ScriptBlock { + # define a new custom authentication scheme + $custom_scheme = New-PodeAuthScheme -Custom -ScriptBlock { param($e, $opts) # get client/user/pass field names to get from payload @@ -152,7 +152,7 @@ Start-PodeServer { } # now, add a new custom authentication method - $custom_type | Add-PodeAuth -Name 'Login' -Sessionless -ScriptBlock { + $custom_scheme | Add-PodeAuth -Name 'Login' -Sessionless -ScriptBlock { param($client, $username, $password) # check if the client is valid diff --git a/docs/Tutorials/Authentication/Overview.md b/docs/Tutorials/Authentication/Overview.md index 96a9d6b82..40b49db80 100644 --- a/docs/Tutorials/Authentication/Overview.md +++ b/docs/Tutorials/Authentication/Overview.md @@ -9,11 +9,11 @@ To setup and use authentication in Pode you need to use the [`New-PodeAuthScheme ## Usage -### Types/Parsers +### Schemes -The [`New-PodeAuthScheme`](../../../Functions/Authentication/New-PodeAuthScheme) function allows you to create and configure authentication types/parsers, or you can create your own Custom authentication types. These types can then be used on the [`Add-PodeAuth`](../../../Functions/Authentication/Add-PodeAuth) function. There job is to parse the request for any user credentials, or other information, that is required for a user to be authenticated. +The [`New-PodeAuthScheme`](../../../Functions/Authentication/New-PodeAuthScheme) function allows you to create and configure authentication schemes, or you can create your own Custom authentication schemes. These schemes can then be used on the [`Add-PodeAuth`](../../../Functions/Authentication/Add-PodeAuth) function. There job is to parse the request for any user credentials, or other information, that is required for a user to be authenticated. -An example of creating some authentication types is as follows: +An example of creating some authentication schemes is as follows: ```powershell Start-PodeServer { @@ -21,10 +21,11 @@ Start-PodeServer { $digest_auth = New-PodeAuthScheme -Digest $bearer_auth = New-PodeAuthScheme -Bearer $form_auth = New-PodeAuthScheme -Form + $cert_auth = New-PodeAuthScheme -ClientCertificate } ``` -Where as the following example defines a Custom type that retrieves the user's credentials from the Request's Payload: +Where as the following example defines a Custom scheme that retrieves the user's credentials from the Request's Payload: ```powershell Start-PodeServer { @@ -47,9 +48,9 @@ Start-PodeServer { } ``` -### Method/Validator +### Validators -The [`Add-PodeAuth`](../../../Functions/Authentication/Add-PodeAuth) function allows you to add authentication methods/validators to your server. You can have many methods configured, defining which one to validate against using the `-Authentication` parameter on Routes. Their job is to validate the information parsed from the supplied scheme to ensure a user is valid. +The [`Add-PodeAuth`](../../../Functions/Authentication/Add-PodeAuth) function allows you to add authentication validators to your server. You can have many methods configured, defining which one to validate against using the `-Authentication` parameter on Routes. Their job is to validate the information parsed from the supplied scheme to ensure a user is valid. An example of using [`Add-PodeAuth`](../../../Functions/Authentication/Add-PodeAuth) for Basic sessionless authentication is as follows: @@ -63,7 +64,7 @@ Start-PodeServer { } ``` -The `-Name` of the authentication method must be unique. The `-Type` comes from the object returned via the [`New-PodeAuthScheme`](../../../Functions/Authentication/New-PodeAuthScheme) function, and can also be piped in. +The `-Name` of the authentication method must be unique. The `-Scheme` comes from the object returned via the [`New-PodeAuthScheme`](../../../Functions/Authentication/New-PodeAuthScheme) function, and can also be piped in. The `-ScriptBlock` is used to validate a user, checking if they exist and the password is correct (or checking if they exist in some data store). If the ScriptBlock succeeds, then a `User` object needs to be returned from the script as `@{ User = $user }`. If `$null`, or a null user, is returned then the script is assumed to have failed - meaning the user will have failed authentication, and a 401 response is returned. @@ -132,9 +133,24 @@ WWW-Authenticate: Basic realm="Enter creds to access site" !!! note If no Realm was set then it would just look as follows: `WWW-Authenticate: Basic` +#### WebEvent + +By default the web event for the current request is not supplied to the validator's ScriptBlock. If you ever need the web event though, such as for accessing other request details like a client certificate, then you can supply the `-PassEvent` switch on [`Add-PodeAuth`](../../../Functions/Authentication/Add-PodeAuth). With this, Pode will supply the current web event as the first parameter: + +```powershell +Start-PodeServer { + New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Login' -Sessionless -PassEvent -ScriptBlock { + param($e, $username, $pass) + # logic to check user + # logic to check client cert (found at: $e.Request.ClientCertificate) + return @{ 'user' = $user } + } +} +``` + ### Routes/Middleware -To use an authentication type on a specific route, you can use the `-Authentication` parameter on the [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute) function; this takes the Name supplied to the `-Name` parameter on [`Add-PodeAuth`](../../../Functions/Authentication/Add-PodeAuth). This will set the authentication up to run before other route middleware. +To use an authentication on a specific route, you can use the `-Authentication` parameter on the [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute) function; this takes the Name supplied to the `-Name` parameter on [`Add-PodeAuth`](../../../Functions/Authentication/Add-PodeAuth). This will set the authentication up to run before other route middleware. An example of using some Basic authentication on a REST API route is as follows: diff --git a/docs/Tutorials/WebEvent.md b/docs/Tutorials/WebEvent.md index 75ee7adb4..f3813eaa3 100644 --- a/docs/Tutorials/WebEvent.md +++ b/docs/Tutorials/WebEvent.md @@ -43,3 +43,5 @@ Add-PodeRoute -Method Get -Path '/' -ScriptBlock { ## Customise The web event itself is just a HashTable, which means you can add your own properties to it within Middleware for further use in other Middleware down the flow, or in the Route itself. + +Make sure these custom properties have a unique name, so as to not clash with already existing properties. diff --git a/examples/web-auth-clientcert.ps1 b/examples/web-auth-clientcert.ps1 new file mode 100644 index 000000000..c8eaebff8 --- /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 '3571B3BE3CA202FA56F73691FC258E653D0874C1') { + 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..722568436 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 { @@ -633,18 +669,23 @@ function Get-PodeAuthMiddlewareScript } try { - # run auth type script to parse request for data - $_args = @($e) + @($auth.Type.Arguments) - if ($null -ne $auth.Type.ScriptBlock.UsingVariables) { - $_args = @($auth.Type.ScriptBlock.UsingVariables.Value) + $_args + # run auth scheme script to parse request for data + $_args = @($e) + @($auth.Scheme.Arguments) + if ($null -ne $auth.Scheme.ScriptBlock.UsingVariables) { + $_args = @($auth.Scheme.ScriptBlock.UsingVariables.Value) + $_args } - $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Type.ScriptBlock.Script -Arguments $_args -Return -Splat) + $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.ScriptBlock.Script -Arguments $_args -Return -Splat) # if data is a hashtable, then don't call validator (parser either failed, or forced a success) if ($result -isnot [hashtable]) { $original = $result + $_args = @($result) + @($auth.Arguments) + if ($auth.PassEvent) { + $_args = @($e) + $_args + } + if ($null -ne $auth.UsingVariables) { $_args = @($auth.UsingVariables.Value) + $_args } @@ -652,13 +693,13 @@ function Get-PodeAuthMiddlewareScript $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.ScriptBlock -Arguments $_args -Return -Splat) # if we have user, then run post validator if present - if ([string]::IsNullOrWhiteSpace($result.Code) -and !(Test-PodeIsEmpty $auth.Type.PostValidator.Script)) { - $_args = @($e) + @($original) + @($result) + @($auth.Type.Arguments) - if ($null -ne $auth.Type.PostValidator.UsingVariables) { - $_args = @($auth.Type.PostValidator.UsingVariables.Value) + $_args + if ([string]::IsNullOrWhiteSpace($result.Code) -and !(Test-PodeIsEmpty $auth.Scheme.PostValidator.Script)) { + $_args = @($e) + @($original) + @($result) + @($auth.Scheme.Arguments) + if ($null -ne $auth.Scheme.PostValidator.UsingVariables) { + $_args = @($auth.Scheme.PostValidator.UsingVariables.Value) + $_args } - $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Type.PostValidator.Script -Arguments $_args -Return -Splat) + $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.PostValidator.Script -Arguments $_args -Return -Splat) } } } @@ -676,7 +717,7 @@ function Get-PodeAuthMiddlewareScript $validHeaders = (($null -eq $result.Headers) -or !$result.Headers.ContainsKey('WWW-Authenticate')) if ($validCode -and $validHeaders) { - $_wwwHeader = Get-PodeAuthWwwHeaderValue -Name $auth.Type.Name -Realm $auth.Type.Realm -Challenge $result.Challenge + $_wwwHeader = Get-PodeAuthWwwHeaderValue -Name $auth.Scheme.Name -Realm $auth.Scheme.Realm -Challenge $result.Challenge if (![string]::IsNullOrWhiteSpace($_wwwHeader)) { Set-PodeHeader -Name 'WWW-Authenticate' -Value $_wwwHeader } 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..83a6a36d0 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) @@ -98,7 +101,7 @@ function New-PodeAuthScheme [Parameter(Mandatory=$true, ParameterSetName='Custom')] [ValidateScript({ if (Test-PodeIsEmpty $_) { - throw "A non-empty ScriptBlock is required for the Custom authentication type" + throw "A non-empty ScriptBlock is required for the Custom authentication scheme" } return $true @@ -135,6 +138,10 @@ function New-PodeAuthScheme [switch] $Bearer, + [Parameter(ParameterSetName='ClientCertificate')] + [switch] + $ClientCertificate, + [Parameter(ParameterSetName='Bearer')] [string[]] $Scope @@ -143,7 +150,7 @@ function New-PodeAuthScheme # default realm $_realm = 'User' - # configure the auth type + # configure the auth scheme switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { 'basic' { return @{ @@ -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' @@ -252,8 +273,8 @@ Adds a custom Authentication method for verifying users. .PARAMETER Name A unique Name for the Authentication method. -.PARAMETER Type -The Type to use for retrieving credentials (From New-PodeAuthScheme). +.PARAMETER Scheme +The Scheme to use for retrieving credentials (From New-PodeAuthScheme). .PARAMETER ScriptBlock The ScriptBlock defining logic that retrieves and verifys a user. @@ -273,6 +294,9 @@ The URL to redirect to when authentication succeeds when logging in. .PARAMETER Sessionless If supplied, authenticated users will not be stored in sessions, and sessions will not be used. +.PARAMETER PassEvent +If supplied, the current web event will be supplied as the first parameter to the ScriptBlock. + .EXAMPLE New-PodeAuthScheme -Form | Add-PodeAuth -Name 'Main' -ScriptBlock { /* logic */ } #> @@ -286,7 +310,7 @@ function Add-PodeAuth [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [hashtable] - $Type, + $Scheme, [Parameter(Mandatory=$true)] [ValidateScript({ @@ -316,7 +340,10 @@ function Add-PodeAuth $SuccessUrl, [switch] - $Sessionless + $Sessionless, + + [switch] + $PassEvent ) # ensure the name doesn't already exist @@ -324,9 +351,9 @@ function Add-PodeAuth throw "Authentication method already defined: $($Name)" } - # ensure the Type contains a scriptblock - if (Test-PodeIsEmpty $Type.ScriptBlock) { - throw "The supplied Type for the '$($Name)' authentication method requires a valid ScriptBlock" + # ensure the Scheme contains a scriptblock + if (Test-PodeIsEmpty $Scheme.ScriptBlock) { + throw "The supplied Scheme for the '$($Name)' authentication validator requires a valid ScriptBlock" } # if we're using sessions, ensure sessions have been setup @@ -339,11 +366,12 @@ function Add-PodeAuth # add auth method to server $PodeContext.Server.Authentications[$Name] = @{ - Type = $Type + Scheme = $Scheme ScriptBlock = $ScriptBlock UsingVariables = $usingVars Arguments = $ArgumentList Sessionless = $Sessionless + PassEvent = $PassEvent Failure = @{ Url = $FailureUrl Message = $FailureMessage @@ -364,8 +392,8 @@ Adds the inbuilt Windows AD Authentication method for verifying users. .PARAMETER Name A unique Name for the Authentication method. -.PARAMETER Type -The Type to use for retrieving credentials (From New-PodeAuthScheme). +.PARAMETER Scheme +The Scheme to use for retrieving credentials (From New-PodeAuthScheme). .PARAMETER Fqdn A custom FQDN for the DNS of the AD you wish to authenticate against. (Alias: Server) @@ -419,7 +447,7 @@ function Add-PodeAuthWindowsAd [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [hashtable] - $Type, + $Scheme, [Parameter()] [Alias('Server')] @@ -466,9 +494,9 @@ function Add-PodeAuthWindowsAd throw "Windows AD Authentication method already defined: $($Name)" } - # ensure the Type contains a scriptblock - if (Test-PodeIsEmpty $Type.ScriptBlock) { - throw "The supplied Type for the '$($Name)' Windows AD authentication method requires a valid ScriptBlock" + # ensure the Scheme contains a scriptblock + if (Test-PodeIsEmpty $Scheme.ScriptBlock) { + throw "The supplied Scheme for the '$($Name)' Windows AD authentication validator requires a valid ScriptBlock" } # if we're using sessions, ensure sessions have been setup @@ -492,7 +520,7 @@ function Add-PodeAuthWindowsAd # add Windows AD auth method to server $PodeContext.Server.Authentications[$Name] = @{ - Type = $Type + Scheme = $Scheme ScriptBlock = (Get-PodeAuthWindowsADMethod) Arguments = @{ Server = $Fqdn @@ -699,8 +727,8 @@ function Add-PodeAuthIIS throw "IIS Authentication method already defined: $($Name)" } - # create the auth tye for getting the token header - $type = New-PodeAuthScheme -Custom -ScriptBlock { + # create the auth scheme for getting the token header + $scheme = New-PodeAuthScheme -Custom -ScriptBlock { param($e, $options) $header = 'MS-ASPNETCORE-WINAUTHTOKEN' @@ -721,7 +749,7 @@ function Add-PodeAuthIIS # add a custom auth method to validate the user $method = Get-PodeAuthWindowsADIISMethod - $type | Add-PodeAuth ` + $scheme | Add-PodeAuth ` -Name $Name ` -ScriptBlock $method ` -FailureUrl $FailureUrl ` @@ -746,8 +774,8 @@ Adds the inbuilt User File Authentication method for verifying users. .PARAMETER Name A unique Name for the Authentication method. -.PARAMETER Type -The Type to use for retrieving credentials (From New-PodeAuthScheme). +.PARAMETER Scheme +The Scheme to use for retrieving credentials (From New-PodeAuthScheme). .PARAMETER FilePath A path to a users JSON file (Default: ./users.json) @@ -789,7 +817,7 @@ function Add-PodeAuthUserFile [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [hashtable] - $Type, + $Scheme, [Parameter()] [string] @@ -828,9 +856,9 @@ function Add-PodeAuthUserFile throw "User File Authentication method already defined: $($Name)" } - # ensure the Type contains a scriptblock - if (Test-PodeIsEmpty $Type.ScriptBlock) { - throw "The supplied Type for the '$($Name)' User File authentication method requires a valid ScriptBlock" + # ensure the Scheme contains a scriptblock + if (Test-PodeIsEmpty $Scheme.ScriptBlock) { + throw "The supplied Scheme for the '$($Name)' User File authentication validator requires a valid ScriptBlock" } # if we're using sessions, ensure sessions have been setup @@ -853,7 +881,7 @@ function Add-PodeAuthUserFile # add Windows AD auth method to server $PodeContext.Server.Authentications[$Name] = @{ - Type = $Type + Scheme = $Scheme ScriptBlock = (Get-PodeAuthUserFileMethod) Arguments = @{ FilePath = $FilePath diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 26877fe03..9b119aafc 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 @@ -756,6 +762,11 @@ function Add-PodeEndpoint throw "An endpoint with the name '$($Name)' has already been defined" } + # protocol must be https for client certs + if (($Protocol -ine 'https') -and $AllowClientCertificate) { + throw "Client certificates are only supported on HTTPS endpoints" + } + # new endpoint object $obj = @{ Name = $Name @@ -771,6 +782,7 @@ function Add-PodeEndpoint Certificate = @{ Raw = $X509Certificate SelfSigned = $SelfSigned + AllowClientCertificate = $AllowClientCertificate } }