From d0449f933707f355ad60b4bbc7c8fd2fe429e267 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 20 Jan 2025 08:26:49 -0800 Subject: [PATCH 01/13] Update Helpers.ps1 --- src/Private/Helpers.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 8e4e9f37d..c4b23ea9b 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -2237,7 +2237,8 @@ function Find-PodeErrorPage { [string] $ContentType ) - +write-podehost $Code +write-podehost $ContentType # if a defined content type is supplied, attempt to find an error page for that first if (![string]::IsNullOrWhiteSpace($ContentType)) { $path = Get-PodeErrorPage -Code $Code -ContentType $ContentType From 1a423f9b09c464f0023039be836d7f5ff693fa36 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 21 Jan 2025 09:46:12 -0800 Subject: [PATCH 02/13] update 1 --- examples/customerror.ps1 | 72 +++++++++++++++++++++++++++++++++++ src/Pode.psd1 | 1 + src/Public/Authentication.ps1 | 22 +++++++++++ src/Public/Routes.ps1 | 72 ++++++++++++++++++----------------- 4 files changed, 133 insertions(+), 34 deletions(-) create mode 100644 examples/customerror.ps1 diff --git a/examples/customerror.ps1 b/examples/customerror.ps1 new file mode 100644 index 000000000..b12c983e3 --- /dev/null +++ b/examples/customerror.ps1 @@ -0,0 +1,72 @@ +<# +.SYNOPSIS + A sample PowerShell script to set up a Pode server with a view engine and file monitoring. + +.DESCRIPTION + This script sets up a Pode server listening on port 8081, uses Pode's view engine for rendering + web pages, and configures the server to monitor file changes and restart automatically. + +.EXAMPLE + To run the sample: ./File-Monitoring.ps1 + + Invoke-RestMethod -Uri http://localhost:8081 -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/File-Monitoring.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script path and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source path if it exists, otherwise from installed modules + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + + +Start-PodeServer { + Add-PodeEndpoint -Address 'localhost' -Protocol 'Http' -Port '80' + Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -DisableMinimalDefinitions -NoDefaultResponses + Add-PodeOAInfo -Title 'test custom auth error' -Version 1.0.0 + Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' + Enable-PodeOAViewer -Bookmarks -Path '/docs' + Add-PodeOAServerEndpoint -url '/api/v3' -Description 'default endpoint' + New-PodeAuthScheme -ApiKey | Add-PodeAuth -Name 'APIKey' -Sessionless -ScriptBlock { + param($key) + write-podehost $key + write-podehost $WebEvent -Explode + if ($key -eq 'test_user') { + return @{ success=$true; User = 'test_user' } + } + return @{ success=$false; User = $key } + } + + + + Add-PodeRoute -PassThru -Method 'Get' -Path '/api/v3/' -Authentication 'APIKey' -NoValidation -ScriptBlock { + $auth = Invoke-PodeAuth -Name 'APIKey' + + write-podehost $auth -Explode + if ($auth.Success) { + Write-PodeJsonResponse -Value @{ + Username = $auth.User + } + + } + else { + Write-PodeJsonResponse -Value @{ message = 'Unauthorized' ; user = $auth.User } -StatusCode 401 + } + + } | Set-PodeOARouteInfo -Summary 'Who am I' -Tags 'auth' -OperationId 'whoami' +} \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index ad02ac21c..41b9a62c0 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -265,6 +265,7 @@ 'Test-PodeAuthExists', 'Get-PodeAuthUser', 'Add-PodeAuthSession', + 'Invoke-PodeAuth', # access 'New-PodeAccessScheme', diff --git a/src/Public/Authentication.ps1 b/src/Public/Authentication.ps1 index 64d6ea2df..823a2dc7b 100644 --- a/src/Public/Authentication.ps1 +++ b/src/Public/Authentication.ps1 @@ -1173,6 +1173,28 @@ function Test-PodeAuth { return $true } + +function Invoke-PodeAuth { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + try { + $result = Invoke-PodeAuthValidation -Name $Name + + } + catch { + $_ | Write-PodeErrorLog + } + + return $result +} + + + <# .SYNOPSIS Adds the inbuilt Windows AD Authentication method for verifying users. diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 575fede7f..35cb4c99f 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -165,6 +165,9 @@ function Add-PodeRoute { [string] $Access, + [switch] + $NoValidation, + [Parameter()] [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')] [string] @@ -317,47 +320,48 @@ function Add-PodeRoute { # convert any middleware into valid hashtables $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState) + if ( ! $NoValidation) { + # if an access name was supplied, setup access as middleware first to it's after auth middleware + if (![string]::IsNullOrWhiteSpace($Access)) { + if ([string]::IsNullOrWhiteSpace($Authentication)) { + # Access requires Authentication to be supplied on Routes + throw ($PodeLocale.accessRequiresAuthenticationOnRoutesExceptionMessage) + } - # if an access name was supplied, setup access as middleware first to it's after auth middleware - if (![string]::IsNullOrWhiteSpace($Access)) { - if ([string]::IsNullOrWhiteSpace($Authentication)) { - # Access requires Authentication to be supplied on Routes - throw ($PodeLocale.accessRequiresAuthenticationOnRoutesExceptionMessage) - } + if (!(Test-PodeAccessExists -Name $Access)) { + # Access method does not exist + throw ($PodeLocale.accessMethodDoesNotExistExceptionMessage -f $Access) + } - if (!(Test-PodeAccessExists -Name $Access)) { - # Access method does not exist - throw ($PodeLocale.accessMethodDoesNotExistExceptionMessage -f $Access) - } + $options = @{ + Name = $Access + } + $Middleware = (@(Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware) - $options = @{ - Name = $Access } - $Middleware = (@(Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware) - } + # if an auth name was supplied, setup the auth as the first middleware + if (![string]::IsNullOrWhiteSpace($Authentication)) { + if (!(Test-PodeAuthExists -Name $Authentication)) { + # Authentication method does not exist + throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Authentication) + } - # if an auth name was supplied, setup the auth as the first middleware - if (![string]::IsNullOrWhiteSpace($Authentication)) { - if (!(Test-PodeAuthExists -Name $Authentication)) { - # Authentication method does not exist - throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Authentication) - } + $options = @{ + Name = $Authentication + Login = $Login + Logout = $Logout + Anon = $AllowAnon + } - $options = @{ - Name = $Authentication - Login = $Login - Logout = $Logout - Anon = $AllowAnon + $Middleware = (@(Get-PodeAuthMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware) } - $Middleware = (@(Get-PodeAuthMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware) - } - - # custom access - if ($null -eq $CustomAccess) { - $CustomAccess = @{} - } + # custom access + if ($null -eq $CustomAccess) { + $CustomAccess = @{} + } + } # workout a default content type for the route $ContentType = Find-PodeRouteContentType -Path $Path -ContentType $ContentType @@ -410,8 +414,8 @@ function Add-PodeRoute { Logic = $ScriptBlock UsingVariables = $usingVars Middleware = $Middleware - Authentication = $Authentication - Access = $Access + Authentication = $(if (!$NoValidation) { $Authentication }else {}) + Access = $(if (!$NoValidation) { $Access }else {}) AccessMeta = @{ Role = $Role Group = $Group From 1882975522edd6b7b0c817550d3b6555a30d3f2f Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 21 Jan 2025 18:15:38 -0800 Subject: [PATCH 03/13] update 2 --- examples/customerror.ps1 | 15 ++++---- src/Locales/en-us/Pode.psd1 | 6 +++- src/Private/Authentication.ps1 | 22 ++++++++++-- src/Public/Authentication.ps1 | 33 +++++++++++++++++- src/Public/Routes.ps1 | 62 ++++++++++++++++++---------------- 5 files changed, 98 insertions(+), 40 deletions(-) diff --git a/examples/customerror.ps1 b/examples/customerror.ps1 index b12c983e3..0cc891ae9 100644 --- a/examples/customerror.ps1 +++ b/examples/customerror.ps1 @@ -44,28 +44,29 @@ Start-PodeServer { Add-PodeOAServerEndpoint -url '/api/v3' -Description 'default endpoint' New-PodeAuthScheme -ApiKey | Add-PodeAuth -Name 'APIKey' -Sessionless -ScriptBlock { param($key) - write-podehost $key - write-podehost $WebEvent -Explode + if (!$key) { + return @{ success = $false; reason = 'No X-API-KEY Header found' } + } if ($key -eq 'test_user') { - return @{ success=$true; User = 'test_user' } + return @{ success = $true; User = 'test_user'; userId = 1 } } - return @{ success=$false; User = $key } + return @{ success = $false; User = $key; userId = -1 ;reason = 'Not existing user'} } - Add-PodeRoute -PassThru -Method 'Get' -Path '/api/v3/' -Authentication 'APIKey' -NoValidation -ScriptBlock { + Add-PodeRoute -PassThru -Method 'Get' -Path '/api/v3/' -Authentication 'APIKey' -NoMiddlewareAuthentication -ScriptBlock { $auth = Invoke-PodeAuth -Name 'APIKey' write-podehost $auth -Explode - if ($auth.Success) { + if ($auth.Success ) { Write-PodeJsonResponse -Value @{ Username = $auth.User } } else { - Write-PodeJsonResponse -Value @{ message = 'Unauthorized' ; user = $auth.User } -StatusCode 401 + Write-PodeJsonResponse -Value @{ message = $auth.reason ; user = $auth.User } -StatusCode 401 } } | Set-PodeOARouteInfo -Summary 'Who am I' -Tags 'auth' -OperationId 'whoami' diff --git a/src/Locales/en-us/Pode.psd1 b/src/Locales/en-us/Pode.psd1 index 56340e18f..de9614ad0 100644 --- a/src/Locales/en-us/Pode.psd1 +++ b/src/Locales/en-us/Pode.psd1 @@ -291,4 +291,8 @@ getRequestBodyNotAllowedExceptionMessage = "'{0}' operations cannot have a Request Body. Use -AllowNonStandardBody to override this restriction." fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' - LocalEndpointConflictExceptionMessage = "Both '{0}' and '{1}' are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition."} \ No newline at end of file + LocalEndpointConflictExceptionMessage = "Both '{0}' and '{1}' are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition." + authMethodDoesNotExistExceptionMessage = "Authentication method doesn't exist: {0}" + authenticationMethodMergedExceptionMessage = 'Authentication method {0} is merged' + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "The parameter '-NoMiddlewareAuthentication' can only be used when '-Authentication' is specified." +} \ No newline at end of file diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index f5a2273fd..54dc29096 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -1201,10 +1201,14 @@ function Test-PodeAuthValidation { param( [Parameter(Mandatory = $true)] [string] - $Name + $Name, + + [switch] + $RouteScript ) try { + # get auth method $auth = $PodeContext.Server.Authentications.Methods[$Name] @@ -1220,6 +1224,8 @@ function Test-PodeAuthValidation { } } + Wait-Debugger + # run auth scheme script to parse request for data $_args = @(Merge-PodeScriptblockArguments -ArgumentList $auth.Scheme.Arguments -UsingVariables $auth.Scheme.ScriptBlock.UsingVariables) @@ -1249,7 +1255,7 @@ function Test-PodeAuthValidation { $_args += , $schemes } - if ($null -eq $result) { + if ($null -eq $result -and !$RouteScript ) { $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.ScriptBlock.Script -Arguments $_args -Return -Splat) } @@ -1275,6 +1281,18 @@ function Test-PodeAuthValidation { } } + if ($RouteScript -and ($null -ne $result) -and ($result -is [hashtable])) { + $ret = @{ + Success = $true + User = '' + Headers = '' + } + foreach ($key in $result.Keys) { + $ret[$key] = $result[$key] # Overwrites if key exists + } + return $ret + } + # if there's no result, or no user, then the auth failed - but allow auth if anon enabled if (($null -eq $result) -or ($result.Count -eq 0) -or (Test-PodeIsEmpty $result.User)) { $code = (Protect-PodeValue -Value $result.Code -Default 401) diff --git a/src/Public/Authentication.ps1 b/src/Public/Authentication.ps1 index 823a2dc7b..246682d89 100644 --- a/src/Public/Authentication.ps1 +++ b/src/Public/Authentication.ps1 @@ -1146,6 +1146,10 @@ function Test-PodeAuth { $IgnoreSession ) + if (! (Test-PodeAuthExists -Name $Name)) { + throw ($PodeLocale.authMethodDoesNotExistExceptionMessage -f $Name) + } + # if the session already has a user/isAuth'd, then skip auth - or allow anon if (!$IgnoreSession -and (Test-PodeSessionsInUse) -and (Test-PodeAuthUser)) { return $true @@ -1173,7 +1177,22 @@ function Test-PodeAuth { return $true } +<# +.SYNOPSIS + Invokes an authentication method in Pode. + +.DESCRIPTION + This function attempts to invoke an authentication method by its name, + ensuring that it exists and has not been merged. If the authentication + method does not exist or is merged, it throws an exception. + +.PARAMETER Name + The name of the authentication method to invoke. This parameter is mandatory. + +.OUTPUTS + A hashtable containing the authentication result, including success status,user information, and headers. +#> function Invoke-PodeAuth { [CmdletBinding()] param( @@ -1183,8 +1202,20 @@ function Invoke-PodeAuth { ) try { - $result = Invoke-PodeAuthValidation -Name $Name + # Check if the authentication method exists + if (! (Test-PodeAuthExists -Name $Name)) { + # Authentication method doesn't exist: + throw ($PodeLocale.authMethodDoesNotExistExceptionMessage -f $Name) + } + + # Ensure the authentication method is not merged + if ($PodeContext.Server.Authentications.Methods[$Name].Merged) { + # Authentication method {0} is merged + throw ($PodeLocale.authenticationMethodMergedExceptionMessage -f $Name) + } + # Perform authentication validation + $result = Test-PodeAuthValidation -Name $Name -RouteScript } catch { $_ | Write-PodeErrorLog diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 35cb4c99f..9149b8f17 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -166,7 +166,7 @@ function Add-PodeRoute { $Access, [switch] - $NoValidation, + $NoMiddlewareAuthentication, [Parameter()] [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')] @@ -320,33 +320,34 @@ function Add-PodeRoute { # convert any middleware into valid hashtables $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState) - if ( ! $NoValidation) { - # if an access name was supplied, setup access as middleware first to it's after auth middleware - if (![string]::IsNullOrWhiteSpace($Access)) { - if ([string]::IsNullOrWhiteSpace($Authentication)) { - # Access requires Authentication to be supplied on Routes - throw ($PodeLocale.accessRequiresAuthenticationOnRoutesExceptionMessage) - } - if (!(Test-PodeAccessExists -Name $Access)) { - # Access method does not exist - throw ($PodeLocale.accessMethodDoesNotExistExceptionMessage -f $Access) - } + # if an access name was supplied, setup access as middleware first to it's after auth middleware + if (![string]::IsNullOrWhiteSpace($Access)) { + if ([string]::IsNullOrWhiteSpace($Authentication)) { + # Access requires Authentication to be supplied on Routes + throw ($PodeLocale.accessRequiresAuthenticationOnRoutesExceptionMessage) + } - $options = @{ - Name = $Access - } - $Middleware = (@(Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware) + if (!(Test-PodeAccessExists -Name $Access)) { + # Access method does not exist + throw ($PodeLocale.accessMethodDoesNotExistExceptionMessage -f $Access) + } + $options = @{ + Name = $Access } - # if an auth name was supplied, setup the auth as the first middleware - if (![string]::IsNullOrWhiteSpace($Authentication)) { - if (!(Test-PodeAuthExists -Name $Authentication)) { - # Authentication method does not exist - throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Authentication) - } + $Middleware = (@(Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware) + } + + # if an auth name was supplied, setup the auth as the first middleware + if (![string]::IsNullOrWhiteSpace($Authentication)) { + if (!(Test-PodeAuthExists -Name $Authentication)) { + # Authentication method does not exist + throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Authentication) + } + if ( ! $NoMiddlewareAuthentication) { $options = @{ Name = $Authentication Login = $Login @@ -356,12 +357,15 @@ function Add-PodeRoute { $Middleware = (@(Get-PodeAuthMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware) } + } + elseif ($NoMiddlewareAuthentication) { + throw "Parameter '-NoMiddlewareAuthentication' can be only used with '-Authentication'" + } - # custom access - if ($null -eq $CustomAccess) { - $CustomAccess = @{} - } - } + # custom access + if ($null -eq $CustomAccess) { + $CustomAccess = @{} + } # workout a default content type for the route $ContentType = Find-PodeRouteContentType -Path $Path -ContentType $ContentType @@ -414,8 +418,8 @@ function Add-PodeRoute { Logic = $ScriptBlock UsingVariables = $usingVars Middleware = $Middleware - Authentication = $(if (!$NoValidation) { $Authentication }else {}) - Access = $(if (!$NoValidation) { $Access }else {}) + Authentication = $(if (!$NoMiddlewareAuthentication) { $Authentication } else {}) + Access = $Access AccessMeta = @{ Role = $Role Group = $Group From 256a0aedfe56d5f36110afd3db03c1cf22aeef6c Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 21 Jan 2025 18:47:08 -0800 Subject: [PATCH 04/13] first drop --- examples/ManualAuthErrorHandling.ps1 | 99 +++++++++++++++++++++++ examples/customerror.ps1 | 73 ----------------- src/Locales/ar/Pode.psd1 | 3 + src/Locales/de/Pode.psd1 | 3 + src/Locales/en/Pode.psd1 | 3 + src/Locales/es/Pode.psd1 | 3 + src/Locales/fr/Pode.psd1 | 3 + src/Locales/it/Pode.psd1 | 3 + src/Locales/ja/Pode.psd1 | 3 + src/Locales/ko/Pode.psd1 | 3 + src/Locales/nl/Pode.psd1 | 3 + src/Locales/pl/Pode.psd1 | 3 + src/Locales/pt/Pode.psd1 | 3 + src/Locales/zh/Pode.psd1 | 3 + src/Private/Authentication.ps1 | 69 ++++++++++++----- src/Public/Authentication.ps1 | 23 +++--- src/Public/Routes.ps1 | 112 ++++++++++++++------------- tests/unit/Authentication.Tests.ps1 | 44 +++++++++++ 18 files changed, 296 insertions(+), 160 deletions(-) create mode 100644 examples/ManualAuthErrorHandling.ps1 delete mode 100644 examples/customerror.ps1 diff --git a/examples/ManualAuthErrorHandling.ps1 b/examples/ManualAuthErrorHandling.ps1 new file mode 100644 index 000000000..79f2e3b93 --- /dev/null +++ b/examples/ManualAuthErrorHandling.ps1 @@ -0,0 +1,99 @@ +<# +.SYNOPSIS + A Pode server setup with manual authentication error handling. + +.DESCRIPTION + This script initializes a Pode web server on port 80 and configures custom API key authentication. + Instead of using Pode's default error handling, it manually processes authentication failures, providing + detailed responses based on the authentication result. + +.EXAMPLE + To run the script: + + ./ManualAuthErrorHandling.ps1 + + Test it using: + + Invoke-RestMethod -Uri http://localhost/api/v3/ -Headers @{ 'X-API-KEY' = 'test_user' } -Method Get + +.LINK + https://github.com/Badgerati/Pode/blob/develop/examples/ManualAuthErrorHandling.ps1 + +.NOTES + Author: Pode Team + License: MIT License +#> + +try { + # Determine the script's directory and Pode module path + $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) + $podePath = Split-Path -Parent -Path $ScriptPath + + # Import the Pode module from the source directory if available, otherwise use the installed module + if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop + } + else { + Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop + } +} +catch { throw } + +# Start the Pode server +Start-PodeServer { + # Define an HTTP endpoint for the server + Add-PodeEndpoint -Address 'localhost' -Protocol 'Http' -Port '80' + + # Enable OpenAPI documentation and viewers + Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -DisableMinimalDefinitions -NoDefaultResponses + Add-PodeOAInfo -Title 'Custom Authentication Error Handling' -Version 1.0.0 + Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' + Enable-PodeOAViewer -Bookmarks -Path '/docs' + Add-PodeOAServerEndpoint -Url '/api/v3' -Description 'Default API endpoint' + + # Configure custom API key authentication + New-PodeAuthScheme -ApiKey | Add-PodeAuth -Name 'APIKey' -Sessionless -ScriptBlock { + param($key) + + # Handle missing API key + if (!$key) { + return @{ Success = $false; Reason = 'No X-API-KEY Header found' } + } + + # Validate API key + if ($key -eq 'test_user') { + return @{ Success = $true; User = 'test_user'; UserId = 1 } + } + + # Return failure for invalid users + return @{ Success = $false; User = $key; UserId = -1; Reason = 'Not existing user' } + } + + # Define an API route with manual authentication error handling + Add-PodeRoute -PassThru -Method 'Get' -Path '/api/v3/' -Authentication 'APIKey' -NoMiddlewareAuthentication -ScriptBlock { + # Manually invoke authentication + $auth = Invoke-PodeAuth -Name 'APIKey' + + # Log authentication details for debugging + Write-PodeHost $auth -Explode + + # If authentication succeeds, return user details + if ($auth.Success) { + Write-PodeJsonResponse -StatusCode 200 -Value @{ + Success = $true + Username = $auth.User + UserId = $auth.UserId + } + } + else { + # Handle authentication failures with a custom error response + Write-PodeJsonResponse -StatusCode 401 -Value @{ + Success = $false + Message = $auth.Reason + Username = $auth.User + } + } + } | Set-PodeOARouteInfo -Summary 'Who am I' -Tags 'auth' -OperationId 'whoami' -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = (New-PodeOABoolProperty -Name 'Success' -Default $true | New-PodeOAStringProperty -Name 'Username' | New-PodeOAIntProperty -Name 'UserId' | New-PodeOAObjectProperty ) } -PassThru | + Add-PodeOAResponse -StatusCode 401 -Description 'Authentication failure' -Content @{ 'application/json' = (New-PodeOABoolProperty -Name 'Success' -Default $false | New-PodeOAStringProperty -Name 'Username' | New-PodeOAStringProperty -Name 'Message' | New-PodeOAObjectProperty ) } +} diff --git a/examples/customerror.ps1 b/examples/customerror.ps1 deleted file mode 100644 index 0cc891ae9..000000000 --- a/examples/customerror.ps1 +++ /dev/null @@ -1,73 +0,0 @@ -<# -.SYNOPSIS - A sample PowerShell script to set up a Pode server with a view engine and file monitoring. - -.DESCRIPTION - This script sets up a Pode server listening on port 8081, uses Pode's view engine for rendering - web pages, and configures the server to monitor file changes and restart automatically. - -.EXAMPLE - To run the sample: ./File-Monitoring.ps1 - - Invoke-RestMethod -Uri http://localhost:8081 -Method Get - -.LINK - https://github.com/Badgerati/Pode/blob/develop/examples/File-Monitoring.ps1 - -.NOTES - Author: Pode Team - License: MIT License -#> - -try { - # Determine the script path and Pode module path - $ScriptPath = (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) - $podePath = Split-Path -Parent -Path $ScriptPath - - # Import the Pode module from the source path if it exists, otherwise from installed modules - if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { - Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop - } - else { - Import-Module -Name 'Pode' -MaximumVersion 2.99 -ErrorAction Stop - } -} -catch { throw } - - -Start-PodeServer { - Add-PodeEndpoint -Address 'localhost' -Protocol 'Http' -Port '80' - Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -DisableMinimalDefinitions -NoDefaultResponses - Add-PodeOAInfo -Title 'test custom auth error' -Version 1.0.0 - Enable-PodeOAViewer -Type Swagger -Path '/docs/swagger' - Enable-PodeOAViewer -Bookmarks -Path '/docs' - Add-PodeOAServerEndpoint -url '/api/v3' -Description 'default endpoint' - New-PodeAuthScheme -ApiKey | Add-PodeAuth -Name 'APIKey' -Sessionless -ScriptBlock { - param($key) - if (!$key) { - return @{ success = $false; reason = 'No X-API-KEY Header found' } - } - if ($key -eq 'test_user') { - return @{ success = $true; User = 'test_user'; userId = 1 } - } - return @{ success = $false; User = $key; userId = -1 ;reason = 'Not existing user'} - } - - - - Add-PodeRoute -PassThru -Method 'Get' -Path '/api/v3/' -Authentication 'APIKey' -NoMiddlewareAuthentication -ScriptBlock { - $auth = Invoke-PodeAuth -Name 'APIKey' - - write-podehost $auth -Explode - if ($auth.Success ) { - Write-PodeJsonResponse -Value @{ - Username = $auth.User - } - - } - else { - Write-PodeJsonResponse -Value @{ message = $auth.reason ; user = $auth.User } -StatusCode 401 - } - - } | Set-PodeOARouteInfo -Summary 'Who am I' -Tags 'auth' -OperationId 'whoami' -} \ No newline at end of file diff --git a/src/Locales/ar/Pode.psd1 b/src/Locales/ar/Pode.psd1 index 1c7ff7daf..44b3b4243 100644 --- a/src/Locales/ar/Pode.psd1 +++ b/src/Locales/ar/Pode.psd1 @@ -292,4 +292,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "الدالة '{0}' لا تقبل مصفوفة كمدخل لأنبوب البيانات." unsupportedStreamCompressionEncodingExceptionMessage = 'تشفير الضغط غير مدعوم للتشفير {0}' LocalEndpointConflictExceptionMessage = "تم تعريف كل من '{0}' و '{1}' كنقاط نهاية محلية لـ OpenAPI، لكن يُسمح فقط بنقطة نهاية محلية واحدة لكل تعريف API." + authMethodDoesNotExistExceptionMessage = 'طريقة المصادقة غير موجودة: {0}' + authenticationMethodMergedExceptionMessage = 'تم دمج طريقة المصادقة {0}' + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "المعلمة '-NoMiddlewareAuthentication' يمكن استخدامها فقط عند تحديد '-Authentication'." } diff --git a/src/Locales/de/Pode.psd1 b/src/Locales/de/Pode.psd1 index c90847e01..7a893b8cf 100644 --- a/src/Locales/de/Pode.psd1 +++ b/src/Locales/de/Pode.psd1 @@ -292,4 +292,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Die Funktion '{0}' akzeptiert kein Array als Pipeline-Eingabe." unsupportedStreamCompressionEncodingExceptionMessage = 'Die Stream-Komprimierungskodierung wird nicht unterstützt: {0}' LocalEndpointConflictExceptionMessage = "Sowohl '{0}' als auch '{1}' sind als lokale OpenAPI-Endpunkte definiert, aber es ist nur ein lokaler Endpunkt pro API-Definition erlaubt." + authMethodDoesNotExistExceptionMessage = 'Authentifizierungsmethode existiert nicht: {0}' + authenticationMethodMergedExceptionMessage = 'Authentifizierungsmethode {0} wurde zusammengeführt' + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "Der Parameter '-NoMiddlewareAuthentication' kann nur verwendet werden, wenn '-Authentication' angegeben ist." } \ No newline at end of file diff --git a/src/Locales/en/Pode.psd1 b/src/Locales/en/Pode.psd1 index 44a1ee102..cf8cfd1da 100644 --- a/src/Locales/en/Pode.psd1 +++ b/src/Locales/en/Pode.psd1 @@ -292,4 +292,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "The function '{0}' does not accept an array as pipeline input." unsupportedStreamCompressionEncodingExceptionMessage = 'Unsupported stream compression encoding: {0}' LocalEndpointConflictExceptionMessage = "Both '{0}' and '{1}' are defined as local OpenAPI endpoints, but only one local endpoint is allowed per API definition." + authMethodDoesNotExistExceptionMessage = 'The authentication method does not exist: {0}' + authenticationMethodMergedExceptionMessage = 'The authentication method {0} has been merged' + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "The parameter '-NoMiddlewareAuthentication' can only be used when '-Authentication' is specified." } \ No newline at end of file diff --git a/src/Locales/es/Pode.psd1 b/src/Locales/es/Pode.psd1 index 9c2ee1194..d73221687 100644 --- a/src/Locales/es/Pode.psd1 +++ b/src/Locales/es/Pode.psd1 @@ -292,4 +292,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La función '{0}' no acepta una matriz como entrada de canalización." unsupportedStreamCompressionEncodingExceptionMessage = 'La codificación de compresión de transmisión no es compatible: {0}' LocalEndpointConflictExceptionMessage = "Tanto '{0}' como '{1}' están definidos como puntos finales locales de OpenAPI, pero solo se permite un punto final local por definición de API." + authMethodDoesNotExistExceptionMessage = 'El método de autenticación no existe: {0}' + authenticationMethodMergedExceptionMessage = 'El método de autenticación {0} ha sido fusionado' + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "El parámetro '-NoMiddlewareAuthentication' solo se puede usar cuando se especifica '-Authentication'." } \ No newline at end of file diff --git a/src/Locales/fr/Pode.psd1 b/src/Locales/fr/Pode.psd1 index 2c9dfc579..36d7cadf9 100644 --- a/src/Locales/fr/Pode.psd1 +++ b/src/Locales/fr/Pode.psd1 @@ -292,4 +292,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La fonction '{0}' n'accepte pas un tableau en tant qu'entrée de pipeline." unsupportedStreamCompressionEncodingExceptionMessage = "La compression de flux {0} n'est pas prise en charge." LocalEndpointConflictExceptionMessage = "Les deux '{0}' et '{1}' sont définis comme des points de terminaison locaux pour OpenAPI, mais un seul point de terminaison local est autorisé par définition d'API." + authMethodDoesNotExistExceptionMessage = "La méthode d'authentification n'existe pas : {0}" + authenticationMethodMergedExceptionMessage = "La méthode d'authentification {0} est fusionnée" + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "Le paramètre '-NoMiddlewareAuthentication' ne peut être utilisé que lorsque '-Authentication' est spécifié." } \ No newline at end of file diff --git a/src/Locales/it/Pode.psd1 b/src/Locales/it/Pode.psd1 index 999bf85c3..bdcb8805e 100644 --- a/src/Locales/it/Pode.psd1 +++ b/src/Locales/it/Pode.psd1 @@ -292,4 +292,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "La funzione '{0}' non accetta una matrice come input della pipeline." unsupportedStreamCompressionEncodingExceptionMessage = 'La compressione dello stream non è supportata per la codifica {0}' LocalEndpointConflictExceptionMessage = "Sia '{0}' che '{1}' sono definiti come endpoint locali OpenAPI, ma è consentito solo un endpoint locale per definizione API." + authMethodDoesNotExistExceptionMessage = 'Il metodo di autenticazione non esiste: {0}' + authenticationMethodMergedExceptionMessage = 'Il metodo di autenticazione {0} è stato unito' + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "Il parametro '-NoMiddlewareAuthentication' può essere utilizzato solo se '-Authentication' è specificato." } \ No newline at end of file diff --git a/src/Locales/ja/Pode.psd1 b/src/Locales/ja/Pode.psd1 index e65627c59..a3c7a9093 100644 --- a/src/Locales/ja/Pode.psd1 +++ b/src/Locales/ja/Pode.psd1 @@ -292,4 +292,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "関数 '{0}' は配列をパイプライン入力として受け付けません。" unsupportedStreamCompressionEncodingExceptionMessage = 'サポートされていないストリーム圧縮エンコーディングが提供されました: {0}' LocalEndpointConflictExceptionMessage = "'{0}' と '{1}' は OpenAPI のローカルエンドポイントとして定義されていますが、API 定義ごとに 1 つのローカルエンドポイントのみ許可されます。" + authMethodDoesNotExistExceptionMessage = '認証方法が存在しません: {0}' + authenticationMethodMergedExceptionMessage = '認証方法 {0} が統合されました' + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "パラメーター '-NoMiddlewareAuthentication' は、'-Authentication' が指定されている場合にのみ使用できます。" } \ No newline at end of file diff --git a/src/Locales/ko/Pode.psd1 b/src/Locales/ko/Pode.psd1 index f64f0c61f..6747d5bf4 100644 --- a/src/Locales/ko/Pode.psd1 +++ b/src/Locales/ko/Pode.psd1 @@ -292,4 +292,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "함수 '{0}'은(는) 배열을 파이프라인 입력으로 받지 않습니다." unsupportedStreamCompressionEncodingExceptionMessage = '지원되지 않는 스트림 압축 인코딩: {0}' LocalEndpointConflictExceptionMessage = "'{0}' 와 '{1}' 는 OpenAPI 로컬 엔드포인트로 정의되었지만, API 정의당 하나의 로컬 엔드포인트만 허용됩니다." + authMethodDoesNotExistExceptionMessage = '인증 방법이 존재하지 않습니다: {0}' + authenticationMethodMergedExceptionMessage = '인증 방법 {0}이(가) 병합되었습니다' + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "매개변수 '-NoMiddlewareAuthentication'은 '-Authentication'이 지정된 경우에만 사용할 수 있습니다." } \ No newline at end of file diff --git a/src/Locales/nl/Pode.psd1 b/src/Locales/nl/Pode.psd1 index d7933a0d9..be574fa11 100644 --- a/src/Locales/nl/Pode.psd1 +++ b/src/Locales/nl/Pode.psd1 @@ -292,4 +292,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "De functie '{0}' accepteert geen array als pipeline-invoer." unsupportedStreamCompressionEncodingExceptionMessage = 'Niet-ondersteunde streamcompressie-encodering: {0}' LocalEndpointConflictExceptionMessage = "Zowel '{0}' als '{1}' zijn gedefinieerd als lokale OpenAPI-eindpunten, maar er is slechts één lokaal eindpunt per API-definitie toegestaan." + authMethodDoesNotExistExceptionMessage = 'Authenticatiemethode bestaat niet: {0}' + authenticationMethodMergedExceptionMessage = 'Authenticatiemethode {0} is samengevoegd' + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "De parameter '-NoMiddlewareAuthentication' kan alleen worden gebruikt als '-Authentication' is opgegeven." } \ No newline at end of file diff --git a/src/Locales/pl/Pode.psd1 b/src/Locales/pl/Pode.psd1 index cd632c469..683a7f92e 100644 --- a/src/Locales/pl/Pode.psd1 +++ b/src/Locales/pl/Pode.psd1 @@ -292,4 +292,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "Funkcja '{0}' nie akceptuje tablicy jako wejścia potoku." unsupportedStreamCompressionEncodingExceptionMessage = 'Kodowanie kompresji strumienia nie jest obsługiwane: {0}' LocalEndpointConflictExceptionMessage = "Zarówno '{0}', jak i '{1}' są zdefiniowane jako lokalne punkty końcowe OpenAPI, ale na jedną definicję API dozwolony jest tylko jeden lokalny punkt końcowy." + authMethodDoesNotExistExceptionMessage = 'Metoda uwierzytelniania nie istnieje: {0}' + authenticationMethodMergedExceptionMessage = 'Metoda uwierzytelniania {0} została scalona' + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "Parametr '-NoMiddlewareAuthentication' można używać tylko, gdy określono '-Authentication'." } \ No newline at end of file diff --git a/src/Locales/pt/Pode.psd1 b/src/Locales/pt/Pode.psd1 index a0604c179..c3dfc0757 100644 --- a/src/Locales/pt/Pode.psd1 +++ b/src/Locales/pt/Pode.psd1 @@ -292,4 +292,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "A função '{0}' não aceita uma matriz como entrada de pipeline." unsupportedStreamCompressionEncodingExceptionMessage = 'A codificação de compressão de fluxo não é suportada.' LocalEndpointConflictExceptionMessage = "Tanto '{0}' quanto '{1}' estão definidos como endpoints locais do OpenAPI, mas apenas um endpoint local é permitido por definição de API." + authMethodDoesNotExistExceptionMessage = 'O método de autenticação não existe: {0}' + authenticationMethodMergedExceptionMessage = 'O método de autenticação {0} foi mesclado' + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "O parâmetro '-NoMiddlewareAuthentication' só pode ser usado quando '-Authentication' for especificado." } \ No newline at end of file diff --git a/src/Locales/zh/Pode.psd1 b/src/Locales/zh/Pode.psd1 index 26b013c95..016aebe2f 100644 --- a/src/Locales/zh/Pode.psd1 +++ b/src/Locales/zh/Pode.psd1 @@ -292,4 +292,7 @@ fnDoesNotAcceptArrayAsPipelineInputExceptionMessage = "函数 '{0}' 不接受数组作为管道输入。" unsupportedStreamCompressionEncodingExceptionMessage = '不支持的流压缩编码: {0}' LocalEndpointConflictExceptionMessage = "'{0}' 和 '{1}' 都被定义为 OpenAPI 的本地端点,但每个 API 定义仅允许一个本地端点。" + authMethodDoesNotExistExceptionMessage = '身份验证方法不存在: {0}' + authenticationMethodMergedExceptionMessage = '身份验证方法 {0} 已合并' + parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage = "参数 '-NoMiddlewareAuthentication' 只能在指定 '-Authentication' 时使用。" } \ No newline at end of file diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index 54dc29096..df45a36e9 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -1197,6 +1197,27 @@ function Invoke-PodeAuthValidation { return $result } +<# +.SYNOPSIS + Tests the authentication validation for a specified authentication method. + +.DESCRIPTION + The `Test-PodeAuthValidation` function processes an authentication method by its name, + running the associated scripts, middleware, and validations to determine authentication success or failure. + +.PARAMETER Name + The name of the authentication method to validate. This parameter is mandatory. + +.PARAMETER RouteScript + A switch to indicate whether the function is being called from a route script, affecting response formatting. + +.OUTPUTS + A hashtable containing the authentication validation result, including success status, user details, + headers, and redirection information if applicable. + +.NOTES + This is an internal function and is subject to change in future versions of Pode. +#> function Test-PodeAuthValidation { param( [Parameter(Mandatory = $true)] @@ -1208,14 +1229,13 @@ function Test-PodeAuthValidation { ) try { - - # get auth method + # Retrieve authentication method configuration from Pode context $auth = $PodeContext.Server.Authentications.Methods[$Name] - # auth result + # Initialize authentication result variable $result = $null - # run pre-auth middleware + # Run pre-authentication middleware if defined if ($null -ne $auth.Scheme.Middleware) { if (!(Invoke-PodeMiddleware -Middleware $auth.Scheme.Middleware)) { return @{ @@ -1224,28 +1244,28 @@ function Test-PodeAuthValidation { } } - Wait-Debugger - - # run auth scheme script to parse request for data + # Prepare arguments for the authentication scheme script $_args = @(Merge-PodeScriptblockArguments -ArgumentList $auth.Scheme.Arguments -UsingVariables $auth.Scheme.ScriptBlock.UsingVariables) - # call inner schemes first + # Handle inner authentication schemes (if any) if ($null -ne $auth.Scheme.InnerScheme) { $schemes = @() - $_scheme = $auth.Scheme + + # Traverse through the inner schemes to collect them $_inner = @(while ($null -ne $_scheme.InnerScheme) { $_scheme = $_scheme.InnerScheme $_scheme }) + # Process inner schemes in reverse order for ($i = $_inner.Length - 1; $i -ge 0; $i--) { $_tmp_args = @(Merge-PodeScriptblockArguments -ArgumentList $_inner[$i].Arguments -UsingVariables $_inner[$i].ScriptBlock.UsingVariables) - $_tmp_args += , $schemes + $result = (Invoke-PodeScriptBlock -ScriptBlock $_inner[$i].ScriptBlock.Script -Arguments $_tmp_args -Return -Splat) if ($result -is [hashtable]) { - break + break # Exit if a valid result is returned } $schemes += , $result @@ -1255,25 +1275,27 @@ function Test-PodeAuthValidation { $_args += , $schemes } - if ($null -eq $result -and !$RouteScript ) { + # Execute the primary authentication script if no result from inner schemes and not a route script + if ($null -eq $result -and !$RouteScript) { $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 authentication script returns a non-hashtable, perform further validation if ($result -isnot [hashtable]) { $original = $result - $_args = @($result) + @($auth.Arguments) + + # Run main authentication validation script $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.ScriptBlock -Arguments $_args -UsingVariables $auth.UsingVariables -Return -Splat) - # if we have user, then run post validator if present + # Run post-authentication validation if applicable if ([string]::IsNullOrEmpty($result.Code) -and ($null -ne $auth.Scheme.PostValidator.Script)) { $_args = @($original) + @($result) + @($auth.Scheme.Arguments) $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.PostValidator.Script -Arguments $_args -UsingVariables $auth.Scheme.PostValidator.UsingVariables -Return -Splat) } } - # is the auth trying to redirect ie: oauth? + # Handle authentication redirection scenarios (e.g., OAuth) if ($result.IsRedirected) { return @{ Success = $false @@ -1281,11 +1303,12 @@ function Test-PodeAuthValidation { } } + # Handle results when invoked from a route script if ($RouteScript -and ($null -ne $result) -and ($result -is [hashtable])) { $ret = @{ Success = $true - User = '' - Headers = '' + User = '' + Headers = '' } foreach ($key in $result.Keys) { $ret[$key] = $result[$key] # Overwrites if key exists @@ -1293,11 +1316,11 @@ function Test-PodeAuthValidation { return $ret } - # if there's no result, or no user, then the auth failed - but allow auth if anon enabled + # Authentication failure handling if (($null -eq $result) -or ($result.Count -eq 0) -or (Test-PodeIsEmpty $result.User)) { $code = (Protect-PodeValue -Value $result.Code -Default 401) - # set the www-auth header + # Set WWW-Authenticate header for appropriate HTTP response $validCode = (($code -eq 401) -or ![string]::IsNullOrEmpty($result.Challenge)) if ($validCode) { @@ -1309,6 +1332,7 @@ function Test-PodeAuthValidation { $result.Headers = @{} } + # Generate authentication challenge header if (![string]::IsNullOrWhiteSpace($auth.Scheme.Name) -and !$result.Headers.ContainsKey('WWW-Authenticate')) { $authHeader = Get-PodeAuthWwwHeaderValue -Name $auth.Scheme.Name -Realm $auth.Scheme.Realm -Challenge $result.Challenge $result.Headers['WWW-Authenticate'] = $authHeader @@ -1324,7 +1348,7 @@ function Test-PodeAuthValidation { } } - # authentication was successful + # Authentication succeeded, return user and headers return @{ Success = $true User = $result.User @@ -1333,6 +1357,8 @@ function Test-PodeAuthValidation { } catch { $_ | Write-PodeErrorLog + + # Handle unexpected errors and log them return @{ Success = $false StatusCode = 500 @@ -1341,6 +1367,7 @@ function Test-PodeAuthValidation { } } + function Get-PodeAuthMiddlewareScript { return { param($opts) diff --git a/src/Public/Authentication.ps1 b/src/Public/Authentication.ps1 index 246682d89..14cd6eace 100644 --- a/src/Public/Authentication.ps1 +++ b/src/Public/Authentication.ps1 @@ -1201,19 +1201,18 @@ function Invoke-PodeAuth { $Name ) - try { - # Check if the authentication method exists - if (! (Test-PodeAuthExists -Name $Name)) { - # Authentication method doesn't exist: - throw ($PodeLocale.authMethodDoesNotExistExceptionMessage -f $Name) - } - - # Ensure the authentication method is not merged - if ($PodeContext.Server.Authentications.Methods[$Name].Merged) { - # Authentication method {0} is merged - throw ($PodeLocale.authenticationMethodMergedExceptionMessage -f $Name) - } + # Check if the authentication method exists + if (! (Test-PodeAuthExists -Name $Name)) { + # Authentication method doesn't exist: + throw ($PodeLocale.authMethodDoesNotExistExceptionMessage -f $Name) + } + # Ensure the authentication method is not merged + if ($PodeContext.Server.Authentications.Methods[$Name].Merged) { + # Authentication method {0} is merged + throw ($PodeLocale.authenticationMethodMergedExceptionMessage -f $Name) + } + try { # Perform authentication validation $result = Test-PodeAuthValidation -Name $Name -RouteScript } diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 9149b8f17..61bc66652 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -1,113 +1,117 @@ <# .SYNOPSIS -Adds a Route for a specific HTTP Method(s). + Adds a Route for a specific HTTP Method(s). .DESCRIPTION -Adds a Route for a specific HTTP Method(s), with path, that when called with invoke any logic and/or Middleware. + Adds a Route for a specific HTTP Method(s) with a given path that, when called, will invoke any specified logic and/or middleware. .PARAMETER Method -The HTTP Method of this Route, multiple can be supplied. + The HTTP Method(s) of this Route. Multiple methods can be supplied. .PARAMETER Path -The URI path for the Route. + The URI path for the Route. .PARAMETER Middleware -An array of ScriptBlocks for optional Middleware. + An array of ScriptBlocks for optional Middleware to be executed before the main route logic. .PARAMETER ScriptBlock -A ScriptBlock for the Route's main logic. + A ScriptBlock containing the main logic for the Route. + +.PARAMETER FilePath + A literal or relative path to a file containing a ScriptBlock for the Route's main logic. + +.PARAMETER ArgumentList + An array of arguments to supply to the Route's ScriptBlock. .PARAMETER EndpointName -The EndpointName of an Endpoint(s) this Route should be bound against. + The EndpointName(s) this Route should be bound against. .PARAMETER ContentType -The content type the Route should use when parsing any payloads. + The content type the Route should use when parsing incoming payloads. .PARAMETER TransferEncoding -The transfer encoding the Route should use when parsing any payloads. + The transfer encoding the Route should use when processing incoming payloads. + Acceptable values: '', 'gzip', 'deflate'. .PARAMETER ErrorContentType -The content type of any error pages that may get returned. - -.PARAMETER FilePath -A literal, or relative, path to a file containing a ScriptBlock for the Route's main logic. - -.PARAMETER ArgumentList -An array of arguments to supply to the Route's ScriptBlock. + The content type of any error pages that may be returned by this Route. .PARAMETER Authentication -The name of an Authentication method which should be used as middleware on this Route. + The name of an Authentication method to be used as middleware on this Route. + +.PARAMETER NoMiddlewareAuthentication + If specified, disables automatic authentication middleware attachment when an authentication method is provided. .PARAMETER Access -The name of an Access method which should be used as middleware on this Route. + The name of an Access method to be used as middleware on this Route. -.PARAMETER AllowAnon -If supplied, the Route will allow anonymous access for non-authenticated users. +.PARAMETER Role + One or more optional Roles that are authorised to access this Route when using Authentication with an Access method. -.PARAMETER Login -If supplied, the Route will be flagged to Authentication as being a Route that handles user logins. +.PARAMETER Group + One or more optional Groups that are authorised to access this Route when using Authentication with an Access method. -.PARAMETER Logout -If supplied, the Route will be flagged to Authentication as being a Route that handles users logging out. +.PARAMETER Scope + One or more optional Scopes that are authorised to access this Route when using Authentication with an Access method. -.PARAMETER PassThru -If supplied, the route created will be returned so it can be passed through a pipe. +.PARAMETER User + One or more optional Users that are authorised to access this Route when using Authentication with an Access method. -.PARAMETER IfExists -Specifies what action to take when a Route already exists. (Default: Default) +.PARAMETER AllowAnon + If specified, the Route will allow anonymous access for non-authenticated users. -.PARAMETER Role -One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method. +.PARAMETER Login + If specified, flags the Route to Authentication as handling user logins. -.PARAMETER Group -One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method. +.PARAMETER Logout + If specified, flags the Route to Authentication as handling user logouts. -.PARAMETER Scope -One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method. +.PARAMETER IfExists + Specifies the action to take when a Route already exists. + Options: 'Default', 'Error', 'Overwrite', 'Skip'. (Default: Default) -.PARAMETER User -One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method. +.PARAMETER PassThru + If specified, the created Route will be returned, allowing it to be passed through a pipeline. .PARAMETER OAResponses -An alternative way to associate OpenApi responses unsing New-PodeOAResponse instead of piping multiple Add-PodeOAResponse + Allows associating OpenAPI responses using `New-PodeOAResponse` instead of piping multiple `Add-PodeOAResponse` calls. .PARAMETER OAReference -A reference to OpenAPI reusable pathItem component created with Add-PodeOAComponentPathItem + A reference to an OpenAPI reusable pathItem component created using `Add-PodeOAComponentPathItem`. .PARAMETER OADefinitionTag -An Array of strings representing the unique tag for the API specification. -This tag helps in distinguishing between different versions or types of API specifications within the application. -You can use this tag to reference the specific API documentation, schema, or version that your function interacts with. + An array of strings representing unique tags for the API specification. + This helps distinguish between different versions or types of API specifications within the application. .EXAMPLE -Add-PodeRoute -Method Get -Path '/' -ScriptBlock { /* logic */ } + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { /* logic */ } .EXAMPLE -Add-PodeRoute -Method Post -Path '/users/:userId/message' -Middleware (Get-PodeCsrfMiddleware) -ScriptBlock { /* logic */ } + Add-PodeRoute -Method Post -Path '/users/:userId/message' -Middleware (Get-PodeCsrfMiddleware) -ScriptBlock { /* logic */ } .EXAMPLE -Add-PodeRoute -Method Post -Path '/user' -ContentType 'application/json' -ScriptBlock { /* logic */ } + Add-PodeRoute -Method Post -Path '/user' -ContentType 'application/json' -ScriptBlock { /* logic */ } .EXAMPLE -Add-PodeRoute -Method Post -Path '/user' -ContentType 'application/json' -TransferEncoding gzip -ScriptBlock { /* logic */ } + Add-PodeRoute -Method Post -Path '/user' -ContentType 'application/json' -TransferEncoding gzip -ScriptBlock { /* logic */ } .EXAMPLE -Add-PodeRoute -Method Get -Path '/api/cpu' -ErrorContentType 'application/json' -ScriptBlock { /* logic */ } + Add-PodeRoute -Method Get -Path '/api/cpu' -ErrorContentType 'application/json' -ScriptBlock { /* logic */ } .EXAMPLE -Add-PodeRoute -Method Get -Path '/' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2' + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { /* logic */ } -ArgumentList 'arg1', 'arg2' .EXAMPLE -Add-PodeRoute -Method Get -Path '/' -Role 'Developer', 'QA' -ScriptBlock { /* logic */ } + Add-PodeRoute -Method Get -Path '/' -Role 'Developer', 'QA' -ScriptBlock { /* logic */ } .EXAMPLE -$Responses = New-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' | - New-PodeOAResponse -StatusCode 404 -Description 'User not found' | - New-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' + $Responses = New-PodeOAResponse -StatusCode 400 -Description 'Invalid username supplied' | + New-PodeOAResponse -StatusCode 404 -Description 'User not found' | + New-PodeOAResponse -StatusCode 405 -Description 'Invalid Input' -Add-PodeRoute -PassThru -Method Put -Path '/user/:username' -OAResponses $Responses -ScriptBlock { - #code is going here - } + Add-PodeRoute -PassThru -Method Put -Path '/user/:username' -OAResponses $Responses -ScriptBlock { + # Logic here + } #> function Add-PodeRoute { [CmdletBinding(DefaultParameterSetName = 'Script')] diff --git a/tests/unit/Authentication.Tests.ps1 b/tests/unit/Authentication.Tests.ps1 index 38190f523..a3ac4e7ff 100644 --- a/tests/unit/Authentication.Tests.ps1 +++ b/tests/unit/Authentication.Tests.ps1 @@ -191,3 +191,47 @@ Describe 'Expand-PodeAuthMerge Tests' { } } +# Pester test for Invoke-PodeAuth function + +Describe 'Invoke-PodeAuth Tests' { + BeforeEach { + # Mock the Pode context and localization messages + $PodeContext = @{ + Server = @{ + Authentications = @{ + Methods = @{ + 'ValidAuth' = @{ Merged = $false } + 'MergedAuth' = @{ Merged = $true } + } + } + } + } + + $PodeLocale = @{ + authMethodDoesNotExistExceptionMessage = "Authentication method {0} does not exist" + authenticationMethodMergedExceptionMessage = "Authentication method {0} is merged" + } + } + + It 'Should successfully invoke a valid authentication method' { + Mock Test-PodeAuthExists { $true } -ParameterFilter { $Name -eq 'ValidAuth' } + Mock Test-PodeAuthValidation { @{ Success = $true; User = 'TestUser'; Headers = @{} } } + + $result = Invoke-PodeAuth -Name 'ValidAuth' + + $result | Should -Not -Be $null + $result.Success | Should -Be $true + $result.User | Should -Be 'TestUser' + } + + It 'Should throw an error when authentication method does not exist' { + { Invoke-PodeAuth -Name 'InvalidAuth' } | Should -Throw ($PodeLocale.authMethodDoesNotExistExceptionMessage -f 'InvalidAuth' ) + } + + It 'Should throw an error when authentication method is merged' { + + { Invoke-PodeAuth -Name 'MergedAuth' } | Should -Throw ($PodeLocale.authenticationMethodMergedExceptionMessage -f 'MergedAuth' ) + } + + +} From 886913c206fd552cf632647c7774a337b4166cff Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 21 Jan 2025 18:59:47 -0800 Subject: [PATCH 05/13] fix test --- src/Public/Routes.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 61bc66652..00cacd15d 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -363,7 +363,7 @@ function Add-PodeRoute { } } elseif ($NoMiddlewareAuthentication) { - throw "Parameter '-NoMiddlewareAuthentication' can be only used with '-Authentication'" + throw $PodeLocale.parameterNoMiddlewareAuthRequiresAuthenticationExceptionMessage } # custom access From d95fbcba381fa2c488dcbd3f7ceacde18d57136d Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 22 Jan 2025 06:02:26 -0800 Subject: [PATCH 06/13] Fix No X-API-KEY Header found issue --- src/Private/Authentication.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index df45a36e9..520057194 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -1276,7 +1276,7 @@ function Test-PodeAuthValidation { } # Execute the primary authentication script if no result from inner schemes and not a route script - if ($null -eq $result -and !$RouteScript) { + if ($null -eq $result) { $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.ScriptBlock.Script -Arguments $_args -Return -Splat) } From d0c55f105325367fe787b442a22f191d9233b883 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 22 Jan 2025 06:55:34 -0800 Subject: [PATCH 07/13] adding documentation --- docs/Tutorials/Authentication/Overview.md | 66 +++++++++++++++++++ ...ng.ps1 => Web-AuthManualErrorHandling.ps1} | 4 +- src/Private/Authentication.ps1 | 19 +++--- src/Public/Authentication.ps1 | 4 +- 4 files changed, 81 insertions(+), 12 deletions(-) rename examples/{ManualAuthErrorHandling.ps1 => Web-AuthManualErrorHandling.ps1} (96%) diff --git a/docs/Tutorials/Authentication/Overview.md b/docs/Tutorials/Authentication/Overview.md index 19ddacece..32dfbd738 100644 --- a/docs/Tutorials/Authentication/Overview.md +++ b/docs/Tutorials/Authentication/Overview.md @@ -388,3 +388,69 @@ Start-PodeServer { New-PodeAuthScheme -Basic | Add-PodeAuthWindowsAd -Name 'Login' } ``` + +## Handling Authentication Manually + +Sometimes, the standard Pode authentication approach may not provide the flexibility needed to accommodate all use cases. For example, specific requirements such as customizing an OpenAPI definition for an unsuccessful authentication might necessitate a more tailored solution. This approach offers greater control over authentication processing within routes. + +### Invoke-PodeAuth + +The `Invoke-PodeAuth` function allows for direct invocation of authentication methods and returns the result from `Add-PodeAuth`. This function offers a streamlined approach for manually handling authentication within routes. + +#### Usage Example + +```powershell +New-PodeAuthScheme -ApiKey | Add-PodeAuth -Name 'APIKey' -Sessionless -ScriptBlock { + param($key) + + # Handle missing API key + if (!$key) { + return @{ Success = $false; Reason = 'No X-API-KEY Header found' } + } + + # Validate API key + if ($key -eq 'test_user') { + return @{ Success = $true; User = 'test_user'; UserId = 1 } + } + + # Return failure for invalid users + return @{ Success = $false; User = $key; UserId = -1; Reason = 'Not existing user' } +} + +Add-PodeRoute -Method 'Get' -Path '/api/v3/' -Authentication 'APIKey' -NoMiddlewareAuthentication -ScriptBlock { + $auth = Invoke-PodeAuth -Name 'APIKey' + + if ($auth.Success) { + Write-PodeJsonResponse -Value @{ Username = $auth.User } + } else { + Write-PodeJsonResponse -Value @{ message = $auth.Reason; user = $auth.User } -StatusCode 401 + } +} +``` + +The `Auth` object, when managed this way, includes any value returned by the authentication scriptblock, such as `User`, `UserId`, and `Reason` fields as shown in the example. + +### NoMiddlewareAuthentication Parameter + +The `-NoMiddlewareAuthentication` parameter for `Add-PodeRoute` enables routes to bypass automatic authentication middleware processing. This allows developers to manually handle authentication within route script blocks. + +#### Example Usage + +```powershell +Add-PodeRoute -Method 'Get' -Path '/api/v3/' -Authentication 'APIKey' -NoMiddlewareAuthentication -ScriptBlock { + $auth = Invoke-PodeAuth -Name 'APIKey' + + if ($auth.Success) { + Write-PodeJsonResponse -Value @{ Username = $auth.User } + } else { + Write-PodeJsonResponse -Value @{ message = $auth.Reason; user = $auth.User } -StatusCode 401 + } +} +``` + +### Benefits + +- Allows developers to manually control authentication within route logic. +- Provides flexibility to bypass automatic authentication middleware. +- Ensures better customization for complex authentication scenarios. + diff --git a/examples/ManualAuthErrorHandling.ps1 b/examples/Web-AuthManualErrorHandling.ps1 similarity index 96% rename from examples/ManualAuthErrorHandling.ps1 rename to examples/Web-AuthManualErrorHandling.ps1 index 79f2e3b93..c3b70c207 100644 --- a/examples/ManualAuthErrorHandling.ps1 +++ b/examples/Web-AuthManualErrorHandling.ps1 @@ -10,14 +10,14 @@ .EXAMPLE To run the script: - ./ManualAuthErrorHandling.ps1 + ./Web-AuthManualErrorHandling.ps1 Test it using: Invoke-RestMethod -Uri http://localhost/api/v3/ -Headers @{ 'X-API-KEY' = 'test_user' } -Method Get .LINK - https://github.com/Badgerati/Pode/blob/develop/examples/ManualAuthErrorHandling.ps1 + https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthManualErrorHandling.ps1 .NOTES Author: Pode Team diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index 520057194..75cd6a6e5 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -1208,8 +1208,8 @@ function Invoke-PodeAuthValidation { .PARAMETER Name The name of the authentication method to validate. This parameter is mandatory. -.PARAMETER RouteScript - A switch to indicate whether the function is being called from a route script, affecting response formatting. +.PARAMETER NoMiddlewareAuthentication + A switch to indicate whether the function has to threat the authentication because no Middleware authentication has been executed. .OUTPUTS A hashtable containing the authentication validation result, including success status, user details, @@ -1225,7 +1225,7 @@ function Test-PodeAuthValidation { $Name, [switch] - $RouteScript + $NoMiddlewareAuthentication ) try { @@ -1304,11 +1304,15 @@ function Test-PodeAuthValidation { } # Handle results when invoked from a route script - if ($RouteScript -and ($null -ne $result) -and ($result -is [hashtable])) { + if ($NoMiddlewareAuthentication -and ($null -ne $result) -and ($result -is [hashtable])) { $ret = @{ - Success = $true - User = '' - Headers = '' + Success = $true + User = '' + Headers = '' + IsAuthenticated = $result.Success + IsAuthorised = $result.Success + Store = !$auth.Sessionless + Name = $Name } foreach ($key in $result.Keys) { $ret[$key] = $result[$key] # Overwrites if key exists @@ -1492,7 +1496,6 @@ function Test-PodeAuthInternal { Store = !$auth.Sessionless Name = $result.Auth } - # successful auth $authName = $null if ($auth.Merged -and !$auth.PassOne) { diff --git a/src/Public/Authentication.ps1 b/src/Public/Authentication.ps1 index 14cd6eace..8c748cb14 100644 --- a/src/Public/Authentication.ps1 +++ b/src/Public/Authentication.ps1 @@ -1214,13 +1214,13 @@ function Invoke-PodeAuth { } try { # Perform authentication validation - $result = Test-PodeAuthValidation -Name $Name -RouteScript + $WebEvent.Auth = Test-PodeAuthValidation -Name $Name -NoMiddlewareAuthentication } catch { $_ | Write-PodeErrorLog } - return $result + return $WebEvent.Auth } From ce6a0db5ade603f1d4b76a523d77bd15580c4716 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 22 Jan 2025 06:57:13 -0800 Subject: [PATCH 08/13] remove refuse --- src/Private/Helpers.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index c4b23ea9b..387a7ebe4 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -2237,8 +2237,7 @@ function Find-PodeErrorPage { [string] $ContentType ) -write-podehost $Code -write-podehost $ContentType + # if a defined content type is supplied, attempt to find an error page for that first if (![string]::IsNullOrWhiteSpace($ContentType)) { $path = Get-PodeErrorPage -Code $Code -ContentType $ContentType From 318a0f25a1aed8801a6567eb7d6db2015ac8b84f Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 22 Jan 2025 06:58:07 -0800 Subject: [PATCH 09/13] . --- src/Private/Helpers.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 387a7ebe4..8e4e9f37d 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -2237,7 +2237,7 @@ function Find-PodeErrorPage { [string] $ContentType ) - + # if a defined content type is supplied, attempt to find an error page for that first if (![string]::IsNullOrWhiteSpace($ContentType)) { $path = Get-PodeErrorPage -Code $Code -ContentType $ContentType From e5771323c1b92e691fd808eba0f23118506b12f2 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 22 Jan 2025 19:43:56 -0800 Subject: [PATCH 10/13] fix empty key --- examples/Web-AuthManualErrorHandling.ps1 | 5 ++--- src/Private/Authentication.ps1 | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/examples/Web-AuthManualErrorHandling.ps1 b/examples/Web-AuthManualErrorHandling.ps1 index c3b70c207..98b37b462 100644 --- a/examples/Web-AuthManualErrorHandling.ps1 +++ b/examples/Web-AuthManualErrorHandling.ps1 @@ -54,10 +54,9 @@ Start-PodeServer { # Configure custom API key authentication New-PodeAuthScheme -ApiKey | Add-PodeAuth -Name 'APIKey' -Sessionless -ScriptBlock { param($key) - # Handle missing API key if (!$key) { - return @{ Success = $false; Reason = 'No X-API-KEY Header found' } + return @{ Success = $false; Reason = 'No Authentication Header found' } } # Validate API key @@ -66,7 +65,7 @@ Start-PodeServer { } # Return failure for invalid users - return @{ Success = $false; User = $key; UserId = -1; Reason = 'Not existing user' } + return @{ Success = $false; User = $key; Reason = 'Not existing user' } } # Define an API route with manual authentication error handling diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index 75cd6a6e5..b01c4034a 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -1280,6 +1280,11 @@ function Test-PodeAuthValidation { $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.ScriptBlock.Script -Arguments $_args -Return -Splat) } + # Remove the Middleware processed data if code is 400 - no token + if ($NoMiddlewareAuthentication -and ($result.Code -eq 400)) { + $result = '' + } + # If authentication script returns a non-hashtable, perform further validation if ($result -isnot [hashtable]) { $original = $result @@ -1305,12 +1310,20 @@ function Test-PodeAuthValidation { # Handle results when invoked from a route script if ($NoMiddlewareAuthentication -and ($null -ne $result) -and ($result -is [hashtable])) { + if ($result.Success -is [bool]) { + $success = $result.Success + } + else { + $success = $false + [System.Exception]::new("The authentication Scriptblock must return an hashtable with a key named 'Success'") | Write-PodeErrorLog + } + $ret = @{ - Success = $true + Success = $success User = '' Headers = '' - IsAuthenticated = $result.Success - IsAuthorised = $result.Success + IsAuthenticated = $success + IsAuthorised = $success Store = !$auth.Sessionless Name = $Name } From fe397f8d055c0d8ebe209daff69ee703c49178b0 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Thu, 23 Jan 2025 10:16:19 -0800 Subject: [PATCH 11/13] digest issue --- examples/Web-AuthDigest.ps1 | 31 +- examples/Web-AuthManualErrorHandling.ps1 | 40 +- my modified authentication - Private.ps1 | 2544 ++++++++++++++++++++ my modified authentication - Public.ps1 | 2690 ++++++++++++++++++++++ tests/unit/Authentication.Tests.ps1 | 5 +- 5 files changed, 5303 insertions(+), 7 deletions(-) create mode 100644 my modified authentication - Private.ps1 create mode 100644 my modified authentication - Public.ps1 diff --git a/examples/Web-AuthDigest.ps1 b/examples/Web-AuthDigest.ps1 index 7c26eef53..2ea531958 100644 --- a/examples/Web-AuthDigest.ps1 +++ b/examples/Web-AuthDigest.ps1 @@ -9,7 +9,29 @@ .EXAMPLE To run the sample: ./Web-AuthDigest.ps1 - Invoke-RestMethod -Uri http://localhost:8081/users -Method Get + # Define the URI and credentials + $uri = [System.Uri]::new("http://localhost:8081/users") + $username = "morty" + $password = "pickle" + + # Create a credential cache and add Digest authentication + $credentialCache = [System.Net.CredentialCache]::new() + $networkCredential = [System.Net.NetworkCredential]::new($username, $password) + $credentialCache.Add($uri, "Digest", $networkCredential) + + # Create the HTTP client handler with the credential cache + $handler = [System.Net.Http.HttpClientHandler]::new() + $handler.Credentials = $credentialCache + + # Create the HTTP client + $httpClient = [System.Net.Http.HttpClient]::new($handler) + + # Send the GET request and capture the response + $response = $httpClient.GetStringAsync($uri).Result + + # Display the response + $response + .LINK https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthDigest.ps1 @@ -45,7 +67,7 @@ Start-PodeServer -Threads 2 { # setup digest auth New-PodeAuthScheme -Digest | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { param($username, $params) - +write-podehost "username=$username" # here you'd check a real user storage, this is just for example if ($username -ieq 'morty') { return @{ @@ -57,12 +79,13 @@ Start-PodeServer -Threads 2 { Password = 'pickle' } } - +write-podehost 'no auth' return $null } # GET request to get list of users (since there's no session, authentication will always happen) - Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock { + write-podehsot '1' Write-PodeJsonResponse -Value @{ Users = @( @{ diff --git a/examples/Web-AuthManualErrorHandling.ps1 b/examples/Web-AuthManualErrorHandling.ps1 index 98b37b462..efc76cdd1 100644 --- a/examples/Web-AuthManualErrorHandling.ps1 +++ b/examples/Web-AuthManualErrorHandling.ps1 @@ -68,8 +68,19 @@ Start-PodeServer { return @{ Success = $false; User = $key; Reason = 'Not existing user' } } + + New-PodeAuthScheme -ApiKey | Add-PodeAuth -Name 'APIKey_standard' -Sessionless -ScriptBlock { + param($key) + + # Validate API key + if ($key -eq 'test_user') { + return @{ Success = $true; User = 'test_user'; UserId = 1 } + } + + } + # Define an API route with manual authentication error handling - Add-PodeRoute -PassThru -Method 'Get' -Path '/api/v3/' -Authentication 'APIKey' -NoMiddlewareAuthentication -ScriptBlock { + Add-PodeRoute -PassThru -Method 'Get' -Path '/api/v3/whoami' -Authentication 'APIKey' -NoMiddlewareAuthentication -ScriptBlock { # Manually invoke authentication $auth = Invoke-PodeAuth -Name 'APIKey' @@ -95,4 +106,31 @@ Start-PodeServer { } | Set-PodeOARouteInfo -Summary 'Who am I' -Tags 'auth' -OperationId 'whoami' -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = (New-PodeOABoolProperty -Name 'Success' -Default $true | New-PodeOAStringProperty -Name 'Username' | New-PodeOAIntProperty -Name 'UserId' | New-PodeOAObjectProperty ) } -PassThru | Add-PodeOAResponse -StatusCode 401 -Description 'Authentication failure' -Content @{ 'application/json' = (New-PodeOABoolProperty -Name 'Success' -Default $false | New-PodeOAStringProperty -Name 'Username' | New-PodeOAStringProperty -Name 'Message' | New-PodeOAObjectProperty ) } + + Add-PodeRoute -PassThru -Method 'Get' -Path '/api/v3/whoami_standard' -Authentication 'APIKey_standard' -ErrorContentType 'application/json' -ScriptBlock { + # Manually invoke authentication + # $auth = Invoke-PodeAuth -Name 'APIKey' + + # Log authentication details for debugging + Write-PodeHost $auth -Explode + + # If authentication succeeds, return user details + if ($auth.Success) { + Write-PodeJsonResponse -StatusCode 200 -Value @{ + Success = $true + Username = $auth.User + UserId = $auth.UserId + } + } + else { + # Handle authentication failures with a custom error response + Write-PodeJsonResponse -StatusCode 401 -Value @{ + Success = $false + Message = $auth.Reason + Username = $auth.User + } + } + } | Set-PodeOARouteInfo -Summary 'Who am I (default auth)' -Tags 'auth' -OperationId 'whoami_standard' -PassThru | + Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = (New-PodeOABoolProperty -Name 'Success' -Default $true | New-PodeOAStringProperty -Name 'Username' | New-PodeOAIntProperty -Name 'UserId' | New-PodeOAObjectProperty ) } -PassThru | + Add-PodeOAResponse -StatusCode 401 -Description 'Authentication failure' -Content @{ 'application/json' = (New-PodeOABoolProperty -Name 'Success' -Default $false | New-PodeOAStringProperty -Name 'Username' | New-PodeOAStringProperty -Name 'Message' | New-PodeOAObjectProperty ) } } diff --git a/my modified authentication - Private.ps1 b/my modified authentication - Private.ps1 new file mode 100644 index 000000000..5979dc87d --- /dev/null +++ b/my modified authentication - Private.ps1 @@ -0,0 +1,2544 @@ +function Get-PodeAuthBasicType { + return { + param($options) + + # get the auth header + $header = (Get-PodeHeader -Name 'Authorization') + if ($null -eq $header) { + return @{ + Message = 'No Authorization header found' + Code = 401 + Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`"" } + } + } + + # ensure the first atom is basic (or opt override) + $atoms = $header -isplit '\s+' + if ($atoms.Length -lt 2) { + return @{ + Message = 'Invalid Authorization header' + Code = 400 + Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`"" } + } + } + + if ($atoms[0] -ine $options.HeaderTag) { + return @{ + Message = "Header is not for $($options.HeaderTag) Authorization" + Code = 400 + Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`"" } + } + } + + # decode the auth header + try { + $enc = [System.Text.Encoding]::GetEncoding($options.Encoding) + } + catch { + return @{ + Message = 'Invalid encoding specified for Authorization' + Code = 400 + Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`"" } + } + } + + try { + $decoded = $enc.GetString([System.Convert]::FromBase64String($atoms[1])) + } + catch { + return @{ + Message = 'Invalid Base64 string found in Authorization header' + Code = 400 + Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`"" } + } + } + + # validate and return user/result + $index = $decoded.IndexOf(':') + $username = $decoded.Substring(0, $index) + $password = $decoded.Substring($index + 1) + + # build the result + $result = @($username, $password) + + # convert to credential? + if ($options.AsCredential) { + $passSecure = ConvertTo-SecureString -String $password -AsPlainText -Force + $creds = [pscredential]::new($username, $passSecure) + $result = @($creds) + } + + # return data for calling validator + return $result + } +} + +function Get-PodeAuthOAuth2Type { + return { + param($options, $schemes) + + # set default scopes + if (($null -eq $options.Scopes) -or ($options.Scopes.Length -eq 0)) { + $options.Scopes = @('openid', 'profile', 'email') + } + + $scopes = ($options.Scopes -join ' ') + + # if there's an error, fail + if (![string]::IsNullOrWhiteSpace($WebEvent.Query['error'])) { + return @{ + Message = $WebEvent.Query['error'] + Code = 401 + IsErrored = $true + } + } + + # set grant type + $hasInnerScheme = (($null -ne $schemes) -and ($schemes.Length -gt 0)) + $grantType = 'authorization_code' + if ($hasInnerScheme) { + $grantType = 'password' + } + + # if there's a code query param, or inner scheme, get access token + if ($hasInnerScheme -or ![string]::IsNullOrWhiteSpace($WebEvent.Query['code'])) { + try { + # ensure the state is valid + if ((Test-PodeSessionsInUse) -and ($WebEvent.Query['state'] -ne $WebEvent.Session.Data['__pode_oauth_state__'])) { + return @{ + Message = 'OAuth2 state returned is invalid' + Code = 401 + IsErrored = $true + } + } + + # build tokenUrl query with client info + $body = "client_id=$($options.Client.ID)" + $body += "&grant_type=$($grantType)" + + if (![string]::IsNullOrEmpty($options.Client.Secret)) { + $body += "&client_secret=$([System.Web.HttpUtility]::UrlEncode($options.Client.Secret))" + } + + # add PKCE code verifier + if ($options.PKCE.Enabled) { + $body += "&code_verifier=$($WebEvent.Session.Data['__pode_oauth_code_verifier__'])" + } + + # if there's an inner scheme, get the username/password, and set query + if ($hasInnerScheme) { + $body += "&username=$($schemes[-1][0])" + $body += "&password=$($schemes[-1][1])" + $body += "&scope=$([System.Web.HttpUtility]::UrlEncode($scopes))" + } + + # otherwise, set query for auth_code + else { + $redirectUrl = Get-PodeOAuth2RedirectHost -RedirectUrl $options.Urls.Redirect + $body += "&code=$($WebEvent.Query['code'])" + $body += "&redirect_uri=$([System.Web.HttpUtility]::UrlEncode($redirectUrl))" + } + + # POST the tokenUrl + try { + $result = Invoke-RestMethod -Method Post -Uri $options.Urls.Token -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop + } + catch [System.Net.WebException], [System.Net.Http.HttpRequestException] { + $response = Read-PodeWebExceptionInfo -ErrorRecord $_ + $result = ($response.Body | ConvertFrom-Json) + } + + # was there an error? + if (![string]::IsNullOrWhiteSpace($result.error)) { + return @{ + Message = "$($result.error): $($result.error_description)" + Code = 401 + IsErrored = $true + } + } + + # get user details - if url supplied + if (![string]::IsNullOrWhiteSpace($options.Urls.User.Url)) { + try { + $user = Invoke-RestMethod -Method $options.Urls.User.Method -Uri $options.Urls.User.Url -Headers @{ Authorization = "Bearer $($result.access_token)" } + } + catch [System.Net.WebException], [System.Net.Http.HttpRequestException] { + $response = Read-PodeWebExceptionInfo -ErrorRecord $_ + $user = ($response.Body | ConvertFrom-Json) + } + + if (![string]::IsNullOrWhiteSpace($user.error)) { + return @{ + Message = "$($user.error): $($user.error_description)" + Code = 401 + IsErrored = $true + } + } + } + elseif (![string]::IsNullOrWhiteSpace($result.id_token)) { + try { + $user = ConvertFrom-PodeJwt -Token $result.id_token -IgnoreSignature + } + catch { + $user = @{ Provider = 'OAuth2' } + } + } + else { + $user = @{ Provider = 'OAuth2' } + } + + # return the user for the validator + return @($user, $result.access_token, $result.refresh_token, $result) + } + finally { + if ($null -ne $WebEvent.Session.Data) { + # clear state + $WebEvent.Session.Data.Remove('__pode_oauth_state__') + + # clear PKCE + if ($options.PKCE.Enabled) { + $WebEvent.Session.Data.Remove('__pode_oauth_code_verifier__') + } + } + } + } + + # redirect to the authUrl - only if no inner scheme supplied + if (!$hasInnerScheme) { + # get the redirectUrl + $redirectUrl = Get-PodeOAuth2RedirectHost -RedirectUrl $options.Urls.Redirect + + # add authUrl query params + $query = "client_id=$($options.Client.ID)" + $query += '&response_type=code' + $query += "&redirect_uri=$([System.Web.HttpUtility]::UrlEncode($redirectUrl))" + $query += '&response_mode=query' + $query += "&scope=$([System.Web.HttpUtility]::UrlEncode($scopes))" + + # add csrf state + if (Test-PodeSessionsInUse) { + $guid = New-PodeGuid + $WebEvent.Session.Data['__pode_oauth_state__'] = $guid + $query += "&state=$($guid)" + } + + # build a code verifier for PKCE, and add to query + if ($options.PKCE.Enabled) { + $guid = New-PodeGuid + $codeVerifier = "$($guid)-$($guid)" + $WebEvent.Session.Data['__pode_oauth_code_verifier__'] = $codeVerifier + + $codeChallenge = $codeVerifier + if ($options.PKCE.CodeChallenge.Method -ieq 'S256') { + $codeChallenge = ConvertTo-PodeBase64UrlValue -Value (Invoke-PodeSHA256Hash -Value $codeChallenge) -NoConvert + } + + $query += "&code_challenge=$($codeChallenge)" + $query += "&code_challenge_method=$($options.PKCE.CodeChallenge.Method)" + } + + # are custom parameters already on the URL? + $url = $options.Urls.Authorise + if (!$url.Contains('?')) { + $url += '?' + } + else { + $url += '&' + } + + # redirect to OAuth2 endpoint + Move-PodeResponseUrl -Url "$($url)$($query)" + return @{ IsRedirected = $true } + } + + # hmm, this is unexpected + return @{ + Message = 'Well, this is awkward...' + Code = 500 + IsErrored = $true + } + } +} + +function Get-PodeOAuth2RedirectHost { + param( + [Parameter()] + [string] + $RedirectUrl + ) + + if ($RedirectUrl.StartsWith('/')) { + if ($PodeContext.Server.IsIIS -or $PodeContext.Server.IsHeroku) { + $protocol = Get-PodeHeader -Name 'X-Forwarded-Proto' + if ([string]::IsNullOrWhiteSpace($protocol)) { + $protocol = 'https' + } + + $domain = "$($protocol)://$($WebEvent.Request.Host)" + } + else { + $domain = Get-PodeEndpointUrl + } + + $RedirectUrl = "$($domain.TrimEnd('/'))$($RedirectUrl)" + } + + return $RedirectUrl +} + +function Get-PodeAuthClientCertificateType { + return { + param($options) + $cert = $WebEvent.Request.ClientCertificate + + # ensure we have a client cert + if ($null -eq $cert) { + $message = 'No client certificate supplied' + return @{ + Message = $message + Code = 400 + Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`", error=`"invalid_request`", error_description=`"$message`"" } + } + } + + # ensure the cert has a thumbprint + if ([string]::IsNullOrWhiteSpace($cert.Thumbprint)) { + $message = 'Invalid client certificate supplied: missing thumbprint' + return @{ + Message = $message + Code = 400 + Headers = @{'WWW-Authenticate' = "TLS realm=`"$($options.Realm)`", error=`"invalid_token`", error_description=`"$message`"" } + } + } + + # ensure the cert hasn't expired, or has it even started + $now = [datetime]::UtcNow + if (($cert.NotAfter -lt $now) -or ($cert.NotBefore -gt $now)) { + $message = 'Client certificate supplied is expired or not valid yet' + return @{ + Message = $message + Code = 403 + Headers = @{'WWW-Authenticate' = "TLS realm=`"$($options.Realm)`", error=`"invalid_token`", error_description=`"$message`"" } + } + } + + # return data for calling validator + return @($cert, $WebEvent.Request.ClientCertificateErrors) + } +} + +function Get-PodeAuthApiKeyType { + return { + param($options) + + # get api key from appropriate location + $apiKey = [string]::Empty + + switch ($options.Location.ToLowerInvariant()) { + 'header' { + $apiKey = Get-PodeHeader -Name $options.LocationName + } + + 'query' { + $apiKey = $WebEvent.Query[$options.LocationName] + } + + 'cookie' { + $apiKey = Get-PodeCookieValue -Name $options.LocationName + } + } + # 400 if no key + if ([string]::IsNullOrWhiteSpace($apiKey)) { + $message = "No $($options.LocationName) $($options.Location) found" + return @{ + Message = $message + Code = 400 + Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`", error=`"invalid_request`", error_description=`"$message`"" } + } + } + + # build the result + $apiKey = $apiKey.Trim() + $result = @($apiKey) + + # convert as jwt? + if ($options.AsJWT) { + try { + $payload = ConvertFrom-PodeJwt -Token $apiKey -Secret $options.Secret + Test-PodeJwt -Payload $payload + } + catch { + if ($_.Exception.Message -ilike '*jwt*') { + return @{ + Message = $_.Exception.Message + Code = 400 + Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`", error=`"invalid_request`", error_description=`"$($_.Exception.Message)`"" } + } + } + + throw + } + + $result = @($payload) + } + + # return the result + return $result + } +} + +function Get-PodeAuthBearerType { + return { + <# + .SYNOPSIS + Validates the Bearer token in the Authorization header. + + .DESCRIPTION + This function processes the Authorization header, verifies the presence of a Bearer token, + and optionally decodes it as a JWT. It returns appropriate HTTP response codes + as per RFC 6750 (OAuth 2.0 Bearer Token Usage). + + .PARAMETER $options + A hashtable containing the following keys: + - Realm: The authentication realm. + - Scopes: Expected scopes for the token. + - HeaderTag: The expected Authorization header tag (e.g., 'Bearer'). + - AsJWT: Boolean indicating if the token should be processed as a JWT. + - Secret: Secret key for JWT verification. + + .OUTPUTS + A hashtable containing the following keys based on the validation result: + - Message: Error or success message. + - Code: HTTP response code. + - Header: HTTP response header for authentication challenges. + - Challenge: Optional authentication challenge. + + .NOTES + The function adheres to RFC 6750, which mandates: + - 401 Unauthorized for missing or invalid authentication credentials. + - 400 Bad Request for malformed requests. + + RFC 6750 HTTP Status Code Usage + # | Scenario | Recommended Status Code | + # |-------------------------------------------|-------------------------| + # | No Authorization header provided | 401 Unauthorized | + # | Incorrect Authorization header format | 401 Unauthorized | + # | Wrong authentication scheme used | 401 Unauthorized | + # | Token is empty or malformed | 400 Bad Request | + # | Invalid JWT signature | 401 Unauthorized | + #> + + param($options) + write-podehost "I'm here" + # Define common WWW-Authenticate header with placeholders + $authHeaderBase = "Bearer realm=`"$($options.Realm)`", error=`"{0}`", error_description=`"{1}`"" + + # Get the Authorization header + $header = (Get-PodeHeader -Name 'Authorization') + + # If no Authorization header is provided, return 401 Unauthorized + if ($null -eq $header) { + $message = 'No Authorization header found' + return @{ + Message = $message + Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request) + Code = 401 # RFC 6750: Missing credentials should return 401 + Header = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } + } + } + + # Ensure the first part of the header is 'Bearer' + $atoms = $header -isplit '\s+' + if ($atoms.Length -lt 2) { + $message = 'Invalid Authorization header format' + return @{ + Message = $message + Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request) + Code = 401 # RFC 6750: Invalid credentials format should return 401 + Header = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } + } + } + + if ($atoms[0] -ine $options.HeaderTag) { + $message = "Authorization header is not $($options.HeaderTag)" + return @{ + Message = $message + Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request) + Code = 401 # RFC 6750: Wrong authentication scheme should return 401 + Header = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } + } + } + + # 400 Bad Request if no token is provided + $token = $atoms[1] + if ([string]::IsNullOrWhiteSpace($token)) { + $message = 'No Bearer token found' + return @{ + Message = $message + Code = 400 # RFC 6750: Malformed request should return 400 + Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } + } + } + + # Trim and build the result + $token = $token.Trim() + $result = @($token) + + # Convert to JWT if required + if ($options.AsJWT) { + try { + $payload = ConvertFrom-PodeJwt -Token $token -Secret $options.Secret + Test-PodeJwt -Payload $payload + } + catch { + if ($_.Exception.Message -ilike '*jwt*') { + return @{ + Message = $_.Exception.Message + Code = 401 # RFC 6750: Invalid token should return 401 + Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_token', $_.Exception.Message } + } + } + + throw + } + + $result = @($payload) + } + write-podehost "I'm here2" + # Return the validated result + return $result + } +} + + +function Get-PodeAuthBearerPostValidator { + return { + param($token, $result, $options) + + # if there's no user, fail with challenge + if (($null -eq $result) -or ($null -eq $result.User)) { + return @{ + Message = 'User not found' + Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_token) + Code = 401 + } + } + + # check for an error and description + if (![string]::IsNullOrWhiteSpace($result.Error)) { + return @{ + Message = 'Authorization failed' + Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType $result.Error -ErrorDescription $result.ErrorDescription) + Code = 401 + } + } + + # check the scopes + $hasAuthScopes = (($null -ne $options.Scopes) -and ($options.Scopes.Length -gt 0)) + $hasTokenScope = ![string]::IsNullOrWhiteSpace($result.Scope) + + # 403 if we have auth scopes but no token scope + if ($hasAuthScopes -and !$hasTokenScope) { + return @{ + Message = 'Invalid Scope' + Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType insufficient_scope) + Code = 403 + } + } + + # 403 if we have both, but token not in auth scope + if ($hasAuthScopes -and $hasTokenScope -and ($options.Scopes -notcontains $result.Scope)) { + return @{ + Message = 'Invalid Scope' + Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType insufficient_scope) + Code = 403 + } + } + + # return result + return $result + } +} + +function New-PodeAuthBearerChallenge { + param( + [Parameter()] + [string[]] + $Scopes, + + [Parameter()] + [ValidateSet('', 'invalid_request', 'invalid_token', 'insufficient_scope')] + [string] + $ErrorType, + + [Parameter()] + [string] + $ErrorDescription + ) + + $items = @() + if (($null -ne $Scopes) -and ($Scopes.Length -gt 0)) { + $items += "scope=`"$($Scopes -join ' ')`"" + } + + if (![string]::IsNullOrWhiteSpace($ErrorType)) { + $items += "error=`"$($ErrorType)`"" + } + + if (![string]::IsNullOrWhiteSpace($ErrorDescription)) { + $items += "error_description=`"$($ErrorDescription)`"" + } + + return ($items -join ', ') +} + +<# +.SYNOPSIS + Validates the Digest token in the Authorization header. + +.DESCRIPTION + This function processes the Authorization header, verifies the presence of a Digest token, + and optionally decodes it. It returns appropriate HTTP response codes + as per RFC 7616 (HTTP Digest Access Authentication). + +.PARAMETER $options + A hashtable containing the following keys: + - Realm: The authentication realm. + - Nonce: A unique value provided by the server to prevent replay attacks. + - HeaderTag: The expected Authorization header tag (e.g., 'Digest'). + - Algorithm: The hashing algorithm used (e.g., MD5, SHA-256). + +.OUTPUTS + A hashtable containing the following keys based on the validation result: + - Message: Error or success message. + - Code: HTTP response code. + - Header: HTTP response header for authentication challenges. + - Challenge: Optional authentication challenge. + +.NOTES + The function adheres to RFC 7616, which mandates: + - 401 Unauthorized for missing or invalid authentication credentials. + - 400 Bad Request for malformed requests. + + - RFC 7616 HTTP Status Code Usage + | Scenario | Recommended Status Code | + |-------------------------------------------|-------------------------| + | No Authorization header provided | 401 Unauthorized | + | Incorrect Authorization header format | 401 Unauthorized | + | Wrong authentication scheme used | 401 Unauthorized | + | Token is empty or malformed | 400 Bad Request | + | Invalid digest response | 401 Unauthorized | + + #> +function Get-PodeAuthDigestType { + return { + param($options) + write-podehost "I'm here1" + # Define common WWW-Authenticate header with placeholders + $authHeaderBase = "Digest realm=`"$($options.Realm)`", nonce=`"$($options.Nonce)`", algorithm=`"$($options.Algorithm)`", error=`"{0}`", error_description=`"{1}`"" + + # Get the Authorization header - send challenge if missing + $header = (Get-PodeHeader -Name 'Authorization') + if ($null -eq $header) { + $message = 'No Authorization header found' + write-podehost $message + return @{ + Message = $message + Code = 401 # RFC 7616: Missing credentials should return 401 + Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } + } + } + + # If auth header isn't digest, send challenge + $atoms = $header -isplit '\s+' + if ($atoms.Length -lt 2) { + $message = 'Invalid Authorization header format' + write-podehost $message + return @{ + Message = $message + Code = 401 # RFC 7616: Invalid credentials format should return 401 + Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } + } + } + + if ($atoms[0] -ine $options.HeaderTag) { + $message = "Authorization header is not $($options.HeaderTag)" + write-podehost $message + return @{ + Message = $message + Code = 401 # RFC 7616: Wrong authentication scheme should return 401 + Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } + } + } + + # Parse the other atoms of the header (after the scheme), return 400 if none + $params = ConvertFrom-PodeAuthDigestHeader -Parts ($atoms[1..$($atoms.Length - 1)]) + if ($params.Count -eq 0) { + $message = 'Invalid Authorization header' + write-podehost $message + return @{ + Message = $message + Code = 400 + Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } + } + } + + # If no username then 401 and challenge + if ([string]::IsNullOrWhiteSpace($params.username)) { + $message = 'Authorization header is missing username' + write-podehost $message + return @{ + Message = $message + Challenge = (New-PodeAuthDigestChallenge) + Code = 401 + Header = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } + } + } + + # Return 400 if domain doesn't match request domain + if ($WebEvent.Path -ine $params.uri) { + $message = 'Invalid Authorization header' + write-podehost $message + return @{ + Message = $message + Code = 400 + Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } + } + } + + write-podehost "I'm here2" + # Return data for calling validator + return @($params.username, $params) + } +} + + +function Get-PodeAuthDigestPostValidator { + return { + param($username, $params, $result, $options) + + # if there's no user or password, fail with challenge + if (($null -eq $result) -or ($null -eq $result.User) -or [string]::IsNullOrWhiteSpace($result.Password)) { + return @{ + Message = 'User not found' + Challenge = (New-PodeAuthDigestChallenge) + Code = 401 + } + } + + # generate the first hash + $hash1 = Invoke-PodeMD5Hash -Value "$($params.username):$($params.realm):$($result.Password)" + + # generate the second hash + $hash2 = Invoke-PodeMD5Hash -Value "$($WebEvent.Method.ToUpperInvariant()):$($params.uri)" + + # generate final hash + $final = Invoke-PodeMD5Hash -Value "$($hash1):$($params.nonce):$($params.nc):$($params.cnonce):$($params.qop):$($hash2)" + + # compare final hash to client response + if ($final -ne $params.response) { + return @{ + Message = 'Hashes failed to match' + Challenge = (New-PodeAuthDigestChallenge) + Code = 401 + } + } + + # hashes are valid, remove password and return result + $null = $result.Remove('Password') + return $result + } +} + +function ConvertFrom-PodeAuthDigestHeader { + param( + [Parameter()] + [string[]] + $Parts + ) + + if (($null -eq $Parts) -or ($Parts.Length -eq 0)) { + return @{} + } + + $obj = @{} + $value = ($Parts -join ' ') + + @($value -isplit ',(?=(?:[^"]|"[^"]*")*$)') | ForEach-Object { + if ($_ -imatch '(?\w+)=["]?(?[^"]+)["]?$') { + $obj[$Matches['name']] = $Matches['value'] + } + } + + return $obj +} + +function New-PodeAuthDigestChallenge { + $items = @('qop="auth"', 'algorithm="MD5"', "nonce=`"$(New-PodeGuid -Secure -NoDashes)`"") + return ($items -join ', ') +} + +function Get-PodeAuthFormType { + return { + param($options) + + # get user/pass keys to get from payload + $userField = $options.Fields.Username + $passField = $options.Fields.Password + + # get the user/pass + $username = $WebEvent.Data.$userField + $password = $WebEvent.Data.$passField + + # if either are empty, fail auth + if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) { + return @{ + Message = 'Username or Password not supplied' + Code = 401 + } + } + + # build the result + $result = @($username, $password) + + # convert to credential? + if ($options.AsCredential) { + $passSecure = ConvertTo-SecureString -String $password -AsPlainText -Force + $creds = [pscredential]::new($username, $passSecure) + $result = @($creds) + } + + # return data for calling validator + return $result + } +} + +<# +.SYNOPSIS + Authenticates a user based on a username and password provided as parameters. + +.DESCRIPTION + This function finds a user whose username matches the provided username, and checks the user's password. + If the password is correct, it converts the user into a hashtable and checks if the user is valid for any users/groups specified by the options parameter. If the user is valid, it returns a hashtable containing the user object. If the user is not valid, it returns a hashtable with a message indicating that the user is not authorized to access the website. + +.PARAMETER username + The username of the user to authenticate. + +.PARAMETER password + The password of the user to authenticate. + +.PARAMETER options + A hashtable containing options for the function. It can include the following keys: + - FilePath: The path to the JSON file containing user data. + - HmacSecret: The secret key for computing a HMAC-SHA256 hash of the password. + - Users: A list of valid users. + - Groups: A list of valid groups. + - ScriptBlock: A script block for additional validation. + +.EXAMPLE + Get-PodeAuthUserFileMethod -username "admin" -password "password123" -options @{ FilePath = "C:\Users.json"; HmacSecret = "secret"; Users = @("admin"); Groups = @("Administrators"); ScriptBlock = { param($user) $user.Name -eq "admin" } } + + This example authenticates a user with username "admin" and password "password123". It reads user data from the JSON file at "C:\Users.json", computes a HMAC-SHA256 hash of the password using "secret" as the secret key, and checks if the user is in the "admin" user or "Administrators" group. It also performs additional validation using a script block that checks if the user's name is "admin". +#> +function Get-PodeAuthUserFileMethod { + return { + param($username, $password, $options) + + # using pscreds? + if (($null -eq $options) -and ($username -is [pscredential])) { + $_username = ([pscredential]$username).UserName + $_password = ([pscredential]$username).GetNetworkCredential().Password + $_options = [hashtable]$password + } + else { + $_username = $username + $_password = $password + $_options = $options + } + + # load the file + $users = (Get-Content -Path $_options.FilePath -Raw | ConvertFrom-Json) + + # find the user by username - only use the first one + $user = @(foreach ($_user in $users) { + if ($_user.Username -ieq $_username) { + $_user + break + } + })[0] + + # fail if no user + if ($null -eq $user) { + return @{ Message = 'You are not authorised to access this website' } + } + + # check the user's password + if (![string]::IsNullOrWhiteSpace($_options.HmacSecret)) { + $hash = Invoke-PodeHMACSHA256Hash -Value $_password -Secret $_options.HmacSecret + } + else { + $hash = Invoke-PodeSHA256Hash -Value $_password + } + + if ($user.Password -ne $hash) { + return @{ Message = 'You are not authorised to access this website' } + } + + # convert the user to a hashtable + $user = @{ + Name = $user.Name + Username = $user.Username + Email = $user.Email + Groups = $user.Groups + Metadata = $user.Metadata + } + + # is the user valid for any users/groups? + if (!(Test-PodeAuthUserGroup -User $user -Users $_options.Users -Groups $_options.Groups)) { + return @{ Message = 'You are not authorised to access this website' } + } + + $result = @{ User = $user } + + # call additional scriptblock if supplied + if ($null -ne $_options.ScriptBlock.Script) { + $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $_options.ScriptBlock.Script -UsingVariables $_options.ScriptBlock.UsingVariables + } + + # return final result, this could contain a user obj, or an error message from custom scriptblock + return $result + } +} + +function Get-PodeAuthWindowsADMethod { + return { + param($username, $password, $options) + + # using pscreds? + if (($null -eq $options) -and ($username -is [pscredential])) { + $_username = ([pscredential]$username).UserName + $_password = ([pscredential]$username).GetNetworkCredential().Password + $_options = [hashtable]$password + } + else { + $_username = $username + $_password = $password + $_options = $options + } + + # parse username to remove domains + $_username = (($_username -split '@')[0] -split '\\')[-1] + + # validate and retrieve the AD user + $noGroups = $_options.NoGroups + $directGroups = $_options.DirectGroups + $keepCredential = $_options.KeepCredential + + $result = Get-PodeAuthADResult ` + -Server $_options.Server ` + -Domain $_options.Domain ` + -SearchBase $_options.SearchBase ` + -Username $_username ` + -Password $_password ` + -Provider $_options.Provider ` + -NoGroups:$noGroups ` + -DirectGroups:$directGroups ` + -KeepCredential:$keepCredential + + # if there's a message, fail and return the message + if (![string]::IsNullOrWhiteSpace($result.Message)) { + return $result + } + + # if there's no user, then, err, oops + if (Test-PodeIsEmpty $result.User) { + return @{ Message = 'An unexpected error occured' } + } + + # is the user valid for any users/groups - if not, error! + if (!(Test-PodeAuthUserGroup -User $result.User -Users $_options.Users -Groups $_options.Groups)) { + return @{ Message = 'You are not authorised to access this website' } + } + + # call additional scriptblock if supplied + if ($null -ne $_options.ScriptBlock.Script) { + $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $_options.ScriptBlock.Script -UsingVariables $_options.ScriptBlock.UsingVariables + } + + # return final result, this could contain a user obj, or an error message from custom scriptblock + return $result + } +} + +function Invoke-PodeAuthInbuiltScriptBlock { + param( + [Parameter(Mandatory = $true)] + [hashtable] + $User, + + [Parameter(Mandatory = $true)] + [scriptblock] + $ScriptBlock, + + [Parameter()] + $UsingVariables + ) + + return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Arguments $User -UsingVariables $UsingVariables -Return) +} + +function Get-PodeAuthWindowsLocalMethod { + return { + param($username, $password, $options) + + # using pscreds? + if (($null -eq $options) -and ($username -is [pscredential])) { + $_username = ([pscredential]$username).UserName + $_password = ([pscredential]$username).GetNetworkCredential().Password + $_options = [hashtable]$password + } + else { + $_username = $username + $_password = $password + $_options = $options + } + + $user = @{ + UserType = 'Local' + AuthenticationType = 'WinNT' + Username = $_username + Name = [string]::Empty + Fqdn = $PodeContext.Server.ComputerName + Domain = 'localhost' + Groups = @() + } + + Add-Type -AssemblyName System.DirectoryServices.AccountManagement -ErrorAction Stop + $context = [System.DirectoryServices.AccountManagement.PrincipalContext]::new('Machine', $PodeContext.Server.ComputerName) + $valid = $context.ValidateCredentials($_username, $_password) + + if (!$valid) { + return @{ Message = 'Invalid credentials supplied' } + } + + try { + $tmpUsername = $_username -replace '\\', '/' + if ($_username -inotlike "$($PodeContext.Server.ComputerName)*") { + $tmpUsername = "$($PodeContext.Server.ComputerName)/$($_username)" + } + + $ad = [adsi]"WinNT://$($tmpUsername)" + $user.Name = @($ad.FullName)[0] + + if (!$_options.NoGroups) { + $cmd = "`$ad = [adsi]'WinNT://$($tmpUsername)'; @(`$ad.Groups() | Foreach-Object { `$_.GetType().InvokeMember('Name', 'GetProperty', `$null, `$_, `$null) })" + $user.Groups = [string[]](powershell -c $cmd) + } + } + finally { + Close-PodeDisposable -Disposable $ad -Close + } + + # is the user valid for any users/groups - if not, error! + if (!(Test-PodeAuthUserGroup -User $user -Users $_options.Users -Groups $_options.Groups)) { + return @{ Message = 'You are not authorised to access this website' } + } + + $result = @{ User = $user } + + # call additional scriptblock if supplied + if ($null -ne $_options.ScriptBlock.Script) { + $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $_options.ScriptBlock.Script -UsingVariables $_options.ScriptBlock.UsingVariables + } + + # return final result, this could contain a user obj, or an error message from custom scriptblock + return $result + } +} + +function Get-PodeAuthWindowsADIISMethod { + return { + param($token, $options) + + # get the close handler + $win32Handler = Add-Type -Name Win32CloseHandle -PassThru -MemberDefinition @' + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle(IntPtr handle); +'@ + + try { + # parse the auth token and get the user + $winAuthToken = [System.IntPtr][Int]"0x$($token)" + $winIdentity = [System.Security.Principal.WindowsIdentity]::new($winAuthToken, 'Windows') + + # get user and domain + $username = ($winIdentity.Name -split '\\')[-1] + $domain = ($winIdentity.Name -split '\\')[0] + + # create base user object + $user = @{ + UserType = 'Domain' + Identity = @{ + AccessToken = $winIdentity.AccessToken + } + AuthenticationType = $winIdentity.AuthenticationType + DistinguishedName = [string]::Empty + Username = $username + Name = [string]::Empty + Email = [string]::Empty + Fqdn = [string]::Empty + Domain = $domain + Groups = @() + } + + # if the domain isn't local, attempt AD user + if (![string]::IsNullOrWhiteSpace($domain) -and (@('.', $PodeContext.Server.ComputerName) -inotcontains $domain)) { + # get the server's fdqn (and name/email) + try { + # Open ADSISearcher and change context to given domain + $searcher = [adsisearcher]'' + $searcher.SearchRoot = [adsi]"LDAP://$($domain)" + $searcher.Filter = "ObjectSid=$($winIdentity.User.Value.ToString())" + + # Query the ADSISearcher for the above defined SID + $ad = $searcher.FindOne() + + # Save it to our existing array for later usage + $user.DistinguishedName = @($ad.Properties.distinguishedname)[0] + $user.Name = @($ad.Properties.name)[0] + $user.Email = @($ad.Properties.mail)[0] + $user.Fqdn = (Get-PodeADServerFromDistinguishedName -DistinguishedName $user.DistinguishedName) + } + finally { + Close-PodeDisposable -Disposable $searcher + } + + try { + if (!$options.NoGroups) { + + # open a new connection + $result = (Open-PodeAuthADConnection -Server $user.Fqdn -Domain $domain -Provider $options.Provider) + if (!$result.Success) { + return @{ Message = "Failed to connect to Domain Server '$($user.Fqdn)' of $domain for $($user.DistinguishedName)." } + } + + # get the connection + $connection = $result.Connection + + # get the users groups + $directGroups = $options.DirectGroups + $user.Groups = (Get-PodeAuthADGroup -Connection $connection -DistinguishedName $user.DistinguishedName -Username $user.Username -Direct:$directGroups -Provider $options.Provider) + } + } + finally { + if ($null -ne $connection) { + Close-PodeDisposable -Disposable $connection.Searcher + Close-PodeDisposable -Disposable $connection.Entry -Close + $connection.Credential = $null + } + } + } + + # otherwise, get details of local user + else { + # get the user's name and groups + try { + $user.UserType = 'Local' + + if (!$options.NoLocalCheck) { + $localUser = $winIdentity.Name -replace '\\', '/' + $ad = [adsi]"WinNT://$($localUser)" + $user.Name = @($ad.FullName)[0] + + # dirty, i know :/ - since IIS runs using pwsh, the InvokeMember part fails + # we can safely call windows powershell here, as IIS is only on windows. + if (!$options.NoGroups) { + $cmd = "`$ad = [adsi]'WinNT://$($localUser)'; @(`$ad.Groups() | Foreach-Object { `$_.GetType().InvokeMember('Name', 'GetProperty', `$null, `$_, `$null) })" + $user.Groups = [string[]](powershell -c $cmd) + } + } + } + finally { + Close-PodeDisposable -Disposable $ad -Close + } + } + } + catch { + $_ | Write-PodeErrorLog + return @{ Message = 'Failed to retrieve user using Authentication Token' } + } + finally { + $win32Handler::CloseHandle($winAuthToken) + } + + # is the user valid for any users/groups - if not, error! + if (!(Test-PodeAuthUserGroup -User $user -Users $options.Users -Groups $options.Groups)) { + return @{ Message = 'You are not authorised to access this website' } + } + + $result = @{ User = $user } + + # call additional scriptblock if supplied + if ($null -ne $options.ScriptBlock.Script) { + $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $options.ScriptBlock.Script -UsingVariables $options.ScriptBlock.UsingVariables + } + + # return final result, this could contain a user obj, or an error message from custom scriptblock + return $result + } +} + +<# +.SYNOPSIS + Authenticates a user based on group membership or specific user authorization. + +.DESCRIPTION + This function checks if a given user is authorized based on supplied lists of users and groups. The user is considered authorized if their username is directly specified in the list of users, or if they are a member of any of the specified groups. + +.PARAMETER User + A hashtable representing the user, expected to contain at least the 'Username' and 'Groups' keys. + +.PARAMETER Users + An optional array of usernames. If specified, the function checks if the user's username exists in this list. + +.PARAMETER Groups + An optional array of group names. If specified, the function checks if the user belongs to any of these groups. + +.EXAMPLE + $user = @{ Username = 'john.doe'; Groups = @('Administrators', 'Users') } + $authorizedUsers = @('john.doe', 'jane.doe') + $authorizedGroups = @('Administrators') + + Test-PodeAuthUserGroup -User $user -Users $authorizedUsers -Groups $authorizedGroups + # Returns true if John Doe is either listed as an authorized user or is a member of an authorized group. +#> +function Test-PodeAuthUserGroup { + param( + [Parameter(Mandatory = $true)] + [hashtable] + $User, + + [Parameter()] + [string[]] + $Users, + + [Parameter()] + [string[]] + $Groups + ) + + $haveUsers = (($null -ne $Users) -and ($Users.Length -gt 0)) + $haveGroups = (($null -ne $Groups) -and ($Groups.Length -gt 0)) + + # if there are no groups/users supplied, return user is valid + if (!$haveUsers -and !$haveGroups) { + return $true + } + + # before checking supplied groups, is the user in the supplied list of authorised users? + if ($haveUsers -and (@($Users) -icontains $User.Username)) { + return $true + } + + # if there are groups supplied, check the user is a member of one + if ($haveGroups) { + foreach ($group in $Groups) { + if (@($User.Groups) -icontains $group) { + return $true + } + } + } + + return $false +} + +function Invoke-PodeAuthValidation { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + # get auth method + $auth = $PodeContext.Server.Authentications.Methods[$Name] + + # if it's a merged auth, re-call this function and check against "succeed" value + if ($auth.Merged) { + $results = @{} + foreach ($authName in $auth.Authentications) { + $result = Invoke-PodeAuthValidation -Name $authName + + # if the auth is trying to redirect, we need to bubble the this back now + if ($result.Redirected) { + return $result + } + + # if the auth passed, and we only need one auth to pass, return current result + if ($result.Success -and $auth.PassOne) { + return $result + } + + # if the auth failed, but we need all to pass, return current result + if (!$result.Success -and !$auth.PassOne) { + return $result + } + + # remember result if we need all to pass + if (!$auth.PassOne) { + $results[$authName] = $result + } + } + # if the last auth failed, and we only need one auth to pass, set failure and return + if (!$result.Success -and $auth.PassOne) { + return $result + } + + # if the last auth succeeded, and we need all to pass, merge users/headers and return result + if ($result.Success -and !$auth.PassOne) { + # invoke scriptblock, or use result of merge default + if ($null -ne $auth.ScriptBlock.Script) { + $result = Invoke-PodeAuthInbuiltScriptBlock -User $results -ScriptBlock $auth.ScriptBlock.Script -UsingVariables $auth.ScriptBlock.UsingVariables + } + else { + $result = $results[$auth.MergeDefault] + } + + # reset default properties and return + $result.Success = $true + $result.Auth = $results.Keys + return $result + } + + # default failure + return @{ + Success = $false + StatusCode = 500 + } + } + + # main auth validation logic + $result = (Test-PodeAuthValidation -Name $Name) + $result.Auth = $Name + return $result +} + +<# +.SYNOPSIS + Tests the authentication validation for a specified authentication method. + +.DESCRIPTION + The `Test-PodeAuthValidation` function processes an authentication method by its name, + running the associated scripts, middleware, and validations to determine authentication success or failure. + +.PARAMETER Name + The name of the authentication method to validate. This parameter is mandatory. + +.PARAMETER NoMiddlewareAuthentication + A switch to indicate whether the function has to threat the authentication because no Middleware authentication has been executed. + +.OUTPUTS + A hashtable containing the authentication validation result, including success status, user details, + headers, and redirection information if applicable. + +.NOTES + This is an internal function and is subject to change in future versions of Pode. +#> +function Test-PodeAuthValidation { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [switch] + $NoMiddlewareAuthentication + ) + + try { + # Retrieve authentication method configuration from Pode context + $auth = $PodeContext.Server.Authentications.Methods[$Name] + + # Initialize authentication result variable + $result = $null + + # Run pre-authentication middleware if defined + if ($null -ne $auth.Scheme.Middleware) { + if (!(Invoke-PodeMiddleware -Middleware $auth.Scheme.Middleware)) { + return @{ + Success = $false + } + } + } + + # Prepare arguments for the authentication scheme script + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $auth.Scheme.Arguments -UsingVariables $auth.Scheme.ScriptBlock.UsingVariables) + + # Handle inner authentication schemes (if any) + if ($null -ne $auth.Scheme.InnerScheme) { + $schemes = @() + $_scheme = $auth.Scheme + + # Traverse through the inner schemes to collect them + $_inner = @(while ($null -ne $_scheme.InnerScheme) { + $_scheme = $_scheme.InnerScheme + $_scheme + }) + + # Process inner schemes in reverse order + for ($i = $_inner.Length - 1; $i -ge 0; $i--) { + $_tmp_args = @(Merge-PodeScriptblockArguments -ArgumentList $_inner[$i].Arguments -UsingVariables $_inner[$i].ScriptBlock.UsingVariables) + $_tmp_args += , $schemes + + $result = (Invoke-PodeScriptBlock -ScriptBlock $_inner[$i].ScriptBlock.Script -Arguments $_tmp_args -Return -Splat) + if ($result -is [hashtable]) { + break # Exit if a valid result is returned + } + + $schemes += , $result + $result = $null + } + + $_args += , $schemes + } + + # Execute the primary authentication script if no result from inner schemes and not a route script + if ($null -eq $result) { + $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.ScriptBlock.Script -Arguments $_args -Return -Splat) + } + + # Remove the Middleware processed data if code is 400 - no token + if ($NoMiddlewareAuthentication -and (($result.Code -eq 400) -or ($result.Code -eq 401))) { + $headers = $result.Headers + $result = '' + $code = 401 + } + write-podehost $result -Explode + # If authentication script returns a non-hashtable, perform further validation + if ($result -isnot [hashtable]) { + $original = $result + $_args = @($result) + @($auth.Arguments) + + # Run main authentication validation script + $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.ScriptBlock -Arguments $_args -UsingVariables $auth.UsingVariables -Return -Splat) + + # Run post-authentication validation if applicable + if ([string]::IsNullOrEmpty($result.Code) -and ($null -ne $auth.Scheme.PostValidator.Script)) { + $_args = @($original) + @($result) + @($auth.Scheme.Arguments) + $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.PostValidator.Script -Arguments $_args -UsingVariables $auth.Scheme.PostValidator.UsingVariables -Return -Splat) + } + } + else { + # if ($result.Headers.ContainsKey('WWW-Authenticate')) { + # Add-PodeHeader -Name 'WWW-Authenticate' -Value $result.Headers['WWW-Authenticate'] + # } + } + write-podehost $result -Explode + # Handle authentication redirection scenarios (e.g., OAuth) + if ($result.IsRedirected) { + return @{ + Success = $false + Redirected = $true + } + } + + # Handle results when invoked from a route script + if ($NoMiddlewareAuthentication -and ($null -ne $result) -and ($result -is [hashtable])) { + if ($result.Success -is [bool]) { + $success = $result.Success + } + else { + $success = $false + [System.Exception]::new("The authentication Scriptblock must return an hashtable with a key named 'Success'") | Write-PodeErrorLog + } + + $ret = @{ + Success = $success + User = '' + Headers = $headers + IsAuthenticated = $success + IsAuthorised = $success + Store = !$auth.Sessionless + Name = $Name + } + foreach ($key in $result.Keys) { + $ret[$key] = $result[$key] # Overwrites if key exists + } + + return $ret + } + + # Authentication failure handling + if (($null -eq $result) -or ($result.Count -eq 0) -or (Test-PodeIsEmpty $result.User)) { + $code = (Protect-PodeValue -Value $result.Code -Default 401) + + # Set WWW-Authenticate header for appropriate HTTP response + $validCode = (($code -eq 401) -or ![string]::IsNullOrEmpty($result.Challenge)) + Write-podehost "validCode =$validCode" + if ($validCode) { + if ($null -eq $result) { + $result = @{} + } + + if ($null -eq $result.Headers) { + $result.Headers = @{} + } + + # Generate authentication challenge header + if (![string]::IsNullOrWhiteSpace($auth.Scheme.Name) -and !$result.Headers.ContainsKey('WWW-Authenticate')) { + write-podehost 'Get-PodeAuthWwwHeaderValue' + $authHeader = Get-PodeAuthWwwHeaderValue -Name $auth.Scheme.Name -Realm $auth.Scheme.Realm -Challenge $result.Challenge + $result.Headers['WWW-Authenticate'] = $authHeader + } + else { + $authHeader = Get-PodeAuthWwwHeaderValue -Name $auth.Scheme.Name -Realm $auth.Scheme.Realm -Challenge $result.Challenge + write-podehost $authHeader + $result.Headers['WWW-Authenticate'] = $authHeader + write-podehost 'no Get-PodeAuthWwwHeaderValue' + } + } + + return @{ + Success = $false + StatusCode = $code + Description = $result.Message + Headers = $result.Headers + FailureRedirect = [bool]$result.IsErrored + } + } + + # Authentication succeeded, return user and headers + return @{ + Success = $true + User = $result.User + Headers = $result.Headers + } + } + catch { + $_ | Write-PodeErrorLog + + # Handle unexpected errors and log them + return @{ + Success = $false + StatusCode = 500 + Exception = $_ + } + } +} + + +function Get-PodeAuthMiddlewareScript { + return { + param($opts) + + return Test-PodeAuthInternal ` + -Name $opts.Name ` + -Login:($opts.Login) ` + -Logout:($opts.Logout) ` + -AllowAnon:($opts.Anon) + } +} + +function Test-PodeAuthInternal { + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [switch] + $Login, + + [switch] + $Logout, + + [switch] + $AllowAnon + ) + + # get the auth method + $auth = $PodeContext.Server.Authentications.Methods[$Name] + + # check for logout command + if ($Logout) { + Remove-PodeAuthSession + + if ($PodeContext.Server.Sessions.Info.UseHeaders) { + return Set-PodeAuthStatus ` + -StatusCode 401 ` + -Name $Name ` + -NoSuccessRedirect + } + else { + $auth.Failure.Url = (Protect-PodeValue -Value $auth.Failure.Url -Default $WebEvent.Request.Url.AbsolutePath) + return Set-PodeAuthStatus ` + -StatusCode 302 ` + -Name $Name ` + -NoSuccessRedirect + } + } + + # if the session already has a user/isAuth'd, then skip auth - or allow anon + if (Test-PodeSessionsInUse) { + # existing session auth'd + if (Test-PodeAuthUser) { + $WebEvent.Auth = $WebEvent.Session.Data.Auth + return Set-PodeAuthStatus ` + -Name $Name ` + -LoginRoute:($Login) ` + -NoSuccessRedirect + } + + # if we're allowing anon access, and using sessions, then stop here - as a session will be created from a login route for auth'ing users + if ($AllowAnon) { + if (!(Test-PodeIsEmpty $WebEvent.Session.Data.Auth)) { + Revoke-PodeSession + } + + return $true + } + } + + # check if the login flag is set, in which case just return and load a login get-page (allowing anon access) + if ($Login -and !$PodeContext.Server.Sessions.Info.UseHeaders -and ($WebEvent.Method -ieq 'get')) { + if (!(Test-PodeIsEmpty $WebEvent.Session.Data.Auth)) { + Revoke-PodeSession + } + + return $true + } + + try { + $result = Invoke-PodeAuthValidation -Name $Name + } + catch { + $_ | Write-PodeErrorLog + return Set-PodeAuthStatus ` + -StatusCode 500 ` + -Description $_.Exception.Message ` + -Name $Name + } + + # did the auth force a redirect? + if ($result.Redirected) { + $success = Get-PodeAuthSuccessInfo -Name $Name + Set-PodeAuthRedirectUrl -UseOrigin:($success.UseOrigin) + return $false + } + + # if auth failed, are we allowing anon access? + if (!$result.Success -and $AllowAnon) { + return $true + } + + # if auth failed, set appropriate response headers/redirects + if (!$result.Success) { + return Set-PodeAuthStatus ` + -StatusCode $result.StatusCode ` + -Description $result.Description ` + -Headers $result.Headers ` + -Name $Name ` + -LoginRoute:$Login ` + -NoFailureRedirect:($result.FailureRedirect) + } + + # if auth passed, assign the user to the session + $WebEvent.Auth = [ordered]@{ + User = $result.User + IsAuthenticated = $true + IsAuthorised = $true + Store = !$auth.Sessionless + Name = $result.Auth + } + # successful auth + $authName = $null + if ($auth.Merged -and !$auth.PassOne) { + $authName = $Name + } + else { + $authName = @($result.Auth)[0] + } + + return Set-PodeAuthStatus ` + -Headers $result.Headers ` + -Name $authName ` + -LoginRoute:$Login +} + +function Get-PodeAuthWwwHeaderValue { + param( + [Parameter()] + [string] + $Name, + + [Parameter()] + [string] + $Realm, + + [Parameter()] + [string] + $Challenge + ) + + if ([string]::IsNullOrWhiteSpace($Name)) { + return [string]::Empty + } + + $header = $Name + if (![string]::IsNullOrWhiteSpace($Realm)) { + $header += " realm=`"$($Realm)`"" + } + + if (![string]::IsNullOrWhiteSpace($Challenge)) { + $header += ", $($Challenge)" + } + + return $header +} + +function Remove-PodeAuthSession { + # blank out the auth + $WebEvent.Auth = @{} + + # if a session auth is found, blank it + if (!(Test-PodeIsEmpty $WebEvent.Session.Data.Auth)) { + $WebEvent.Session.Data.Remove('Auth') + } + + # Delete the current session (remove from store, blank it, and remove from Response) + Revoke-PodeSession +} + +function Get-PodeAuthFailureInfo { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [hashtable] + $Info, + + [Parameter()] + [string] + $BaseName + ) + + # base name + if ([string]::IsNullOrEmpty($BaseName)) { + $BaseName = $Name + } + + # get auth method + $auth = $PodeContext.Server.Authentications.Methods[$Name] + + # cached failure? + if ($null -ne $auth.Cache.Failure) { + return $auth.Cache.Failure + } + + # find failure info + if ($null -eq $Info) { + $Info = @{ + Url = $auth.Failure.Url + Message = $auth.Failure.Message + } + } + + if ([string]::IsNullOrEmpty($Info.Url)) { + $Info.Url = $auth.Failure.Url + } + + if ([string]::IsNullOrEmpty($Info.Message)) { + $Info.Message = $auth.Failure.Message + } + + if ((![string]::IsNullOrEmpty($Info.Url) -and ![string]::IsNullOrEmpty($Info.Message)) -or [string]::IsNullOrEmpty($auth.Parent)) { + $PodeContext.Server.Authentications.Methods[$BaseName].Cache.Failure = $Info + return $Info + } + + return (Get-PodeAuthFailureInfo -Name $auth.Parent -Info $Info -BaseName $BaseName) +} + +function Get-PodeAuthSuccessInfo { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [hashtable] + $Info, + + [Parameter()] + [string] + $BaseName + ) + + # base name + if ([string]::IsNullOrEmpty($BaseName)) { + $BaseName = $Name + } + + # get auth method + $auth = $PodeContext.Server.Authentications.Methods[$Name] + + # cached success? + if ($null -ne $auth.Cache.Success) { + return $auth.Cache.Success + } + + # find success info + if ($null -eq $Info) { + $Info = @{ + Url = $auth.Success.Url + UseOrigin = $auth.Success.UseOrigin + } + } + + if ([string]::IsNullOrEmpty($Info.Url)) { + $Info.Url = $auth.Success.Url + } + + if (!$Info.UseOrigin) { + $Info.UseOrigin = $auth.Success.UseOrigin + } + + if ((![string]::IsNullOrEmpty($Info.Url) -and $Info.UseOrigin) -or [string]::IsNullOrEmpty($auth.Parent)) { + $PodeContext.Server.Authentications.Methods[$BaseName].Cache.Success = $Info + return $Info + } + + return (Get-PodeAuthSuccessInfo -Name $auth.Parent -Info $Info -BaseName $BaseName) +} + +function Set-PodeAuthStatus { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [int] + $StatusCode = 0, + + [Parameter()] + [string] + $Description, + + [Parameter()] + [hashtable] + $Headers, + + [switch] + $LoginRoute, + + [switch] + $NoSuccessRedirect, + + [switch] + $NoFailureRedirect + ) + + # if we have any headers, set them + if (($null -ne $Headers) -and ($Headers.Count -gt 0)) { + foreach ($key in $Headers.Keys) { + Set-PodeHeader -Name $key -Value $Headers[$key] + } + } + + # get auth method + $auth = $PodeContext.Server.Authentications.Methods[$Name] + + # get Success object from auth + $success = Get-PodeAuthSuccessInfo -Name $Name + + # if a statuscode supplied, assume failure + if ($StatusCode -gt 0) { + # get Failure object from auth + $failure = Get-PodeAuthFailureInfo -Name $Name + + # override description with the failureMessage if supplied + $Description = (Protect-PodeValue -Value $failure.Message -Default $Description) + + # add error to flash + if ($LoginRoute -and !$auth.Sessionless -and ![string]::IsNullOrWhiteSpace($Description)) { + Add-PodeFlashMessage -Name 'auth-error' -Message $Description + } + + # check if we have a failure url redirect + if (!$NoFailureRedirect -and ![string]::IsNullOrWhiteSpace($failure.Url)) { + Set-PodeAuthRedirectUrl -UseOrigin:($success.UseOrigin) + Move-PodeResponseUrl -Url $failure.Url + } + else { + Set-PodeResponseStatus -Code $StatusCode -Description $Description + } + + return $false + } + + # if no statuscode, success, so check if we have a success url redirect (but only for auto-login routes) + if (!$NoSuccessRedirect -or $LoginRoute) { + $url = Get-PodeAuthRedirectUrl -Url $success.Url -UseOrigin:($success.UseOrigin) + if (![string]::IsNullOrWhiteSpace($url)) { + Move-PodeResponseUrl -Url $url + return $false + } + } + + return $true +} + +function Get-PodeADServerFromDistinguishedName { + param( + [Parameter()] + [string] + $DistinguishedName + ) + + if ([string]::IsNullOrWhiteSpace($DistinguishedName)) { + return [string]::Empty + } + + $parts = @($DistinguishedName -split ',') + $name = @() + + foreach ($part in $parts) { + if ($part -imatch '^DC=(?.+)$') { + $name += $Matches['name'] + } + } + + return ($name -join '.') +} + +function Get-PodeAuthADResult { + param( + [Parameter()] + [string] + $Server, + + [Parameter()] + [string] + $Domain, + + [Parameter()] + [string] + $SearchBase, + + [Parameter()] + [string] + $Username, + + [Parameter()] + [string] + $Password, + + [Parameter()] + [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')] + [string] + $Provider, + + [switch] + $NoGroups, + + [switch] + $DirectGroups, + + [switch] + $KeepCredential + ) + + try { + # validate the user's AD creds + $result = (Open-PodeAuthADConnection -Server $Server -Domain $Domain -Username $Username -Password $Password -Provider $Provider) + if (!$result.Success) { + return @{ Message = 'Invalid credentials supplied' } + } + + # get the connection + $connection = $result.Connection + + # get the user + $user = (Get-PodeAuthADUser -Connection $connection -Username $Username -Provider $Provider) + if ($null -eq $user) { + return @{ Message = 'User not found in Active Directory' } + } + + # get the users groups + $groups = @() + if (!$NoGroups) { + $groups = (Get-PodeAuthADGroup -Connection $connection -DistinguishedName $user.DistinguishedName -Username $Username -Direct:$DirectGroups -Provider $Provider) + } + + # check if we want to keep the credentials in the User object + if ($KeepCredential) { + $credential = [pscredential]::new($($Domain + '\' + $Username), (ConvertTo-SecureString -String $Password -AsPlainText -Force)) + } + else { + $credential = $null + } + + # return the user + return @{ + User = @{ + UserType = 'Domain' + AuthenticationType = 'LDAP' + DistinguishedName = $user.DistinguishedName + Username = ($Username -split '\\')[-1] + Name = $user.Name + Email = $user.Email + Fqdn = $Server + Domain = $Domain + Groups = $groups + Credential = $credential + } + } + } + finally { + if ($null -ne $connection) { + switch ($Provider.ToLowerInvariant()) { + 'openldap' { + $connection.Username = $null + $connection.Password = $null + } + + 'activedirectory' { + $connection.Credential = $null + } + + 'directoryservices' { + Close-PodeDisposable -Disposable $connection.Searcher + Close-PodeDisposable -Disposable $connection.Entry -Close + } + } + } + } +} + +function Open-PodeAuthADConnection { + param( + [Parameter(Mandatory = $true)] + [string] + $Server, + + [Parameter()] + [string] + $Domain, + + [Parameter()] + [string] + $SearchBase, + + [Parameter()] + [string] + $Username, + + [Parameter()] + [string] + $Password, + + [Parameter()] + [ValidateSet('LDAP', 'WinNT')] + [string] + $Protocol = 'LDAP', + + [Parameter()] + [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')] + [string] + $Provider + ) + + $result = $true + $connection = $null + + # validate the user's AD creds + switch ($Provider.ToLowerInvariant()) { + 'openldap' { + if (![string]::IsNullOrWhiteSpace($SearchBase)) { + $baseDn = $SearchBase + } + else { + $baseDn = "DC=$(($Server -split '\.') -join ',DC=')" + } + + $query = (Get-PodeAuthADQuery -Username $Username) + $hostname = "$($Protocol)://$($Server)" + + $user = $Username + if (!$Username.StartsWith($Domain)) { + $user = "$($Domain)\$($Username)" + } + + $null = (ldapsearch -x -LLL -H "$($hostname)" -D "$($user)" -w "$($Password)" -b "$($baseDn)" -o ldif-wrap=no "$($query)" dn) + if (!$? -or ($LASTEXITCODE -ne 0)) { + $result = $false + } + else { + $connection = @{ + Hostname = $hostname + Username = $user + BaseDN = $baseDn + Password = $Password + } + } + } + + 'activedirectory' { + try { + $creds = [pscredential]::new($Username, (ConvertTo-SecureString -String $Password -AsPlainText -Force)) + $null = Get-ADUser -Identity $Username -Credential $creds -ErrorAction Stop + $connection = @{ + Credential = $creds + } + } + catch { + $result = $false + } + } + + 'directoryservices' { + if ([string]::IsNullOrWhiteSpace($Password)) { + $ad = [System.DirectoryServices.DirectoryEntry]::new("$($Protocol)://$($Server)") + } + else { + $ad = [System.DirectoryServices.DirectoryEntry]::new("$($Protocol)://$($Server)", "$($Username)", "$($Password)") + } + + if (Test-PodeIsEmpty $ad.distinguishedName) { + $result = $false + } + else { + $connection = @{ + Entry = $ad + } + } + } + } + + return @{ + Success = $result + Connection = $connection + } +} + +function Get-PodeAuthADQuery { + param( + [Parameter(Mandatory = $true)] + [string] + $Username + ) + + return "(&(objectCategory=person)(samaccountname=$($Username)))" +} + +function Get-PodeAuthADUser { + param( + [Parameter(Mandatory = $true)] + $Connection, + + [Parameter(Mandatory = $true)] + [string] + $Username, + + [Parameter()] + [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')] + [string] + $Provider + ) + + $query = (Get-PodeAuthADQuery -Username $Username) + $user = $null + + # generate query to find user + switch ($Provider.ToLowerInvariant()) { + 'openldap' { + $result = (ldapsearch -x -LLL -H "$($Connection.Hostname)" -D "$($Connection.Username)" -w "$($Connection.Password)" -b "$($Connection.BaseDN)" -o ldif-wrap=no "$($query)" name mail) + if (!$? -or ($LASTEXITCODE -ne 0)) { + return $null + } + + $user = @{ + DistinguishedName = (Get-PodeOpenLdapValue -Lines $result -Property 'dn') + Name = (Get-PodeOpenLdapValue -Lines $result -Property 'name') + Email = (Get-PodeOpenLdapValue -Lines $result -Property 'mail') + } + } + + 'activedirectory' { + $result = Get-ADUser -LDAPFilter $query -Credential $Connection.Credential -Properties mail + $user = @{ + DistinguishedName = $result.DistinguishedName + Name = $result.Name + Email = $result.mail + } + } + + 'directoryservices' { + $Connection.Searcher = [System.DirectoryServices.DirectorySearcher]::new($Connection.Entry) + $Connection.Searcher.filter = $query + + $result = $Connection.Searcher.FindOne().Properties + if (Test-PodeIsEmpty $result) { + return $null + } + + $user = @{ + DistinguishedName = @($result.distinguishedname)[0] + Name = @($result.name)[0] + Email = @($result.mail)[0] + } + } + } + + return $user +} + +function Get-PodeOpenLdapValue { + param( + [Parameter()] + [string[]] + $Lines, + + [Parameter()] + [string] + $Property, + + [switch] + $All + ) + + foreach ($line in $Lines) { + if ($line -imatch "^$($Property)\:\s+(?<$($Property)>.+)$") { + # return the first found + if (!$All) { + return $Matches[$Property] + } + + # return array of all + $Matches[$Property] + } + } +} +<# +.SYNOPSIS + Retrieves Active Directory (AD) group information for a user. + +.DESCRIPTION + This function retrieves AD group information for a specified user. It supports two modes of operation: + 1. Direct: Retrieves groups directly associated with the user. + 2. All: Retrieves all groups within the specified distinguished name (DN). + +.PARAMETER Connection + The AD connection object or credentials for connecting to the AD server. + +.PARAMETER DistinguishedName + The distinguished name (DN) of the user or group. If not provided, the default DN is used. + +.PARAMETER Username + The username for which to retrieve group information. + +.PARAMETER Provider + The AD provider to use (e.g., 'DirectoryServices', 'ActiveDirectory', 'OpenLDAP'). + +.PARAMETER Direct + Switch parameter. If specified, retrieves only direct group memberships for the user. + +.OUTPUTS + Returns AD group information as needed based on the mode of operation. + +.EXAMPLE + Get-PodeAuthADGroup -Connection $adConnection -Username "john.doe" + # Retrieves all AD groups for the user "john.doe". + + Get-PodeAuthADGroup -Connection $adConnection -Username "jane.smith" -Direct + # Retrieves only direct group memberships for the user "jane.smith". +#> +function Get-PodeAuthADGroup { + param( + [Parameter(Mandatory = $true)] + $Connection, + + [Parameter()] + [string] + $DistinguishedName, + + [Parameter()] + [string] + $Username, + + [Parameter()] + [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')] + [string] + $Provider, + + [switch] + $Direct + ) + + if ($Direct) { + return (Get-PodeAuthADGroupDirect -Connection $Connection -Username $Username -Provider $Provider) + } + + return (Get-PodeAuthADGroupAll -Connection $Connection -DistinguishedName $DistinguishedName -Provider $Provider) +} + +function Get-PodeAuthADGroupDirect { + param( + [Parameter(Mandatory = $true)] + $Connection, + + [Parameter()] + [string] + $Username, + + [Parameter()] + [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')] + [string] + $Provider + ) + + # create the query + $query = "(&(objectCategory=person)(samaccountname=$($Username)))" + $groups = @() + + # get the groups + switch ($Provider.ToLowerInvariant()) { + 'openldap' { + $result = (ldapsearch -x -LLL -H "$($Connection.Hostname)" -D "$($Connection.Username)" -w "$($Connection.Password)" -b "$($Connection.BaseDN)" -o ldif-wrap=no "$($query)" memberof) + $groups = (Get-PodeOpenLdapValue -Lines $result -Property 'memberof' -All) + } + + 'activedirectory' { + $groups = (Get-ADPrincipalGroupMembership -Identity $Username -Credential $Connection.Credential).distinguishedName + } + + 'directoryservices' { + if ($null -eq $Connection.Searcher) { + $Connection.Searcher = [System.DirectoryServices.DirectorySearcher]::new($Connection.Entry) + } + + $Connection.Searcher.filter = $query + $groups = @($Connection.Searcher.FindOne().Properties.memberof) + } + } + + $groups = @(foreach ($group in $groups) { + if ($group -imatch '^CN=(?.+?),') { + $Matches['group'] + } + }) + + return $groups +} + +function Get-PodeAuthADGroupAll { + param( + [Parameter(Mandatory = $true)] + $Connection, + + [Parameter()] + [string] + $DistinguishedName, + + [Parameter()] + [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')] + [string] + $Provider + ) + + # create the query + $query = "(member:1.2.840.113556.1.4.1941:=$($DistinguishedName))" + $groups = @() + + # get the groups + switch ($Provider.ToLowerInvariant()) { + 'openldap' { + $result = (ldapsearch -x -LLL -H "$($Connection.Hostname)" -D "$($Connection.Username)" -w "$($Connection.Password)" -b "$($Connection.BaseDN)" -o ldif-wrap=no "$($query)" samaccountname) + $groups = (Get-PodeOpenLdapValue -Lines $result -Property 'sAMAccountName' -All) + } + + 'activedirectory' { + $groups = (Get-ADObject -LDAPFilter $query -Credential $Connection.Credential).Name + } + + 'directoryservices' { + if ($null -eq $Connection.Searcher) { + $Connection.Searcher = [System.DirectoryServices.DirectorySearcher]::new($Connection.Entry) + } + + $null = $Connection.Searcher.PropertiesToLoad.Add('samaccountname') + $Connection.Searcher.filter = $query + $groups = @($Connection.Searcher.FindAll().Properties.samaccountname) + } + } + + return $groups +} + +function Get-PodeAuthDomainName { + $domain = $null + + if (Test-PodeIsMacOS) { + $domain = (scutil --dns | grep -m 1 'search domain\[0\]' | cut -d ':' -f 2) + } + elseif (Test-PodeIsUnix) { + $domain = (dnsdomainname) + if ([string]::IsNullOrWhiteSpace($domain)) { + $domain = (/usr/sbin/realm list --name-only) + } + } + else { + $domain = $env:USERDNSDOMAIN + if ([string]::IsNullOrWhiteSpace($domain)) { + $domain = (Get-CimInstance -Class Win32_ComputerSystem -Verbose:$false).Domain + } + } + + if (![string]::IsNullOrEmpty($domain)) { + $domain = $domain.Trim() + } + + return $domain +} + +function Find-PodeAuth { + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Name + ) + + return $PodeContext.Server.Authentications.Methods[$Name] +} + +<# +.SYNOPSIS + Expands a list of authentication names, including merged authentication methods. + +.DESCRIPTION + The Expand-PodeAuthMerge function takes an array of authentication names and expands it by resolving any merged authentication methods + into their individual components. It is particularly useful in scenarios where authentication methods are combined or merged, and there + is a need to process each individual method separately. + +.PARAMETER Names + An array of authentication method names. These names can include both discrete authentication methods and merged ones. + +.EXAMPLE + $expandedAuthNames = Expand-PodeAuthMerge -Names @('BasicAuth', 'CustomMergedAuth') + + Expands the provided authentication names, resolving 'CustomMergedAuth' into its constituent authentication methods if it's a merged one. +#> +function Expand-PodeAuthMerge { + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string[]] + $Names + ) + + # Initialize a hashtable to store expanded authentication names + $authNames = @{} + + # Iterate over each authentication name + foreach ($authName in $Names) { + # Handle the special case of anonymous access + if ($authName -eq '%_allowanon_%') { + $authNames[$authName] = $true + } + else { + # Retrieve the authentication method from the Pode context + $_auth = $PodeContext.Server.Authentications.Methods[$authName] + + # Check if the authentication is a merged one and expand it + if ($_auth.merged) { + foreach ($key in (Expand-PodeAuthMerge -Names $_auth.Authentications)) { + $authNames[$key] = $true + } + } + else { + # If not merged, add the authentication name to the list + $authNames[$_auth.Name] = $true + } + } + } + + # Return the keys of the hashtable, which are the expanded authentication names + return $authNames.Keys +} + + +function Import-PodeAuthADModule { + if (!(Test-PodeIsWindows)) { + # Active Directory module only available on Windows + throw ($PodeLocale.adModuleWindowsOnlyExceptionMessage) + } + + if (!(Test-PodeModuleInstalled -Name ActiveDirectory)) { + # Active Directory module is not installed + throw ($PodeLocale.adModuleNotInstalledExceptionMessage) + } + + Import-Module -Name ActiveDirectory -Force -ErrorAction Stop + Export-PodeModule -Name ActiveDirectory +} + +function Get-PodeAuthADProvider { + param( + [switch] + $OpenLDAP, + + [switch] + $ADModule + ) + + # openldap (literal, or not windows) + if ($OpenLDAP -or !(Test-PodeIsWindows)) { + return 'OpenLDAP' + } + + # ad module + if ($ADModule) { + return 'ActiveDirectory' + } + + # ds + return 'DirectoryServices' +} + +function Set-PodeAuthRedirectUrl { + param( + [switch] + $UseOrigin + ) + + if ($UseOrigin -and ($WebEvent.Method -ieq 'get')) { + $null = Set-PodeCookie -Name 'pode.redirecturl' -Value $WebEvent.Request.Url.PathAndQuery + } +} + +function Get-PodeAuthRedirectUrl { + param( + [Parameter()] + [string] + $Url, + + [switch] + $UseOrigin + ) + + if (!$UseOrigin) { + return $Url + } + + $tmpUrl = Get-PodeCookieValue -Name 'pode.redirecturl' + Remove-PodeCookie -Name 'pode.redirecturl' + + if (![string]::IsNullOrWhiteSpace($tmpUrl)) { + $Url = $tmpUrl + } + + return $Url +} \ No newline at end of file diff --git a/my modified authentication - Public.ps1 b/my modified authentication - Public.ps1 new file mode 100644 index 000000000..2992520e2 --- /dev/null +++ b/my modified authentication - Public.ps1 @@ -0,0 +1,2690 @@ +<# +.SYNOPSIS +Create a new type of Authentication scheme. + +.DESCRIPTION +Create a new type of Authentication scheme, which is used to parse the Request for user credentials for validating. + +.PARAMETER Basic +If supplied, will use the inbuilt Basic Authentication credentials retriever. + +.PARAMETER Encoding +The Encoding to use when decoding the Basic Authorization header. + +.PARAMETER HeaderTag +The Tag name used in the Authorization header, ie: Basic, Bearer, Digest. + +.PARAMETER Form +If supplied, will use the inbuilt Form Authentication credentials retriever. + +.PARAMETER UsernameField +The name of the Username Field in the payload to retrieve the username. + +.PARAMETER PasswordField +The name of the Password Field in the payload to retrieve the password. + +.PARAMETER Custom +If supplied, will allow you to create a Custom Authentication credentials retriever. + +.PARAMETER ScriptBlock +The ScriptBlock is used to parse the request and retieve user credentials and other information. + +.PARAMETER ArgumentList +An array of arguments to supply to the Custom Authentication type's ScriptBlock. + +.PARAMETER Name +The Name of an Authentication type - such as Basic or NTLM. + +.PARAMETER Description +A short description for security scheme. CommonMark syntax MAY be used for rich text representation + +.PARAMETER Realm +The name of scope of the protected area. + +.PARAMETER Type +The scheme type for custom Authentication types. Default is HTTP. + +.PARAMETER Middleware +An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. + +.PARAMETER PostValidator +The PostValidator is a scriptblock that is invoked after user validation. + +.PARAMETER Digest +If supplied, will use the inbuilt Digest Authentication credentials retriever. + +.PARAMETER Bearer +If supplied, will use the inbuilt Bearer Authentication token retriever. + +.PARAMETER Algorithm: +The hashing algorithm used by Digest (e.g., MD5, SHA-256) default SHA-256. + +.PARAMETER ClientCertificate +If supplied, will use the inbuilt Client Certificate Authentication scheme. + +.PARAMETER ClientId +The Application ID generated when registering a new app for OAuth2. + +.PARAMETER ClientSecret +The Application Secret generated when registering a new app for OAuth2 (this is optional when using PKCE). + +.PARAMETER RedirectUrl +An optional OAuth2 Redirect URL (default: /oauth2/callback) + +.PARAMETER AuthoriseUrl +The OAuth2 Authorisation URL to authenticate a User. This is optional if you're using an InnerScheme like Basic/Form. + +.PARAMETER TokenUrl +The OAuth2 Token URL to acquire an access token. + +.PARAMETER UserUrl +An optional User profile URL to retrieve a user's details - for OAuth2 + +.PARAMETER UserUrlMethod +An optional HTTP method to use when calling the User profile URL - for OAuth2 (Default: Post) + +.PARAMETER CodeChallengeMethod +An optional method for sending a PKCE code challenge when calling the Authorise URL - for OAuth2 (Default: S256) + +.PARAMETER UsePKCE +If supplied, OAuth2 authentication will use PKCE code verifiers - for OAuth2 + +.PARAMETER OAuth2 +If supplied, will use the inbuilt OAuth2 Authentication scheme. + +.PARAMETER Scope +An optional array of Scopes for Bearer/OAuth2 Authentication. (These are case-sensitive) + +.PARAMETER ApiKey +If supplied, will use the inbuilt API key Authentication scheme. + +.PARAMETER Location +The Location to find an API key: Header, Query, or Cookie. (Default: Header) + +.PARAMETER LocationName +The Name of the Header, Query, or Cookie to find an API key. (Default depends on Location. Header/Cookie: X-API-KEY, Query: api_key) + +.PARAMETER InnerScheme +An optional authentication Scheme (from New-PodeAuthScheme) that will be called prior to this Scheme. + +.PARAMETER AsCredential +If supplied, username/password credentials for Basic/Form authentication will instead be supplied as a pscredential object. + +.PARAMETER AsJWT +If supplied, the token/key supplied for Bearer/API key authentication will be parsed as a JWT, and the payload supplied instead. + +.PARAMETER Secret +An optional Secret, used to sign/verify JWT signatures. + +.EXAMPLE +$basic_auth = New-PodeAuthScheme -Basic + +.EXAMPLE +$form_auth = New-PodeAuthScheme -Form -UsernameField 'Email' + +.EXAMPLE +$custom_auth = New-PodeAuthScheme -Custom -ScriptBlock { /* logic */ } +#> +function New-PodeAuthScheme { + [CmdletBinding(DefaultParameterSetName = 'Basic')] + [OutputType([hashtable])] + param( + [Parameter(ParameterSetName = 'Basic')] + [switch] + $Basic, + + [Parameter(ParameterSetName = 'Basic')] + [string] + $Encoding = 'ISO-8859-1', + + [Parameter(ParameterSetName = 'Basic')] + [Parameter(ParameterSetName = 'Bearer')] + [Parameter(ParameterSetName = 'Digest')] + [string] + $HeaderTag, + + [Parameter(ParameterSetName = 'Form')] + [switch] + $Form, + + [Parameter(ParameterSetName = 'Form')] + [string] + $UsernameField = 'username', + + [Parameter(ParameterSetName = 'Form')] + [string] + $PasswordField = 'password', + + [Parameter(ParameterSetName = 'Custom')] + [switch] + $Custom, + + [Parameter(Mandatory = $true, ParameterSetName = 'Custom')] + [ValidateScript({ + if (Test-PodeIsEmpty $_) { + # A non-empty ScriptBlock is required for the Custom authentication scheme + throw ($PodeLocale.nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage) + } + + return $true + })] + [scriptblock] + $ScriptBlock, + + [Parameter(ParameterSetName = 'Custom')] + [hashtable] + $ArgumentList, + + [Parameter(ParameterSetName = 'Custom')] + [string] + $Name, + + [string] + $Description, + + [Parameter()] + [string] + $Realm, + + [Parameter(ParameterSetName = 'Custom')] + [ValidateSet('ApiKey', 'Http', 'OAuth2', 'OpenIdConnect')] + [string] + $Type = 'Http', + + [Parameter()] + [object[]] + $Middleware, + + [Parameter(ParameterSetName = 'Custom')] + [scriptblock] + $PostValidator = $null, + + [Parameter(ParameterSetName = 'Digest')] + [switch] + $Digest, + + [Parameter(ParameterSetName = 'Bearer')] + [switch] + $Bearer, + + [Parameter(ParameterSetName = 'Digest')] + [string] + $Algorithm = 'MD5', + + [Parameter(ParameterSetName = 'ClientCertificate')] + [switch] + $ClientCertificate, + + [Parameter(ParameterSetName = 'OAuth2', Mandatory = $true)] + [string] + $ClientId, + + [Parameter(ParameterSetName = 'OAuth2')] + [string] + $ClientSecret, + + [Parameter(ParameterSetName = 'OAuth2')] + [string] + $RedirectUrl, + + [Parameter(ParameterSetName = 'OAuth2')] + [string] + $AuthoriseUrl, + + [Parameter(ParameterSetName = 'OAuth2', Mandatory = $true)] + [string] + $TokenUrl, + + [Parameter(ParameterSetName = 'OAuth2')] + [string] + $UserUrl, + + [Parameter(ParameterSetName = 'OAuth2')] + [ValidateSet('Get', 'Post')] + [string] + $UserUrlMethod = 'Post', + + [Parameter(ParameterSetName = 'OAuth2')] + [ValidateSet('plain', 'S256')] + [string] + $CodeChallengeMethod = 'S256', + + [Parameter(ParameterSetName = 'OAuth2')] + [switch] + $UsePKCE, + + [Parameter(ParameterSetName = 'OAuth2')] + [switch] + $OAuth2, + + [Parameter(ParameterSetName = 'ApiKey')] + [switch] + $ApiKey, + + [Parameter(ParameterSetName = 'ApiKey')] + [ValidateSet('Header', 'Query', 'Cookie')] + [string] + $Location = 'Header', + + [Parameter(ParameterSetName = 'ApiKey')] + [string] + $LocationName, + + [Parameter(ParameterSetName = 'Bearer')] + [Parameter(ParameterSetName = 'OAuth2')] + [string[]] + $Scope, + + [Parameter(ValueFromPipeline = $true)] + [hashtable] + $InnerScheme, + + [Parameter(ParameterSetName = 'Basic')] + [Parameter(ParameterSetName = 'Form')] + [switch] + $AsCredential, + + [Parameter(ParameterSetName = 'Bearer')] + [Parameter(ParameterSetName = 'ApiKey')] + [switch] + $AsJWT, + + [Parameter(ParameterSetName = 'Bearer')] + [Parameter(ParameterSetName = 'ApiKey')] + [string] + $Secret + ) + begin { + $pipelineItemCount = 0 + } + + process { + $pipelineItemCount++ + } + + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # default realm + $_realm = 'User' + + # convert any middleware into valid hashtables + $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState) + + # configure the auth scheme + switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { + 'basic' { + return @{ + Name = (Protect-PodeValue -Value $HeaderTag -Default 'Basic') + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthBasicType) + UsingVariables = $null + } + PostValidator = $null + Middleware = $Middleware + InnerScheme = $InnerScheme + Scheme = 'http' + Arguments = @{ + Description = $Description + HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Basic') + Encoding = (Protect-PodeValue -Value $Encoding -Default 'ISO-8859-1') + AsCredential = $AsCredential + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + } + } + } + + 'clientcertificate' { + return @{ + Name = 'Mutual' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthClientCertificateType) + UsingVariables = $null + } + PostValidator = $null + Middleware = $Middleware + InnerScheme = $InnerScheme + Scheme = 'http' + Arguments = @{ + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + } + } + } + + 'digest' { + return @{ + Name = 'Digest' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthDigestType) + UsingVariables = $null + } + PostValidator = @{ + Script = (Get-PodeAuthDigestPostValidator) + UsingVariables = $null + } + Middleware = $Middleware + InnerScheme = $InnerScheme + Scheme = 'http' + Arguments = @{ + HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Digest') + # Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + # Algorithm = (Protect-PodeValue -Value $Algorithm -Default 'MD5') + } + } + } + + 'bearer' { + $secretBytes = $null + if (![string]::IsNullOrWhiteSpace($Secret)) { + $secretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret) + } + + return @{ + Name = 'Bearer' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthBearerType) + UsingVariables = $null + } + PostValidator = @{ + Script = (Get-PodeAuthBearerPostValidator) + UsingVariables = $null + } + Middleware = $Middleware + Scheme = 'http' + InnerScheme = $InnerScheme + Arguments = @{ + Description = $Description + HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Bearer') + Scopes = $Scope + AsJWT = $AsJWT + Secret = $secretBytes + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + } + } + } + + 'form' { + return @{ + Name = 'Form' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthFormType) + UsingVariables = $null + } + PostValidator = $null + Middleware = $Middleware + InnerScheme = $InnerScheme + Scheme = 'http' + Arguments = @{ + Description = $Description + Fields = @{ + Username = (Protect-PodeValue -Value $UsernameField -Default 'username') + Password = (Protect-PodeValue -Value $PasswordField -Default 'password') + } + AsCredential = $AsCredential + } + } + } + + 'oauth2' { + if (($null -ne $InnerScheme) -and ($InnerScheme.Name -inotin @('basic', 'form'))) { + # OAuth2 InnerScheme can only be one of either Basic or Form authentication, but got: {0} + throw ($PodeLocale.oauth2InnerSchemeInvalidExceptionMessage -f $InnerScheme.Name) + } + + if (($null -eq $InnerScheme) -and [string]::IsNullOrWhiteSpace($AuthoriseUrl)) { + # OAuth2 requires an Authorise URL to be supplied + throw ($PodeLocale.oauth2RequiresAuthorizeUrlExceptionMessage) + } + + if ($UsePKCE -and !(Test-PodeSessionsEnabled)) { + # Sessions are required to use OAuth2 with PKCE + throw ($PodeLocale.sessionsRequiredForOAuth2WithPKCEExceptionMessage) + } + + if (!$UsePKCE -and [string]::IsNullOrEmpty($ClientSecret)) { + # OAuth2 requires a Client Secret when not using PKCE + throw ($PodeLocale.oauth2ClientSecretRequiredExceptionMessage) + } + return @{ + Name = 'OAuth2' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthOAuth2Type) + UsingVariables = $null + } + PostValidator = $null + Middleware = $Middleware + Scheme = 'oauth2' + InnerScheme = $InnerScheme + Arguments = @{ + Description = $Description + Scopes = $Scope + PKCE = @{ + Enabled = $UsePKCE + CodeChallenge = @{ + Method = $CodeChallengeMethod + } + } + Client = @{ + ID = $ClientId + Secret = $ClientSecret + } + Urls = @{ + Redirect = $RedirectUrl + Authorise = $AuthoriseUrl + Token = $TokenUrl + User = @{ + Url = $UserUrl + Method = (Protect-PodeValue -Value $UserUrlMethod -Default 'Post') + } + } + } + } + } + + 'apikey' { + # set default location name + if ([string]::IsNullOrWhiteSpace($LocationName)) { + $LocationName = (@{ + Header = 'X-API-KEY' + Query = 'api_key' + Cookie = 'X-API-KEY' + })[$Location] + } + + $secretBytes = $null + if (![string]::IsNullOrWhiteSpace($Secret)) { + $secretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret) + } + + return @{ + Name = 'ApiKey' + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + ScriptBlock = @{ + Script = (Get-PodeAuthApiKeyType) + UsingVariables = $null + } + PostValidator = $null + Middleware = $Middleware + InnerScheme = $InnerScheme + Scheme = 'apiKey' + Arguments = @{ + Description = $Description + Location = $Location + LocationName = $LocationName + AsJWT = $AsJWT + Secret = $secretBytes + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + } + } + } + + 'custom' { + $ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + + if ($null -ne $PostValidator) { + $PostValidator, $usingPostVars = Convert-PodeScopedVariables -ScriptBlock $PostValidator -PSSession $PSCmdlet.SessionState + } + + return @{ + Name = $Name + Realm = (Protect-PodeValue -Value $Realm -Default $_realm) + InnerScheme = $InnerScheme + Scheme = $Type.ToLowerInvariant() + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingScriptVars + } + PostValidator = @{ + Script = $PostValidator + UsingVariables = $usingPostVars + } + Middleware = $Middleware + Arguments = $ArgumentList + } + } + } + } +} + +<# +.SYNOPSIS +Create an OAuth2 auth scheme for Azure AD. + +.DESCRIPTION +A wrapper for New-PodeAuthScheme and OAuth2, which builds an OAuth2 scheme for Azure AD. + +.PARAMETER Tenant +The Directory/Tenant ID from registering a new app (default: common). + +.PARAMETER ClientId +The Client ID from registering a new app. + +.PARAMETER ClientSecret +The Client Secret from registering a new app (this is optional when using PKCE). + +.PARAMETER RedirectUrl +An optional OAuth2 Redirect URL (default: /oauth2/callback) + +.PARAMETER InnerScheme +An optional authentication Scheme (from New-PodeAuthScheme) that will be called prior to this Scheme. + +.PARAMETER Middleware +An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. + +.PARAMETER UsePKCE +If supplied, OAuth2 authentication will use PKCE code verifiers. + +.EXAMPLE +New-PodeAuthAzureADScheme -Tenant 123-456-678 -ClientId some_id -ClientSecret 1234.abc + +.EXAMPLE +New-PodeAuthAzureADScheme -Tenant 123-456-678 -ClientId some_id -UsePKCE +#> +function New-PodeAuthAzureADScheme { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $Tenant = 'common', + + [Parameter(Mandatory = $true)] + [string] + $ClientId, + + [Parameter()] + [string] + $ClientSecret, + + [Parameter()] + [string] + $RedirectUrl, + + [Parameter(ValueFromPipeline = $true)] + [hashtable] + $InnerScheme, + + [Parameter()] + [object[]] + $Middleware, + + [switch] + $UsePKCE + ) + begin { + $pipelineItemCount = 0 + } + + process { + + $pipelineItemCount++ + } + + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + return New-PodeAuthScheme ` + -OAuth2 ` + -ClientId $ClientId ` + -ClientSecret $ClientSecret ` + -AuthoriseUrl "https://login.microsoftonline.com/$($Tenant)/oauth2/v2.0/authorize" ` + -TokenUrl "https://login.microsoftonline.com/$($Tenant)/oauth2/v2.0/token" ` + -UserUrl 'https://graph.microsoft.com/oidc/userinfo' ` + -RedirectUrl $RedirectUrl ` + -InnerScheme $InnerScheme ` + -Middleware $Middleware ` + -UsePKCE:$UsePKCE + } +} + +<# +.SYNOPSIS +Create an OAuth2 auth scheme for Twitter. + +.DESCRIPTION +A wrapper for New-PodeAuthScheme and OAuth2, which builds an OAuth2 scheme for Twitter apps. + +.PARAMETER ClientId +The Client ID from registering a new app. + +.PARAMETER ClientSecret +The Client Secret from registering a new app (this is optional when using PKCE). + +.PARAMETER RedirectUrl +An optional OAuth2 Redirect URL (default: /oauth2/callback) + +.PARAMETER Middleware +An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. + +.PARAMETER UsePKCE +If supplied, OAuth2 authentication will use PKCE code verifiers. + +.EXAMPLE +New-PodeAuthTwitterScheme -ClientId some_id -ClientSecret 1234.abc + +.EXAMPLE +New-PodeAuthTwitterScheme -ClientId some_id -UsePKCE +#> +function New-PodeAuthTwitterScheme { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $ClientId, + + [Parameter()] + [string] + $ClientSecret, + + [Parameter()] + [string] + $RedirectUrl, + + [Parameter()] + [object[]] + $Middleware, + + [switch] + $UsePKCE + ) + + return New-PodeAuthScheme ` + -OAuth2 ` + -ClientId $ClientId ` + -ClientSecret $ClientSecret ` + -AuthoriseUrl 'https://twitter.com/i/oauth2/authorize' ` + -TokenUrl 'https://api.twitter.com/2/oauth2/token' ` + -UserUrl 'https://api.twitter.com/2/users/me' ` + -UserUrlMethod 'Get' ` + -RedirectUrl $RedirectUrl ` + -Middleware $Middleware ` + -Scope 'tweet.read', 'users.read' ` + -UsePKCE:$UsePKCE +} + +<# +.SYNOPSIS +Adds a custom Authentication method for verifying users. + +.DESCRIPTION +Adds a custom Authentication method for verifying users. + +.PARAMETER Name +A unique Name for the Authentication method. + +.PARAMETER Scheme +The authentication Scheme to use for retrieving credentials (From New-PodeAuthScheme). + +.PARAMETER ScriptBlock +The ScriptBlock defining logic that retrieves and verifys a user. + +.PARAMETER ArgumentList +An array of arguments to supply to the Custom Authentication's ScriptBlock. + +.PARAMETER FailureUrl +The URL to redirect to when authentication fails. + +.PARAMETER FailureMessage +An override Message to throw when authentication fails. + +.PARAMETER SuccessUrl +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 SuccessUseOrigin +If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. + +.EXAMPLE +New-PodeAuthScheme -Form | Add-PodeAuth -Name 'Main' -ScriptBlock { /* logic */ } +#> +function Add-PodeAuth { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [hashtable] + $Scheme, + + [Parameter(Mandatory = $true)] + [ValidateScript({ + if (Test-PodeIsEmpty $_) { + # A non-empty ScriptBlock is required for the authentication method + throw ($PodeLocale.nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage) + } + + return $true + })] + [scriptblock] + $ScriptBlock, + + [Parameter()] + [object[]] + $ArgumentList, + + [Parameter()] + [string] + $FailureUrl, + + [Parameter()] + [string] + $FailureMessage, + + [Parameter()] + [string] + $SuccessUrl, + + [switch] + $Sessionless, + + [switch] + $SuccessUseOrigin + ) + begin { + $pipelineItemCount = 0 + } + + process { + + $pipelineItemCount++ + } + + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # ensure the name doesn't already exist + if (Test-PodeAuthExists -Name $Name) { + # Authentication method already defined: {0} + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) + } + + # ensure the Scheme contains a scriptblock + if (Test-PodeIsEmpty $Scheme.ScriptBlock) { + # The supplied scheme for the '{0}' authentication validator requires a valid ScriptBlock + throw ($PodeLocale.schemeRequiresValidScriptBlockExceptionMessage -f $Name) + } + + # if we're using sessions, ensure sessions have been setup + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { + # Sessions are required to use session persistent authentication + throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) + } + + # check for scoped vars + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + + # add auth method to server + $PodeContext.Server.Authentications.Methods[$Name] = @{ + Name = $Name + Scheme = $Scheme + ScriptBlock = $ScriptBlock + UsingVariables = $usingVars + Arguments = $ArgumentList + Sessionless = $Sessionless.IsPresent + Failure = @{ + Url = $FailureUrl + Message = $FailureMessage + } + Success = @{ + Url = $SuccessUrl + UseOrigin = $SuccessUseOrigin.IsPresent + } + Cache = @{} + Merged = $false + Parent = $null + } + + # if the scheme is oauth2, and there's no redirect, set up a default one + if (($Scheme.Name -ieq 'oauth2') -and ($null -eq $Scheme.InnerScheme) -and [string]::IsNullOrWhiteSpace($Scheme.Arguments.Urls.Redirect)) { + $path = '/oauth2/callback' + $Scheme.Arguments.Urls.Redirect = $path + Add-PodeRoute -Method Get -Path $path -Authentication $Name + } + } +} + +<# +.SYNOPSIS +Lets you merge multiple Authentication methods together, into a "single" Authentication method. + +.DESCRIPTION +Lets you merge multiple Authentication methods together, into a "single" Authentication method. +You can specify if only One or All of the methods need to pass to allow access, and you can also +merge other merged Authentication methods for more advanced scenarios. + +.PARAMETER Name +A unique Name for the Authentication method. + +.PARAMETER Authentication +Multiple Autentication method Names to be merged. + +.PARAMETER Valid +How many of the Authentication methods are required to be valid, One or All. (Default: One) + +.PARAMETER ScriptBlock +This is mandatory, and only used, when $Valid=All. A scriptblock to merge the mutliple users/headers returned by valid authentications into 1 user/header objects. +This scriptblock will receive a hashtable of all result objects returned from Authentication methods. The key for the hashtable will be the authentication names that passed. + +.PARAMETER Default +The Default Authentication method to use as a fallback for Failure URLs and other settings. + +.PARAMETER MergeDefault +The Default Authentication method's User details result object to use, when $Valid=All. + +.PARAMETER FailureUrl +The URL to redirect to when authentication fails. +This will be used as fallback for the merged Authentication methods if not set on them. + +.PARAMETER FailureMessage +An override Message to throw when authentication fails. +This will be used as fallback for the merged Authentication methods if not set on them. + +.PARAMETER SuccessUrl +The URL to redirect to when authentication succeeds when logging in. +This will be used as fallback for the merged Authentication methods if not set on them. + +.PARAMETER Sessionless +If supplied, authenticated users will not be stored in sessions, and sessions will not be used. +This will be used as fallback for the merged Authentication methods if not set on them. + +.PARAMETER SuccessUseOrigin +If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. +This will be used as fallback for the merged Authentication methods if not set on them. + +.EXAMPLE +Merge-PodeAuth -Name MergedAuth -Authentication ApiTokenAuth, BasicAuth -Valid All -ScriptBlock { ... } + +.EXAMPLE +Merge-PodeAuth -Name MergedAuth -Authentication ApiTokenAuth, BasicAuth -Valid All -MergeDefault BasicAuth + +.EXAMPLE +Merge-PodeAuth -Name MergedAuth -Authentication ApiTokenAuth, BasicAuth -FailureUrl 'http://localhost:8080/login' +#> +function Merge-PodeAuth { + [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [Alias('Auth')] + [string[]] + $Authentication, + + [Parameter()] + [ValidateSet('One', 'All')] + [string] + $Valid = 'One', + + [Parameter(ParameterSetName = 'ScriptBlock')] + [scriptblock] + $ScriptBlock, + + [Parameter()] + [string] + $Default, + + [Parameter(ParameterSetName = 'MergeDefault')] + [string] + $MergeDefault, + + [Parameter()] + [string] + $FailureUrl, + + [Parameter()] + [string] + $FailureMessage, + + [Parameter()] + [string] + $SuccessUrl, + + [switch] + $Sessionless, + + [switch] + $SuccessUseOrigin + ) + + # ensure the name doesn't already exist + if (Test-PodeAuthExists -Name $Name) { + # Authentication method already defined: { 0 } + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) + } + + # ensure all the auth methods exist + foreach ($authName in $Authentication) { + if (!(Test-PodeAuthExists -Name $authName)) { + throw ($PodeLocale.authMethodNotExistForMergingExceptionMessage -f $authName) #"Authentication method does not exist for merging: $($authName)" + } + } + + # ensure the merge default is in the auth list + if (![string]::IsNullOrEmpty($MergeDefault) -and ($MergeDefault -inotin @($Authentication))) { + throw ($PodeLocale.mergeDefaultAuthNotInListExceptionMessage -f $MergeDefault) # "the MergeDefault Authentication '$($MergeDefault)' is not in the Authentication list supplied" + } + + # ensure the default is in the auth list + if (![string]::IsNullOrEmpty($Default) -and ($Default -inotin @($Authentication))) { + throw ($PodeLocale.defaultAuthNotInListExceptionMessage -f $Default) # "the Default Authentication '$($Default)' is not in the Authentication list supplied" + } + + # set default + if ([string]::IsNullOrEmpty($Default)) { + $Default = $Authentication[0] + } + + # get auth for default + $tmpAuth = $PodeContext.Server.Authentications.Methods[$Default] + + # check sessionless from default + if (!$Sessionless) { + $Sessionless = $tmpAuth.Sessionless + } + + # if we're using sessions, ensure sessions have been setup + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { + # Sessions are required to use session persistent authentication + throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) + } + + # check failure url from default + if ([string]::IsNullOrEmpty($FailureUrl)) { + $FailureUrl = $tmpAuth.Failure.Url + } + + # check failure message from default + if ([string]::IsNullOrEmpty($FailureMessage)) { + $FailureMessage = $tmpAuth.Failure.Message + } + + # check success url from default + if ([string]::IsNullOrEmpty($SuccessUrl)) { + $SuccessUrl = $tmpAuth.Success.Url + } + + # check success use origin from default + if (!$SuccessUseOrigin) { + $SuccessUseOrigin = $tmpAuth.Success.UseOrigin + } + + # deal with using vars in scriptblock + if (($Valid -ieq 'all') -and [string]::IsNullOrEmpty($MergeDefault)) { + if ($null -eq $ScriptBlock) { + # A Scriptblock for merging multiple authenticated users into 1 object is required When Valid is All + throw ($PodeLocale.scriptBlockRequiredForMergingUsersExceptionMessage) + } + + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + } + else { + if ($null -ne $ScriptBlock) { + Write-Warning -Message 'The Scriptblock for merged authentications, when Valid=One, will be ignored' + } + } + + # set parent auth + foreach ($authName in $Authentication) { + $PodeContext.Server.Authentications.Methods[$authName].Parent = $Name + } + + # add auth method to server + $PodeContext.Server.Authentications.Methods[$Name] = @{ + Name = $Name + Authentications = @($Authentication) + PassOne = ($Valid -ieq 'one') + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingVars + } + Default = $Default + MergeDefault = $MergeDefault + Sessionless = $Sessionless.IsPresent + Failure = @{ + Url = $FailureUrl + Message = $FailureMessage + } + Success = @{ + Url = $SuccessUrl + UseOrigin = $SuccessUseOrigin.IsPresent + } + Cache = @{} + Merged = $true + Parent = $null + } +} + +<# +.SYNOPSIS +Gets an Authentication method. + +.DESCRIPTION +Gets an Authentication method. + +.PARAMETER Name +The Name of an Authentication method. + +.EXAMPLE +Get-PodeAuth -Name 'Main' +#> +function Get-PodeAuth { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + # ensure the name exists + if (!(Test-PodeAuthExists -Name $Name)) { + throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Name) # "Authentication method not defined: $($Name)" + } + + # get auth method + return $PodeContext.Server.Authentications.Methods[$Name] +} + +<# +.SYNOPSIS +Test if an Authentication method exists. + +.DESCRIPTION +Test if an Authentication method exists. + +.PARAMETER Name +The Name of the Authentication method. + +.EXAMPLE +if (Test-PodeAuthExists -Name BasicAuth) { ... } +#> +function Test-PodeAuthExists { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + return $PodeContext.Server.Authentications.Methods.ContainsKey($Name) +} + +<# +.SYNOPSIS +Test and invoke an Authentication method to verify a user. + +.DESCRIPTION +Test and invoke an Authentication method to verify a user. This will verify a user's credentials on the request. +When testing OAuth2 methods, the first attempt will trigger a redirect to the provider and $false will be returned. + +.PARAMETER Name +The Name of the Authentication method. + +.PARAMETER IgnoreSession +If supplied, authentication will be re-verified on each call even if a valid session exists on the request. + +.EXAMPLE +if (Test-PodeAuth -Name 'BasicAuth') { ... } + +.EXAMPLE +if (Test-PodeAuth -Name 'FormAuth' -IgnoreSession) { ... } +#> +function Test-PodeAuth { + [CmdletBinding()] + [OutputType([boolean])] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [switch] + $IgnoreSession + ) + + if (! (Test-PodeAuthExists -Name $Name)) { + throw ($PodeLocale.authMethodDoesNotExistExceptionMessage -f $Name) + } + + # if the session already has a user/isAuth'd, then skip auth - or allow anon + if (!$IgnoreSession -and (Test-PodeSessionsInUse) -and (Test-PodeAuthUser)) { + return $true + } + + try { + $result = Invoke-PodeAuthValidation -Name $Name + } + catch { + $_ | Write-PodeErrorLog + return $false + } + + # did the auth force a redirect? + if ($result.Redirected) { + return $false + } + + # if auth failed, set appropriate response headers/redirects + if (!$result.Success) { + return $false + } + + # successful auth + return $true +} + +<# +.SYNOPSIS + Invokes an authentication method in Pode. + +.DESCRIPTION + This function attempts to invoke an authentication method by its name, + ensuring that it exists and has not been merged. If the authentication + method does not exist or is merged, it throws an exception. + +.PARAMETER Name + The name of the authentication method to invoke. This parameter is mandatory. + +.OUTPUTS + A hashtable containing the authentication result, including success status,user information, and headers. + +#> +function Invoke-PodeAuth { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + # Check if the authentication method exists + if (! (Test-PodeAuthExists -Name $Name)) { + # Authentication method doesn't exist: + throw ($PodeLocale.authMethodDoesNotExistExceptionMessage -f $Name) + } + + # Ensure the authentication method is not merged + if ($PodeContext.Server.Authentications.Methods[$Name].Merged) { + # Authentication method {0} is merged + throw ($PodeLocale.authenticationMethodMergedExceptionMessage -f $Name) + } + try { + # Perform authentication validation + $WebEvent.Auth = Test-PodeAuthValidation -Name $Name -NoMiddlewareAuthentication + Add-PodeHeader -Name 'WWW-Authenticate' -Value $WebEvent.Auth.Headers['WWW-Authenticate'] + } + catch { + $_ | Write-PodeErrorLog + } + + return $WebEvent.Auth +} + + + +<# +.SYNOPSIS +Adds the inbuilt Windows AD Authentication method for verifying users. + +.DESCRIPTION +Adds the inbuilt Windows AD Authentication method for verifying users. + +.PARAMETER Name +A unique Name for the Authentication method. + +.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) + +.PARAMETER Domain +(Unix Only) A custom NetBIOS domain name that is prepended onto usernames that are missing it (\). + +.PARAMETER SearchBase +(Unix Only) An optional searchbase to refine the LDAP query. This should be the full distinguished name. + +.PARAMETER Groups +An array of Group names to only allow access. + +.PARAMETER Users +An array of Usernames to only allow access. + +.PARAMETER FailureUrl +The URL to redirect to when authentication fails. + +.PARAMETER FailureMessage +An override Message to throw when authentication fails. + +.PARAMETER SuccessUrl +The URL to redirect to when authentication succeeds when logging in. + +.PARAMETER ScriptBlock +Optional ScriptBlock that is passed the found user object for further validation. + +.PARAMETER Sessionless +If supplied, authenticated users will not be stored in sessions, and sessions will not be used. + +.PARAMETER NoGroups +If supplied, groups will not be retrieved for the user in AD. + +.PARAMETER DirectGroups +If supplied, only a user's direct groups will be retrieved rather than all groups recursively. + +.PARAMETER OpenLDAP +If supplied, and on Windows, OpenLDAP will be used instead (this is the default for Linux/MacOS). + +.PARAMETER ADModule +If supplied, and on Windows, the ActiveDirectory module will be used instead. + +.PARAMETER SuccessUseOrigin +If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. + +.PARAMETER KeepCredential +If suplied pode will save the AD credential as a PSCredential object in $WebEvent.Auth.User.Credential + +.EXAMPLE +New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'WinAuth' + +.EXAMPLE +New-PodeAuthScheme -Basic | Add-PodeAuthWindowsAd -Name 'WinAuth' -Groups @('Developers') + +.EXAMPLE +New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'WinAuth' -NoGroups + +.EXAMPLE +New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'UnixAuth' -Server 'testdomain.company.com' -Domain 'testdomain' +#> +function Add-PodeAuthWindowsAd { + [CmdletBinding(DefaultParameterSetName = 'Groups')] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [hashtable] + $Scheme, + + [Parameter()] + [Alias('Server')] + [string] + $Fqdn, + + [Parameter()] + [string] + $Domain, + + [Parameter()] + [string] + $SearchBase, + + [Parameter(ParameterSetName = 'Groups')] + [string[]] + $Groups, + + [Parameter()] + [string[]] + $Users, + + [Parameter()] + [string] + $FailureUrl, + + [Parameter()] + [string] + $FailureMessage, + + [Parameter()] + [string] + $SuccessUrl, + + [Parameter()] + [scriptblock] + $ScriptBlock, + + [switch] + $Sessionless, + + [Parameter(ParameterSetName = 'NoGroups')] + [switch] + $NoGroups, + + [Parameter(ParameterSetName = 'Groups')] + [switch] + $DirectGroups, + + [switch] + $OpenLDAP, + + [switch] + $ADModule, + + [switch] + $SuccessUseOrigin, + + [switch] + $KeepCredential + ) + begin { + $pipelineItemCount = 0 + } + + process { + + $pipelineItemCount++ + } + + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # ensure the name doesn't already exist + if (Test-PodeAuthExists -Name $Name) { + # Authentication method already defined: {0} + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) + } + + # ensure the Scheme contains a scriptblock + if (Test-PodeIsEmpty $Scheme.ScriptBlock) { + # The supplied Scheme for the '$($Name)' Windows AD authentication validator requires a valid ScriptBlock + throw ($PodeLocale.schemeRequiresValidScriptBlockExceptionMessage -f $Name) + } + + # if we're using sessions, ensure sessions have been setup + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { + # Sessions are required to use session persistent authentication + throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) + } + + # if AD module set, ensure we're on windows and the module is available, then import/export it + if ($ADModule) { + Import-PodeAuthADModule + } + + # set server name if not passed + if ([string]::IsNullOrWhiteSpace($Fqdn)) { + $Fqdn = Get-PodeAuthDomainName + + if ([string]::IsNullOrWhiteSpace($Fqdn)) { + # No domain server name has been supplied for Windows AD authentication + throw ($PodeLocale.noDomainServerNameForWindowsAdAuthExceptionMessage) + } + } + + # set the domain if not passed + if ([string]::IsNullOrWhiteSpace($Domain)) { + $Domain = ($Fqdn -split '\.')[0] + } + + # if we have a scriptblock, deal with using vars + if ($null -ne $ScriptBlock) { + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + } + + # add Windows AD auth method to server + $PodeContext.Server.Authentications.Methods[$Name] = @{ + Name = $Name + Scheme = $Scheme + ScriptBlock = (Get-PodeAuthWindowsADMethod) + Arguments = @{ + Server = $Fqdn + Domain = $Domain + SearchBase = $SearchBase + Users = $Users + Groups = $Groups + NoGroups = $NoGroups + DirectGroups = $DirectGroups + KeepCredential = $KeepCredential + Provider = (Get-PodeAuthADProvider -OpenLDAP:$OpenLDAP -ADModule:$ADModule) + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingVars + } + } + Sessionless = $Sessionless + Failure = @{ + Url = $FailureUrl + Message = $FailureMessage + } + Success = @{ + Url = $SuccessUrl + UseOrigin = $SuccessUseOrigin + } + Cache = @{} + Merged = $false + Parent = $null + } + } +} + +<# +.SYNOPSIS +Adds the inbuilt Session Authentication method for verifying an authenticated session is present on Requests. + +.DESCRIPTION +Adds the inbuilt Session Authentication method for verifying an authenticated session is present on Requests. + +.PARAMETER Name +A unique Name for the Authentication method. + +.PARAMETER FailureUrl +The URL to redirect to when authentication fails. + +.PARAMETER FailureMessage +An override Message to throw when authentication fails. + +.PARAMETER SuccessUrl +The URL to redirect to when authentication succeeds when logging in. + +.PARAMETER ScriptBlock +Optional ScriptBlock that is passed the found user object for further validation. + +.PARAMETER Middleware +An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. + +.PARAMETER SuccessUseOrigin +If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. + +.EXAMPLE +Add-PodeAuthSession -Name 'SessionAuth' -FailureUrl '/login' +#> +function Add-PodeAuthSession { + [CmdletBinding(DefaultParameterSetName = 'Groups')] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [string] + $FailureUrl, + + [Parameter()] + [string] + $FailureMessage, + + [Parameter()] + [string] + $SuccessUrl, + + [Parameter()] + [scriptblock] + $ScriptBlock, + + [Parameter()] + [object[]] + $Middleware, + + [switch] + $SuccessUseOrigin + ) + + # if sessions haven't been setup, error + if (!(Test-PodeSessionsEnabled)) { + # Sessions have not been configured + throw ($PodeLocale.sessionsNotConfiguredExceptionMessage) + } + + # ensure the name doesn't already exist + if (Test-PodeAuthExists -Name $Name) { + # Authentication method already defined: { 0 } + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) + } + + # if we have a scriptblock, deal with using vars + if ($null -ne $ScriptBlock) { + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + } + + # create the auth scheme for getting the session + $scheme = New-PodeAuthScheme -Custom -Middleware $Middleware -ScriptBlock { + param($options) + + # 401 if sessions not used + if (!(Test-PodeSessionsInUse)) { + Revoke-PodeSession + return @{ + Message = 'Sessions are not being used' + Code = 401 + } + } + + # 401 if no authenticated user + if (!(Test-PodeAuthUser)) { + Revoke-PodeSession + return @{ + Message = 'Session not authenticated' + Code = 401 + } + } + + # return user + return @($WebEvent.Session.Data.Auth) + } + + # add a custom auth method to return user back + $method = { + param($user, $options) + $result = @{ User = $user } + + # call additional scriptblock if supplied + if ($null -ne $options.ScriptBlock.Script) { + $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $options.ScriptBlock.Script -UsingVariables $options.ScriptBlock.UsingVariables + } + + # return user back + return $result + } + + $scheme | Add-PodeAuth ` + -Name $Name ` + -ScriptBlock $method ` + -FailureUrl $FailureUrl ` + -FailureMessage $FailureMessage ` + -SuccessUrl $SuccessUrl ` + -SuccessUseOrigin:$SuccessUseOrigin ` + -ArgumentList @{ + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingVars + } + } +} + +<# +.SYNOPSIS +Remove a specific Authentication method. + +.DESCRIPTION +Remove a specific Authentication method. + +.PARAMETER Name +The Name of the Authentication method. + +.EXAMPLE +Remove-PodeAuth -Name 'Login' +#> +function Remove-PodeAuth { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string] + $Name + ) + process { + $null = $PodeContext.Server.Authentications.Methods.Remove($Name) + } +} + +<# +.SYNOPSIS +Clear all defined Authentication methods. + +.DESCRIPTION +Clear all defined Authentication methods. + +.EXAMPLE +Clear-PodeAuth +#> +function Clear-PodeAuth { + [CmdletBinding()] + param() + + $PodeContext.Server.Authentications.Methods.Clear() +} + +<# +.SYNOPSIS +Adds an authentication method as global middleware. + +.DESCRIPTION +Adds an authentication method as global middleware. + +.PARAMETER Name +The Name of the Middleware. + +.PARAMETER Authentication +The Name of the Authentication method to use. + +.PARAMETER Route +A Route path for which Routes this Middleware should only be invoked against. + +.PARAMETER OADefinitionTag +An array of string representing the unique tag for the API specification. +This tag helps in distinguishing between different versions or types of API specifications within the application. +Use this tag to reference the specific API documentation, schema, or version that your function interacts with. + +.EXAMPLE +Add-PodeAuthMiddleware -Name 'GlobalAuth' -Authentication AuthName + +.EXAMPLE +Add-PodeAuthMiddleware -Name 'GlobalAuth' -Authentication AuthName -Route '/api/*' +#> +function Add-PodeAuthMiddleware { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [Alias('Auth')] + [string] + $Authentication, + + [Parameter()] + [string] + $Route, + + [string[]] + $OADefinitionTag + ) + + $DefinitionTag = Test-PodeOADefinitionTag -Tag $OADefinitionTag + + if (!(Test-PodeAuthExists -Name $Authentication)) { + throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Authentication) # "Authentication method does not exist: $($Authentication)" + } + + Get-PodeAuthMiddlewareScript | + New-PodeMiddleware -ArgumentList @{ Name = $Authentication } | + Add-PodeMiddleware -Name $Name -Route $Route + + Set-PodeOAGlobalAuth -DefinitionTag $DefinitionTag -Name $Authentication -Route $Route +} + +<# +.SYNOPSIS +Adds the inbuilt IIS Authentication method for verifying users passed to Pode from IIS. + +.DESCRIPTION +Adds the inbuilt IIS Authentication method for verifying users passed to Pode from IIS. + +.PARAMETER Name +A unique Name for the Authentication method. + +.PARAMETER Groups +An array of Group names to only allow access. + +.PARAMETER Users +An array of Usernames to only allow access. + +.PARAMETER FailureUrl +The URL to redirect to when authentication fails. + +.PARAMETER FailureMessage +An override Message to throw when authentication fails. + +.PARAMETER SuccessUrl +The URL to redirect to when authentication succeeds when logging in. + +.PARAMETER ScriptBlock +Optional ScriptBlock that is passed the found user object for further validation. + +.PARAMETER Middleware +An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. + +.PARAMETER Sessionless +If supplied, authenticated users will not be stored in sessions, and sessions will not be used. + +.PARAMETER NoGroups +If supplied, groups will not be retrieved for the user in AD. + +.PARAMETER DirectGroups +If supplied, only a user's direct groups will be retrieved rather than all groups recursively. + +.PARAMETER ADModule +If supplied, and on Windows, the ActiveDirectory module will be used instead. + +.PARAMETER NoLocalCheck +If supplied, Pode will not at attempt to retrieve local User/Group information for the authenticated user. + +.PARAMETER SuccessUseOrigin +If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. + +.EXAMPLE +Add-PodeAuthIIS -Name 'IISAuth' + +.EXAMPLE +Add-PodeAuthIIS -Name 'IISAuth' -Groups @('Developers') + +.EXAMPLE +Add-PodeAuthIIS -Name 'IISAuth' -NoGroups +#> +function Add-PodeAuthIIS { + [CmdletBinding(DefaultParameterSetName = 'Groups')] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(ParameterSetName = 'Groups')] + [string[]] + $Groups, + + [Parameter()] + [string[]] + $Users, + + [Parameter()] + [string] + $FailureUrl, + + [Parameter()] + [string] + $FailureMessage, + + [Parameter()] + [string] + $SuccessUrl, + + [Parameter()] + [scriptblock] + $ScriptBlock, + + [Parameter()] + [object[]] + $Middleware, + + [switch] + $Sessionless, + + [Parameter(ParameterSetName = 'NoGroups')] + [switch] + $NoGroups, + + [Parameter(ParameterSetName = 'Groups')] + [switch] + $DirectGroups, + + [switch] + $ADModule, + + [switch] + $NoLocalCheck, + + [switch] + $SuccessUseOrigin + ) + + # ensure we're on Windows! + if (!(Test-PodeIsWindows)) { + # IIS Authentication support is for Windows only + throw ($PodeLocale.iisAuthSupportIsForWindowsOnlyExceptionMessage) + } + + # ensure the name doesn't already exist + if (Test-PodeAuthExists -Name $Name) { + # Authentication method already defined: {0} + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) + } + + # if AD module set, ensure we're on windows and the module is available, then import/export it + if ($ADModule) { + Import-PodeAuthADModule + } + + # if we have a scriptblock, deal with using vars + if ($null -ne $ScriptBlock) { + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + } + + # create the auth scheme for getting the token header + $scheme = New-PodeAuthScheme -Custom -Middleware $Middleware -ScriptBlock { + param($options) + + $header = 'MS-ASPNETCORE-WINAUTHTOKEN' + + # fail if no header + if (!(Test-PodeHeader -Name $header)) { + return @{ + Message = "No $($header) header found" + Code = 401 + } + } + + # return the header for validation + $token = Get-PodeHeader -Name $header + return @($token) + } + + # add a custom auth method to validate the user + $method = Get-PodeAuthWindowsADIISMethod + + $scheme | Add-PodeAuth ` + -Name $Name ` + -ScriptBlock $method ` + -FailureUrl $FailureUrl ` + -FailureMessage $FailureMessage ` + -SuccessUrl $SuccessUrl ` + -Sessionless:$Sessionless ` + -SuccessUseOrigin:$SuccessUseOrigin ` + -ArgumentList @{ + Users = $Users + Groups = $Groups + NoGroups = $NoGroups + DirectGroups = $DirectGroups + Provider = (Get-PodeAuthADProvider -ADModule:$ADModule) + NoLocalCheck = $NoLocalCheck + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingVars + } + } +} + +<# +.SYNOPSIS +Adds the inbuilt User File Authentication method for verifying users. + +.DESCRIPTION +Adds the inbuilt User File Authentication method for verifying users. + +.PARAMETER Name +A unique Name for the Authentication method. + +.PARAMETER Scheme +The Scheme to use for retrieving credentials (From New-PodeAuthScheme). + +.PARAMETER FilePath +A path to a users JSON file (Default: ./users.json) + +.PARAMETER Groups +An array of Group names to only allow access. + +.PARAMETER Users +An array of Usernames to only allow access. + +.PARAMETER HmacSecret +An optional secret if the passwords are HMAC SHA256 hashed. + +.PARAMETER FailureUrl +The URL to redirect to when authentication fails. + +.PARAMETER FailureMessage +An override Message to throw when authentication fails. + +.PARAMETER SuccessUrl +The URL to redirect to when authentication succeeds when logging in. + +.PARAMETER ScriptBlock +Optional ScriptBlock that is passed the found user object for further validation. + +.PARAMETER Sessionless +If supplied, authenticated users will not be stored in sessions, and sessions will not be used. + +.PARAMETER SuccessUseOrigin +If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. + +.EXAMPLE +New-PodeAuthScheme -Form | Add-PodeAuthUserFile -Name 'Login' + +.EXAMPLE +New-PodeAuthScheme -Form | Add-PodeAuthUserFile -Name 'Login' -FilePath './custom/path/users.json' +#> +function Add-PodeAuthUserFile { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [hashtable] + $Scheme, + + [Parameter()] + [string] + $FilePath, + + [Parameter()] + [string[]] + $Groups, + + [Parameter()] + [string[]] + $Users, + + [Parameter(ParameterSetName = 'Hmac')] + [string] + $HmacSecret, + + [Parameter()] + [string] + $FailureUrl, + + [Parameter()] + [string] + $FailureMessage, + + [Parameter()] + [string] + $SuccessUrl, + + [Parameter()] + [scriptblock] + $ScriptBlock, + + [switch] + $Sessionless, + + [switch] + $SuccessUseOrigin + ) + begin { + $pipelineItemCount = 0 + } + + process { + + $pipelineItemCount++ + } + + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # ensure the name doesn't already exist + if (Test-PodeAuthExists -Name $Name) { + # Authentication method already defined: {0} + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) + } + + # ensure the Scheme contains a scriptblock + if (Test-PodeIsEmpty $Scheme.ScriptBlock) { + # The supplied scheme for the '{0}' authentication validator requires a valid ScriptBlock. + throw ($PodeLocale.schemeRequiresValidScriptBlockExceptionMessage -f $Name) + } + + # if we're using sessions, ensure sessions have been setup + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { + # Sessions are required to use session persistent authentication + throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) + } + + # set the file path if not passed + if ([string]::IsNullOrWhiteSpace($FilePath)) { + $FilePath = Join-PodeServerRoot -Folder '.' -FilePath 'users.json' + } + else { + $FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot -Resolve + } + + # ensure the user file exists + if (!(Test-PodePath -Path $FilePath -NoStatus -FailOnDirectory)) { + # The user file does not exist: {0} + throw ($PodeLocale.userFileDoesNotExistExceptionMessage -f $FilePath) + } + + # if we have a scriptblock, deal with using vars + if ($null -ne $ScriptBlock) { + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + } + + # add Windows AD auth method to server + $PodeContext.Server.Authentications.Methods[$Name] = @{ + Name = $Name + Scheme = $Scheme + ScriptBlock = (Get-PodeAuthUserFileMethod) + Arguments = @{ + FilePath = $FilePath + Users = $Users + Groups = $Groups + HmacSecret = $HmacSecret + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingVars + } + } + Sessionless = $Sessionless + Failure = @{ + Url = $FailureUrl + Message = $FailureMessage + } + Success = @{ + Url = $SuccessUrl + UseOrigin = $SuccessUseOrigin + } + Cache = @{} + Merged = $false + Parent = $null + } + } +} + +<# +.SYNOPSIS +Adds the inbuilt Windows Local User Authentication method for verifying users. + +.DESCRIPTION +Adds the inbuilt Windows Local User Authentication method for verifying users. + +.PARAMETER Name +A unique Name for the Authentication method. + +.PARAMETER Scheme +The Scheme to use for retrieving credentials (From New-PodeAuthScheme). + +.PARAMETER Groups +An array of Group names to only allow access. + +.PARAMETER Users +An array of Usernames to only allow access. + +.PARAMETER FailureUrl +The URL to redirect to when authentication fails. + +.PARAMETER FailureMessage +An override Message to throw when authentication fails. + +.PARAMETER SuccessUrl +The URL to redirect to when authentication succeeds when logging in. + +.PARAMETER ScriptBlock +Optional ScriptBlock that is passed the found user object for further validation. + +.PARAMETER Sessionless +If supplied, authenticated users will not be stored in sessions, and sessions will not be used. + +.PARAMETER NoGroups +If supplied, groups will not be retrieved for the user. + +.PARAMETER SuccessUseOrigin +If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. + +.EXAMPLE +New-PodeAuthScheme -Form | Add-PodeAuthWindowsLocal -Name 'WinAuth' + +.EXAMPLE +New-PodeAuthScheme -Basic | Add-PodeAuthWindowsLocal -Name 'WinAuth' -Groups @('Developers') + +.EXAMPLE +New-PodeAuthScheme -Form | Add-PodeAuthWindowsLocal -Name 'WinAuth' -NoGroups +#> +function Add-PodeAuthWindowsLocal { + [CmdletBinding(DefaultParameterSetName = 'Groups')] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [hashtable] + $Scheme, + + [Parameter(ParameterSetName = 'Groups')] + [string[]] + $Groups, + + [Parameter()] + [string[]] + $Users, + + [Parameter()] + [string] + $FailureUrl, + + [Parameter()] + [string] + $FailureMessage, + + [Parameter()] + [string] + $SuccessUrl, + + [Parameter()] + [scriptblock] + $ScriptBlock, + + [switch] + $Sessionless, + + [Parameter(ParameterSetName = 'NoGroups')] + [switch] + $NoGroups, + + [switch] + $SuccessUseOrigin + ) + begin { + $pipelineItemCount = 0 + } + + process { + + $pipelineItemCount++ + } + + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # ensure we're on Windows! + if (!(Test-PodeIsWindows)) { + # Windows Local Authentication support is for Windows only + throw ($PodeLocale.windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage) + } + + # ensure the name doesn't already exist + if (Test-PodeAuthExists -Name $Name) { + # Authentication method already defined: {0} + throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) + } + + # ensure the Scheme contains a scriptblock + if (Test-PodeIsEmpty $Scheme.ScriptBlock) { + # The supplied scheme for the '{0}' authentication validator requires a valid ScriptBlock. + throw ($PodeLocale.schemeRequiresValidScriptBlockExceptionMessage -f $Name) + } + + # if we're using sessions, ensure sessions have been setup + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { + # Sessions are required to use session persistent authentication + throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) + } + + # if we have a scriptblock, deal with using vars + if ($null -ne $ScriptBlock) { + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + } + + # add Windows Local auth method to server + $PodeContext.Server.Authentications.Methods[$Name] = @{ + Name = $Name + Scheme = $Scheme + ScriptBlock = (Get-PodeAuthWindowsLocalMethod) + Arguments = @{ + Users = $Users + Groups = $Groups + NoGroups = $NoGroups + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingVars + } + } + Sessionless = $Sessionless + Failure = @{ + Url = $FailureUrl + Message = $FailureMessage + } + Success = @{ + Url = $SuccessUrl + UseOrigin = $SuccessUseOrigin + } + Cache = @{} + Merged = $false + Parent = $null + } + } +} + +<# +.SYNOPSIS +Convert a Header/Payload into a JWT. + +.DESCRIPTION +Convert a Header/Payload hashtable into a JWT, with the option to sign it. + +.PARAMETER Header +A Hashtable containing the Header information for the JWT. + +.PARAMETER Payload +A Hashtable containing the Payload information for the JWT. + +.PARAMETER Secret +An Optional Secret for signing the JWT, should be a string or byte[]. This is mandatory if the Header algorithm isn't "none". + +.EXAMPLE +ConvertTo-PodeJwt -Header @{ alg = 'none' } -Payload @{ sub = '123'; name = 'John' } + +.EXAMPLE +ConvertTo-PodeJwt -Header @{ alg = 'hs256' } -Payload @{ sub = '123'; name = 'John' } -Secret 'abc' +#> +function ConvertTo-PodeJwt { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + [hashtable] + $Header, + + [Parameter(Mandatory = $true)] + [hashtable] + $Payload, + + [Parameter()] + $Secret = $null + ) + + # validate header + if ([string]::IsNullOrWhiteSpace($Header.alg)) { + # No algorithm supplied in JWT Header + throw ($PodeLocale.noAlgorithmInJwtHeaderExceptionMessage) + } + + # convert the header + $header64 = ConvertTo-PodeBase64UrlValue -Value ($Header | ConvertTo-Json -Compress) + + # convert the payload + $payload64 = ConvertTo-PodeBase64UrlValue -Value ($Payload | ConvertTo-Json -Compress) + + # combine + $jwt = "$($header64).$($payload64)" + + # convert secret to bytes + if (($null -ne $Secret) -and ($Secret -isnot [byte[]])) { + $Secret = [System.Text.Encoding]::UTF8.GetBytes([string]$Secret) + } + + # make the signature + $sig = New-PodeJwtSignature -Algorithm $Header.alg -Token $jwt -SecretBytes $Secret + + # add the signature and return + $jwt += ".$($sig)" + return $jwt +} + +<# +.SYNOPSIS +Convert and return the payload of a JWT token. + +.DESCRIPTION +Convert and return the payload of a JWT token, verifying the signature by default with support to ignore the signature. + +.PARAMETER Token +The JWT token. + +.PARAMETER Secret +The Secret, as a string or byte[], to verify the token's signature. + +.PARAMETER IgnoreSignature +Skip signature verification, and return the decoded payload. + +.EXAMPLE +ConvertFrom-PodeJwt -Token "eyJ0eXAiOiJKV1QiLCJhbGciOiJoczI1NiJ9.eyJleHAiOjE2MjI1NTMyMTQsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMyJ9.LP-O8OKwix91a-SZwVK35gEClLZQmsORbW0un2Z4RkY" +#> +function ConvertFrom-PodeJwt { + [CmdletBinding(DefaultParameterSetName = 'Secret')] + [OutputType([pscustomobject])] + param( + [Parameter(Mandatory = $true)] + [string] + $Token, + + [Parameter(ParameterSetName = 'Signed')] + $Secret = $null, + + [Parameter(ParameterSetName = 'Ignore')] + [switch] + $IgnoreSignature + ) + + # get the parts + $parts = ($Token -isplit '\.') + + # check number of parts (should be 3) + if ($parts.Length -ne 3) { + # Invalid JWT supplied + throw ($PodeLocale.invalidJwtSuppliedExceptionMessage) + } + + # convert to header + $header = ConvertFrom-PodeJwtBase64Value -Value $parts[0] + if ([string]::IsNullOrWhiteSpace($header.alg)) { + # Invalid JWT header algorithm supplied + throw ($PodeLocale.invalidJwtHeaderAlgorithmSuppliedExceptionMessage) + } + + # convert to payload + $payload = ConvertFrom-PodeJwtBase64Value -Value $parts[1] + + # get signature + if ($IgnoreSignature) { + return $payload + } + + $signature = $parts[2] + + # check "none" signature, and return payload if no signature + $isNoneAlg = ($header.alg -ieq 'none') + + if ([string]::IsNullOrWhiteSpace($signature) -and !$isNoneAlg) { + # No JWT signature supplied for {0} + throw ($PodeLocale.noJwtSignatureForAlgorithmExceptionMessage -f $header.alg) + } + + if (![string]::IsNullOrWhiteSpace($signature) -and $isNoneAlg) { + # Expected no JWT signature to be supplied + throw ($PodeLocale.expectedNoJwtSignatureSuppliedExceptionMessage) + } + + if ($isNoneAlg -and ($null -ne $Secret) -and ($Secret.Length -gt 0)) { + # Expected no JWT signature to be supplied + throw ($PodeLocale.expectedNoJwtSignatureSuppliedExceptionMessage) + } + + if ($isNoneAlg) { + return $payload + } + + # otherwise, we have an alg for the signature, so we need to validate it + if (($null -ne $Secret) -and ($Secret -isnot [byte[]])) { + $Secret = [System.Text.Encoding]::UTF8.GetBytes([string]$Secret) + } + + $sig = "$($parts[0]).$($parts[1])" + $sig = New-PodeJwtSignature -Algorithm $header.alg -Token $sig -SecretBytes $Secret + + if ($sig -ne $parts[2]) { + # Invalid JWT signature supplied + throw ($PodeLocale.invalidJwtSignatureSuppliedExceptionMessage) + } + + # it's valid return the payload! + return $payload +} + +<# +.SYNOPSIS +Validates JSON Web Tokens (JWT) claims. + +.DESCRIPTION +Validates JSON Web Tokens (JWT) claims. Checks time related claims: 'exp' and 'nbf'. + +.PARAMETER Payload +Object containing JWT claims. Some of them are: + - exp (expiration time) + - nbf (not before) + +.EXAMPLE +Test-PodeJwt @{exp = 2696258821 } + +.EXAMPLE +Test-PodeJwt -Payload @{nbf = 1696258821 } +#> +function Test-PodeJwt { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [pscustomobject] + $Payload + ) + + $now = [datetime]::UtcNow + $unixStart = [datetime]::new(1970, 1, 1, 0, 0, [DateTimeKind]::Utc) + + # validate expiry + if (![string]::IsNullOrWhiteSpace($Payload.exp)) { + if ($now -gt $unixStart.AddSeconds($Payload.exp)) { + # The JWT has expired + throw ($PodeLocale.jwtExpiredExceptionMessage) + } + } + + # validate not-before + if (![string]::IsNullOrWhiteSpace($Payload.nbf)) { + if ($now -lt $unixStart.AddSeconds($Payload.nbf)) { + # The JWT is not yet valid for use + throw ($PodeLocale.jwtNotYetValidExceptionMessage) + } + } +} + +<# +.SYNOPSIS +Automatically loads auth ps1 files + +.DESCRIPTION +Automatically loads auth ps1 files from either a /auth folder, or a custom folder. Saves space dot-sourcing them all one-by-one. + +.PARAMETER Path +Optional Path to a folder containing ps1 files, can be relative or literal. + +.EXAMPLE +Use-PodeAuth + +.EXAMPLE +Use-PodeAuth -Path './my-auth' +#> +function Use-PodeAuth { + [CmdletBinding()] + param( + [Parameter()] + [string] + $Path + ) + + Use-PodeFolder -Path $Path -DefaultPath 'auth' +} + +<# +.SYNOPSIS +Builds an OAuth2 scheme using an OpenID Connect Discovery URL. + +.DESCRIPTION +Builds an OAuth2 scheme using an OpenID Connect Discovery URL. + +.PARAMETER Url +The OpenID Connect Discovery URL, this must end with '/.well-known/openid-configuration' (if missing, it will be automatically appended). + +.PARAMETER Scope +A list of optional Scopes to use during the OAuth2 request. (Default: the supported list returned) + +.PARAMETER ClientId +The Client ID from registering a new app. + +.PARAMETER ClientSecret +The Client Secret from registering a new app (this is optional when using PKCE). + +.PARAMETER RedirectUrl +An optional OAuth2 Redirect URL (Default: /oauth2/callback) + +.PARAMETER InnerScheme +An optional authentication Scheme (from New-PodeAuthScheme) that will be called prior to this Scheme. + +.PARAMETER Middleware +An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. + +.PARAMETER UsePKCE +If supplied, OAuth2 authentication will use PKCE code verifiers. + +.EXAMPLE +ConvertFrom-PodeOIDCDiscovery -Url 'https://accounts.google.com/.well-known/openid-configuration' -ClientId some_id -UsePKCE + +.EXAMPLE +ConvertFrom-PodeOIDCDiscovery -Url 'https://accounts.google.com' -ClientId some_id -UsePKCE +#> +function ConvertFrom-PodeOIDCDiscovery { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Url, + + [Parameter()] + [string[]] + $Scope, + + [Parameter(Mandatory = $true)] + [string] + $ClientId, + + [Parameter()] + [string] + $ClientSecret, + + [Parameter()] + [string] + $RedirectUrl, + + [Parameter(ValueFromPipeline = $true)] + [hashtable] + $InnerScheme, + + [Parameter()] + [object[]] + $Middleware, + + [switch] + $UsePKCE + ) + begin { + $pipelineItemCount = 0 + } + + process { + + $pipelineItemCount++ + } + + end { + if ($pipelineItemCount -gt 1) { + throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) + } + # get the discovery doc + if (!$Url.EndsWith('/.well-known/openid-configuration')) { + $Url += '/.well-known/openid-configuration' + } + + $config = Invoke-RestMethod -Method Get -Uri $Url + + # check it supports the code response_type + if ($config.response_types_supported -inotcontains 'code') { + # The OAuth2 provider does not support the 'code' response_type + throw ($PodeLocale.oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage) + } + + # can we have an InnerScheme? + if (($null -ne $InnerScheme) -and ($config.grant_types_supported -inotcontains 'password')) { + # The OAuth2 provider does not support the 'password' grant_type required by using an InnerScheme + throw ($PodeLocale.oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage) + } + + # scopes + $scopes = $config.scopes_supported + + if (($null -ne $Scope) -and ($Scope.Length -gt 0)) { + $scopes = @(foreach ($s in $Scope) { + if ($s -iin $config.scopes_supported) { + $s + } + }) + } + + # pkce code challenge method + $codeMethod = 'S256' + if ($config.code_challenge_methods_supported -inotcontains $codeMethod) { + $codeMethod = 'plain' + } + + return New-PodeAuthScheme ` + -OAuth2 ` + -ClientId $ClientId ` + -ClientSecret $ClientSecret ` + -AuthoriseUrl $config.authorization_endpoint ` + -TokenUrl $config.token_endpoint ` + -UserUrl $config.userinfo_endpoint ` + -RedirectUrl $RedirectUrl ` + -Scope $scopes ` + -InnerScheme $InnerScheme ` + -Middleware $Middleware ` + -CodeChallengeMethod $codeMethod ` + -UsePKCE:$UsePKCE + } +} + +<# +.SYNOPSIS +Test whether the current WebEvent or Session has an authenticated user. + +.DESCRIPTION +Test whether the current WebEvent or Session has an authenticated user. Returns true if there is an authenticated user. + +.PARAMETER IgnoreSession +If supplied, only the Auth object in the WebEvent will be checked and the Session will be skipped. + +.EXAMPLE +if (Test-PodeAuthUser) { ... } +#> +function Test-PodeAuthUser { + [CmdletBinding()] + [OutputType([boolean])] + param( + [switch] + $IgnoreSession + ) + + # auth middleware + if (($null -ne $WebEvent.Auth) -and $WebEvent.Auth.IsAuthenticated) { + $auth = $WebEvent.Auth + } + + # session? + elseif (!$IgnoreSession -and ($null -ne $WebEvent.Session.Data.Auth) -and $WebEvent.Session.Data.Auth.IsAuthenticated) { + $auth = $WebEvent.Session.Data.Auth + } + + # null? + if (($null -eq $auth) -or ($null -eq $auth.User)) { + return $false + } + + return ($null -ne $auth.User) +} + +<# +.SYNOPSIS +Get the authenticated user from the WebEvent or Session. + +.DESCRIPTION +Get the authenticated user from the WebEvent or Session. This is similar to calling $Webevent.Auth.User. + +.PARAMETER IgnoreSession +If supplied, only the Auth object in the WebEvent will be used and the Session will be skipped. + +.EXAMPLE +$user = Get-PodeAuthUser +#> +function Get-PodeAuthUser { + [CmdletBinding()] + param( + [switch] + $IgnoreSession + ) + + # auth middleware + if (($null -ne $WebEvent.Auth) -and $WebEvent.Auth.IsAuthenticated) { + $auth = $WebEvent.Auth + } + + # session? + elseif (!$IgnoreSession -and ($null -ne $WebEvent.Session.Data.Auth) -and $WebEvent.Session.Data.Auth.IsAuthenticated) { + $auth = $WebEvent.Session.Data.Auth + } + + # null? + if (($null -eq $auth) -or ($null -eq $auth.User)) { + return $null + } + + return $auth.User +} \ No newline at end of file diff --git a/tests/unit/Authentication.Tests.ps1 b/tests/unit/Authentication.Tests.ps1 index a3ac4e7ff..daa7460f3 100644 --- a/tests/unit/Authentication.Tests.ps1 +++ b/tests/unit/Authentication.Tests.ps1 @@ -206,7 +206,7 @@ Describe 'Invoke-PodeAuth Tests' { } } } - + $WebEvent=@{} $PodeLocale = @{ authMethodDoesNotExistExceptionMessage = "Authentication method {0} does not exist" authenticationMethodMergedExceptionMessage = "Authentication method {0} is merged" @@ -216,6 +216,7 @@ Describe 'Invoke-PodeAuth Tests' { It 'Should successfully invoke a valid authentication method' { Mock Test-PodeAuthExists { $true } -ParameterFilter { $Name -eq 'ValidAuth' } Mock Test-PodeAuthValidation { @{ Success = $true; User = 'TestUser'; Headers = @{} } } + Mock Add-PodeHeader {} $result = Invoke-PodeAuth -Name 'ValidAuth' @@ -233,5 +234,5 @@ Describe 'Invoke-PodeAuth Tests' { { Invoke-PodeAuth -Name 'MergedAuth' } | Should -Throw ($PodeLocale.authenticationMethodMergedExceptionMessage -f 'MergedAuth' ) } - + } From 65714bd1cc18fd6dfa343ad15682807f2213c7f3 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 24 Jan 2025 09:55:12 -0800 Subject: [PATCH 12/13] fixes and improvements --- examples/Web-AuthBasicBearer.ps1 | 10 +- examples/Web-AuthBasicHeader.ps1 | 9 +- examples/Web-AuthDigest.ps1 | 83 +- examples/Web-AuthForm.ps1 | 2 +- examples/Web-AuthManualErrorHandling.ps1 | 87 ++- src/Private/Authentication.ps1 | 919 +++++++++++++++++++---- src/Public/Authentication.ps1 | 3 + 7 files changed, 950 insertions(+), 163 deletions(-) diff --git a/examples/Web-AuthBasicBearer.ps1 b/examples/Web-AuthBasicBearer.ps1 index f6c73820d..3cd1ccaf6 100644 --- a/examples/Web-AuthBasicBearer.ps1 +++ b/examples/Web-AuthBasicBearer.ps1 @@ -9,7 +9,13 @@ .EXAMPLE To run the sample: ./Web-AuthBasicBearer.ps1 - Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -Headers @{ Authorization = 'Bearer test-token' } + Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -Headers @{ Authorization = 'Bearer test-token' } -ResponseHeadersVariable headers + +.EXAMPLE + "No Authorization header found" + + Invoke-RestMethod -Method Get -Uri 'http://localhost:8081/users' -ResponseHeadersVariable headers -Verbose -SkipHttpErrorCheck + $headers | Format-List .LINK https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthBasicBearer.ps1 @@ -64,7 +70,7 @@ Start-PodeServer -Threads 2 { } # GET request to get list of users (since there's no session, authentication will always happen) - Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ErrorContentType 'application/json' -ScriptBlock { Write-PodeJsonResponse -Value @{ Users = @( @{ diff --git a/examples/Web-AuthBasicHeader.ps1 b/examples/Web-AuthBasicHeader.ps1 index 0fb0f8d27..eca16bba7 100644 --- a/examples/Web-AuthBasicHeader.ps1 +++ b/examples/Web-AuthBasicHeader.ps1 @@ -17,7 +17,8 @@ The example used here is Basic authentication. Login: - $session = (Invoke-WebRequest -Uri http://localhost:8081/login -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' }).Headers['pode.sid'] + Invoke-RestMethod -Uri http://localhost:8081/login -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } -ResponseHeadersVariable headers -SkipHttpErrorCheck + $session = $headers['pode.sid'] Users: Invoke-RestMethod -Uri http://localhost:8081/users -Method Post -Headers @{ 'pode.sid' = "$session" } @@ -81,13 +82,13 @@ Start-PodeServer -Threads 2 { } # POST request to login - Add-PodeRoute -Method Post -Path '/login' -Authentication 'Login' + Add-PodeRoute -Method Post -Path '/login' -Authentication 'Login' -ErrorContentType 'application/json' # POST request to logout - Add-PodeRoute -Method Post -Path '/logout' -Authentication 'Login' -Logout + Add-PodeRoute -Method Post -Path '/logout' -Authentication 'Login' -Logout -ErrorContentType 'application/json' # POST request to get list of users - the "pode.sid" header is expected - Add-PodeRoute -Method Post -Path '/users' -Authentication 'Login' -ScriptBlock { + Add-PodeRoute -Method Post -Path '/users' -Authentication 'Login' -ErrorContentType 'application/json' -ScriptBlock { Write-PodeJsonResponse -Value @{ Users = @( @{ diff --git a/examples/Web-AuthDigest.ps1 b/examples/Web-AuthDigest.ps1 index 2ea531958..65c8b37ed 100644 --- a/examples/Web-AuthDigest.ps1 +++ b/examples/Web-AuthDigest.ps1 @@ -26,11 +26,79 @@ # Create the HTTP client $httpClient = [System.Net.Http.HttpClient]::new($handler) - # Send the GET request and capture the response - $response = $httpClient.GetStringAsync($uri).Result + # Create the HTTP GET request message + $requestMessage = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $uri) - # Display the response - $response + # Send the request and get the response + $response = $httpClient.SendAsync($requestMessage).Result + + # Extract and display the response headers + $response.Headers | ForEach-Object { "$($_.Key): $($_.Value)" } + + # Optionally, get content as string if needed + $content = $response.Content.ReadAsStringAsync().Result + $content + +.EXAMPLE + No authentication + + # Define the URI + $uri = [System.Uri]::new("http://localhost:8081/users") + + # Create the HTTP client handler (no authentication) + $handler = [System.Net.Http.HttpClientHandler]::new() + + # Create the HTTP client + $httpClient = [System.Net.Http.HttpClient]::new($handler) + + # Create the HTTP GET request message + $requestMessage = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $uri) + + # Send the request and get the response + $response = $httpClient.SendAsync($requestMessage).Result + + # Extract and display the response headers + $response.Headers | ForEach-Object { "$($_.Key): $($_.Value)" } + + # Optionally, get content as string if needed + $content = $response.Content.ReadAsStringAsync().Result + $content + +.EXAMPLE + Wrong password + + # Define the URI and wrong credentials + $uri = [System.Uri]::new("http://localhost:8081/users") + $wrongUsername = "wrongUser" + $wrongPassword = "wrongPassword" + + # Create a credential cache and add Digest authentication with incorrect credentials + $credentialCache = [System.Net.CredentialCache]::new() + $networkCredential = [System.Net.NetworkCredential]::new($wrongUsername, $wrongPassword) + $credentialCache.Add($uri, "Digest", $networkCredential) + + # Create the HTTP client handler with the credential cache + $handler = [System.Net.Http.HttpClientHandler]::new() + $handler.Credentials = $credentialCache + + # Create the HTTP client + $httpClient = [System.Net.Http.HttpClient]::new($handler) + + # Create the HTTP GET request message + $requestMessage = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $uri) + + # Send the request and get the response + $response = $httpClient.SendAsync($requestMessage).Result + + # Display the response status code (to check for 401 Unauthorized) + $response.StatusCode + + # Extract and display the response headers + $response.Headers | ForEach-Object { "$($_.Key): $($_.Value)" } + + # Optionally, get content as string if needed + $content = $response.Content.ReadAsStringAsync().Result + $content .LINK @@ -67,7 +135,7 @@ Start-PodeServer -Threads 2 { # setup digest auth New-PodeAuthScheme -Digest | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { param($username, $params) -write-podehost "username=$username" + # here you'd check a real user storage, this is just for example if ($username -ieq 'morty') { return @{ @@ -79,13 +147,12 @@ write-podehost "username=$username" Password = 'pickle' } } -write-podehost 'no auth' + return $null } # GET request to get list of users (since there's no session, authentication will always happen) - Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ScriptBlock { - write-podehsot '1' + Add-PodeRoute -Method Get -Path '/users' -Authentication 'Validate' -ErrorContentType 'application/json' -ScriptBlock { Write-PodeJsonResponse -Value @{ Users = @( @{ diff --git a/examples/Web-AuthForm.ps1 b/examples/Web-AuthForm.ps1 index 45fad7a19..03fe2147d 100644 --- a/examples/Web-AuthForm.ps1 +++ b/examples/Web-AuthForm.ps1 @@ -63,7 +63,7 @@ Start-PodeServer -Threads 2 { Enable-PodeSessionMiddleware -Duration 120 -Extend # setup form auth (
in HTML) - New-PodeAuthScheme -Form | Add-PodeAuth -Name 'Login' -FailureUrl '/login' -SuccessUrl '/' -ScriptBlock { + New-PodeAuthScheme -Form | Add-PodeAuth -Name 'Login' -FailureUrl '/login' -SuccessUrl '/' -ScriptBlock { param($username, $password) # here you'd check a real user storage, this is just for example diff --git a/examples/Web-AuthManualErrorHandling.ps1 b/examples/Web-AuthManualErrorHandling.ps1 index efc76cdd1..6e970cf67 100644 --- a/examples/Web-AuthManualErrorHandling.ps1 +++ b/examples/Web-AuthManualErrorHandling.ps1 @@ -16,6 +16,38 @@ Invoke-RestMethod -Uri http://localhost/api/v3/ -Headers @{ 'X-API-KEY' = 'test_user' } -Method Get +.EXAMPLE + Digest + # Define the URI and credentials + $uri = [System.Uri]::new("http://localhost:8081/api/v3/whois") + $username = "morty" + $password = "pickle" + + # Create a credential cache and add Digest authentication + $credentialCache = [System.Net.CredentialCache]::new() + $networkCredential = [System.Net.NetworkCredential]::new($username, $password) + $credentialCache.Add($uri, "Digest", $networkCredential) + + # Create the HTTP client handler with the credential cache + $handler = [System.Net.Http.HttpClientHandler]::new() + $handler.Credentials = $credentialCache + + # Create the HTTP client + $httpClient = [System.Net.Http.HttpClient]::new($handler) + + # Create the HTTP GET request message + $requestMessage = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Get, $uri) + + # Send the request and get the response + $response = $httpClient.SendAsync($requestMessage).Result + + # Extract and display the response headers + $response.Headers | ForEach-Object { "$($_.Key): $($_.Value)" } + + # Optionally, get content as string if needed + $content = $response.Content.ReadAsStringAsync().Result + $content + .LINK https://github.com/Badgerati/Pode/blob/develop/examples/Web-AuthManualErrorHandling.ps1 @@ -42,7 +74,7 @@ catch { throw } # Start the Pode server Start-PodeServer { # Define an HTTP endpoint for the server - Add-PodeEndpoint -Address 'localhost' -Protocol 'Http' -Port '80' + Add-PodeEndpoint -Address 'localhost' -Protocol 'Http' -Port '8081' # Enable OpenAPI documentation and viewers Enable-PodeOpenApi -Path '/docs/openapi' -OpenApiVersion '3.0.3' -DisableMinimalDefinitions -NoDefaultResponses @@ -79,6 +111,24 @@ Start-PodeServer { } + New-PodeAuthScheme -Digest | Add-PodeAuth -Name 'Digest' -Sessionless -ScriptBlock { + param($username, $params) + + # here you'd check a real user storage, this is just for example + if ($username -ieq 'morty') { + return @{ + User = @{ + ID = 'M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + Password = 'pickle' + } + } + + return $null + } + # Define an API route with manual authentication error handling Add-PodeRoute -PassThru -Method 'Get' -Path '/api/v3/whoami' -Authentication 'APIKey' -NoMiddlewareAuthentication -ScriptBlock { # Manually invoke authentication @@ -109,8 +159,7 @@ Start-PodeServer { Add-PodeRoute -PassThru -Method 'Get' -Path '/api/v3/whoami_standard' -Authentication 'APIKey_standard' -ErrorContentType 'application/json' -ScriptBlock { # Manually invoke authentication - # $auth = Invoke-PodeAuth -Name 'APIKey' - + $auth = $WebEvent.Auth # Log authentication details for debugging Write-PodeHost $auth -Explode @@ -133,4 +182,34 @@ Start-PodeServer { } | Set-PodeOARouteInfo -Summary 'Who am I (default auth)' -Tags 'auth' -OperationId 'whoami_standard' -PassThru | Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = (New-PodeOABoolProperty -Name 'Success' -Default $true | New-PodeOAStringProperty -Name 'Username' | New-PodeOAIntProperty -Name 'UserId' | New-PodeOAObjectProperty ) } -PassThru | Add-PodeOAResponse -StatusCode 401 -Description 'Authentication failure' -Content @{ 'application/json' = (New-PodeOABoolProperty -Name 'Success' -Default $false | New-PodeOAStringProperty -Name 'Username' | New-PodeOAStringProperty -Name 'Message' | New-PodeOAObjectProperty ) } -} + + + # Define an API route with manual authentication error handling + Add-PodeRoute -Method 'Get' -Path '/api/v3/whois' -Authentication 'Digest' -ScriptBlock { + # Manually invoke authentication + $auth = $WebEvent.Auth + + # Log authentication details for debugging + Write-PodeHost $Webauth -Explode + + # If authentication succeeds, return user details + if ($auth.Success) { + Write-PodeJsonResponse -StatusCode 200 -Value @{ + Success = $true + Username = $auth.User.Name + UserId = $auth.User.Id + } + } + else { + # Handle authentication failures with a custom error response + Write-PodeJsonResponse -StatusCode 401 -Value @{ + Success = $false + Message = $auth.Reason + Username = $auth.User + } + } + } + #| Set-PodeOARouteInfo -Summary 'Who Is' -Tags 'auth' -OperationId 'whois' -PassThru | + # Add-PodeOAResponse -StatusCode 200 -Description 'Successful operation' -Content @{ 'application/json' = (New-PodeOABoolProperty -Name 'Success' -Default $true | New-PodeOAStringProperty -Name 'Username' | New-PodeOAIntProperty -Name 'UserId' | New-PodeOAObjectProperty ) } -PassThru | + # Add-PodeOAResponse -StatusCode 401 -Description 'Authentication failure' -Content @{ 'application/json' = (New-PodeOABoolProperty -Name 'Success' -Default $false | New-PodeOAStringProperty -Name 'Username' | New-PodeOAStringProperty -Name 'Message' | New-PodeOAObjectProperty ) } +} \ No newline at end of file diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index b01c4034a..af9897ed9 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -1,29 +1,81 @@ function Get-PodeAuthBasicType { + <# + .SYNOPSIS + Processes Basic Authentication from the Authorization header. + + .DESCRIPTION + The `Get-PodeAuthBasicType` function extracts and validates the Basic Authorization header + from an HTTP request. It verifies the header format, decodes Base64 credentials, + and returns the extracted username and password. If any validation step fails, + an appropriate HTTP response code and challenge are returned. + + .PARAMETER options + A hashtable containing options for processing the authentication: + - `HeaderTag` [string]: Expected header prefix (e.g., "Basic"). + - `Encoding` [string]: Character encoding for decoding the credentials (default: UTF-8). + - `AsCredential` [bool]: If true, returns credentials as a [PSCredential] object. + + .OUTPUTS + [array] + Returns an array containing the extracted username and password. + If `AsCredential` is set to `$true`, returns a `[PSCredential]` object. + + .EXAMPLE + $options = @{ HeaderTag = 'Basic'; Encoding = 'UTF-8'; AsCredential = $false } + $result = Get-PodeAuthBasicType -options $options + + Returns: + @('username', 'password') + + .EXAMPLE + $options = @{ HeaderTag = 'Basic'; Encoding = 'UTF-8'; AsCredential = $true } + $result = Get-PodeAuthBasicType -options $options + + Returns: + [PSCredential] object containing username and password. + + .NOTES + This function is internal to Pode and subject to change in future releases. + + Possible response codes: + - 401 Unauthorized: When the Authorization header is missing. + - 400 Bad Request: For invalid format, encoding, or credential issues. + + Challenge responses include the following error types: + - `invalid_request` for missing or incorrectly formatted headers. + - `invalid_token` for improperly encoded or malformed credentials. + #> return { param($options) # get the auth header $header = (Get-PodeHeader -Name 'Authorization') if ($null -eq $header) { + $message = 'No Authorization header found' return @{ - Message = 'No Authorization header found' - Code = 401 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 401 } } # ensure the first atom is basic (or opt override) $atoms = $header -isplit '\s+' if ($atoms.Length -lt 2) { + $message = 'Invalid Authorization header format' return @{ - Message = 'Invalid Authorization header' - Code = 400 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 400 } } if ($atoms[0] -ine $options.HeaderTag) { + $message = "Header is not for $($options.HeaderTag) Authorization" return @{ - Message = "Header is not for $($options.HeaderTag) Authorization" - Code = 400 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 400 } } @@ -32,9 +84,11 @@ function Get-PodeAuthBasicType { $enc = [System.Text.Encoding]::GetEncoding($options.Encoding) } catch { + $message = 'Invalid encoding specified for Authorization' return @{ - Message = 'Invalid encoding specified for Authorization' - Code = 400 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 400 } } @@ -42,9 +96,22 @@ function Get-PodeAuthBasicType { $decoded = $enc.GetString([System.Convert]::FromBase64String($atoms[1])) } catch { + $message = 'Invalid Base64 string found in Authorization header' + return @{ + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_token -ErrorDescription $message) + Code = 400 + } + } + + # ensure the decoded string contains a colon separator + $index = $decoded.IndexOf(':') + if ($index -lt 0) { + $message = 'Invalid Authorization credential format' return @{ - Message = 'Invalid Base64 string found in Authorization header' - Code = 400 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 400 } } @@ -68,6 +135,7 @@ function Get-PodeAuthBasicType { } } + function Get-PodeAuthOAuth2Type { return { param($options, $schemes) @@ -282,32 +350,91 @@ function Get-PodeOAuth2RedirectHost { } function Get-PodeAuthClientCertificateType { + <# + .SYNOPSIS + Validates and extracts information from a client certificate in an HTTP request. + + .DESCRIPTION + The `Get-PodeAuthClientCertificateType` function processes the client certificate + from an incoming HTTP request. It validates whether the certificate is supplied, + checks its validity, and ensures it's trusted. If any of these checks fail, + appropriate response codes and challenges are returned. + + .PARAMETER options + A hashtable containing options that can be used to extend the function in the future. + + .OUTPUTS + [array] + Returns an array containing the validated client certificate and any associated errors. + + .EXAMPLE + $options = @{} + $result = Get-PodeAuthClientCertificateType -options $options + + Returns: + An array with the client certificate object and any certificate validation errors. + + .EXAMPLE + $options = @{} + $result = Get-PodeAuthClientCertificateType -options $options + + Example Output: + @($cert, 0) + + .NOTES + This function is internal to Pode and subject to change in future releases. + + Possible response codes: + - 401 Unauthorized: When the client certificate is missing or invalid. + - 403 Forbidden: When the client certificate is untrusted. + + Challenge responses include the following error types: + - `invalid_request`: If no certificate is provided. + - `invalid_token`: If the certificate is invalid, expired, or untrusted. + + #> return { param($options) $cert = $WebEvent.Request.ClientCertificate # ensure we have a client cert if ($null -eq $cert) { + $message = 'No client certificate supplied' return @{ - Message = 'No client certificate supplied' - Code = 401 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 401 } } # ensure the cert has a thumbprint if ([string]::IsNullOrWhiteSpace($cert.Thumbprint)) { + $message = 'Invalid client certificate supplied' return @{ - Message = 'Invalid client certificate supplied' - Code = 401 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_token -ErrorDescription $message) + 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)) { + $message = 'Invalid client certificate supplied (expired or not yet valid)' return @{ - Message = 'Invalid client certificate supplied' - Code = 401 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_token -ErrorDescription $message) + Code = 401 + } + } + + $errors = $WebEvent.Request.ClientCertificateErrors + if ($errors -ne 0) { + $message = 'Untrusted client certificate supplied' + return @{ + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_token -ErrorDescription $message) + Code = 403 } } @@ -316,13 +443,45 @@ function Get-PodeAuthClientCertificateType { } } + function Get-PodeAuthApiKeyType { + <# + .SYNOPSIS + Handles API key authentication by retrieving the key from various locations. + + .DESCRIPTION + The `Get-PodeAuthApiKeyType` function extracts and validates API keys + from specified locations such as headers, query parameters, or cookies. + If the API key is found, it is returned as a result; otherwise, + an appropriate authentication challenge is issued. + + .PARAMETER $options + A hashtable containing the following keys: + - `Location`: Specifies where to retrieve the API key from (`header`, `query`, or `cookie`). + - `LocationName`: The name of the header, query parameter, or cookie that holds the API key. + - `AsJWT`: (Optional) If set to `$true`, the function will treat the API key as a JWT token. + - `Secret`: (Required if `AsJWT` is `$true`) The secret used to validate the JWT token. + + .OUTPUTS + [array] + Returns an array containing the extracted API key or JWT payload if authentication is successful. + + .NOTES + The function will return an HTTP 400 response code if the API key is not found. + If `AsJWT` is enabled, the key will be decoded and validated using the provided secret. + The challenge response is formatted to align with authentication best practices. + + Possible HTTP response codes: + - 400 Bad Request: When the API key is missing or JWT validation fails. + + #> return { param($options) - # get api key from appropriate location + # Initialize API key variable $apiKey = [string]::Empty + # Determine API key location and retrieve it switch ($options.Location.ToLowerInvariant()) { 'header' { $apiKey = Get-PodeHeader -Name $options.LocationName @@ -335,38 +494,49 @@ function Get-PodeAuthApiKeyType { 'cookie' { $apiKey = Get-PodeCookieValue -Name $options.LocationName } + default { + $message = "Invalid API key location: $($options.Location)" + return @{ + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 400 + } + } } - # 400 if no key + # If no API key found, return error if ([string]::IsNullOrWhiteSpace($apiKey)) { + $message = "API key missing in $($options.Location) location: $($options.LocationName)" return @{ - Message = "No $($options.LocationName) $($options.Location) found" - Code = 400 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 400 } } - # build the result + # Trim and process the API key $apiKey = $apiKey.Trim() $result = @($apiKey) - # convert as jwt? + # Convert to JWT if required if ($options.AsJWT) { try { $payload = ConvertFrom-PodeJwt -Token $apiKey -Secret $options.Secret Test-PodeJwt -Payload $payload + $result = @($payload) } catch { if ($_.Exception.Message -ilike '*jwt*') { + $message = "Invalid JWT token: $($_.Exception.Message)" return @{ - Message = $_.Exception.Message - Code = 400 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 400 } } throw } - - $result = @($payload) } # return the result @@ -375,51 +545,96 @@ function Get-PodeAuthApiKeyType { } function Get-PodeAuthBearerType { + <# + .SYNOPSIS + Validates the Bearer token in the Authorization header. + + .DESCRIPTION + This function processes the Authorization header, verifies the presence of a Bearer token, + and optionally decodes it as a JWT. It returns appropriate HTTP response codes + as per RFC 6750 (OAuth 2.0 Bearer Token Usage). + + .PARAMETER $options + A hashtable containing the following keys: + - Realm: The authentication realm. + - Scopes: Expected scopes for the token. + - HeaderTag: The expected Authorization header tag (e.g., 'Bearer'). + - AsJWT: Boolean indicating if the token should be processed as a JWT. + - Secret: Secret key for JWT verification. + + .OUTPUTS + A hashtable containing the following keys based on the validation result: + - Message: Error or success message. + - Code: HTTP response code. + - Header: HTTP response header for authentication challenges. + - Challenge: Optional authentication challenge. + + .NOTES + The function adheres to RFC 6750, which mandates: + - 401 Unauthorized for missing or invalid authentication credentials. + - 400 Bad Request for malformed requests. + + RFC 6750 HTTP Status Code Usage + # | Scenario | Recommended Status Code | + # |-------------------------------------------|-------------------------| + # | No Authorization header provided | 401 Unauthorized | + # | Incorrect Authorization header format | 401 Unauthorized | + # | Wrong authentication scheme used | 401 Unauthorized | + # | Token is empty or malformed | 400 Bad Request | + # | Invalid JWT signature | 401 Unauthorized | + #> return { param($options) - # get the auth header + # Get the Authorization header $header = (Get-PodeHeader -Name 'Authorization') + + # If no Authorization header is provided, return 401 Unauthorized if ($null -eq $header) { + $message = 'No Authorization header found' return @{ - Message = 'No Authorization header found' - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request) - Code = 400 + Message = $message + Challenge = New-PodeAuthChallenge -Scopes $options.Scopes -ErrorType invalid_request -ErrorDescription $message + Code = 401 # RFC 6750: Missing credentials should return 401 } } - # ensure the first atom is bearer + # Ensure the first part of the header is 'Bearer' $atoms = $header -isplit '\s+' if ($atoms.Length -lt 2) { + $message = 'Invalid Authorization header format' return @{ - Message = 'Invalid Authorization header' - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request) - Code = 400 + Message = $message + Challenge = (New-PodeAuthChallenge -Scopes $options.Scopes -ErrorType invalid_request -ErrorDescription $message) + Code = 401 # RFC 6750: Invalid credentials format should return 401 } } if ($atoms[0] -ine $options.HeaderTag) { + $message = "Authorization header is not $($options.HeaderTag)" return @{ - Message = "Authorization header is not $($options.HeaderTag)" - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request) - Code = 400 + Message = $message + Challenge = (New-PodeAuthChallenge -Scopes $options.Scopes -ErrorType invalid_request -ErrorDescription $message) + Code = 401 # RFC 6750: Wrong authentication scheme should return 401 } } - # 400 if no token + # 400 Bad Request if no token is provided $token = $atoms[1] if ([string]::IsNullOrWhiteSpace($token)) { + $message = 'No Bearer token found' return @{ - Message = 'No Bearer token found' - Code = 400 + Message = $message + Code = 400 # RFC 6750: Malformed request should return 400 + Challenge = New-PodeAuthChallenge -Scopes $options.Scopes -ErrorType invalid_request -ErrorDescription $message } } - # build the result + # Trim and build the result $token = $token.Trim() $result = @($token) - # convert as jwt? + # Convert to JWT if required if ($options.AsJWT) { try { $payload = ConvertFrom-PodeJwt -Token $token -Secret $options.Secret @@ -428,9 +643,9 @@ function Get-PodeAuthBearerType { catch { if ($_.Exception.Message -ilike '*jwt*') { return @{ - Message = $_.Exception.Message - #https://www.rfc-editor.org/rfc/rfc6750 Bearer token should return 401 - Code = 401 + Message = $_.Exception.Message + Code = 401 # RFC 6750: Invalid token should return 401 + Challenge = New-PodeAuthChallenge -Scopes $options.Scopes -ErrorType invalid_token -ErrorDescription $_.Exception.Message } } @@ -440,102 +655,142 @@ function Get-PodeAuthBearerType { $result = @($payload) } - # return the result + # Return the validated result return $result } } function Get-PodeAuthBearerPostValidator { + <# + .SYNOPSIS + Validates the Bearer token and user authentication. + + .DESCRIPTION + This function processes the Bearer token, checks for the presence of a valid user, + and verifies token scopes against required scopes. It returns appropriate HTTP response codes + as per RFC 6750 (OAuth 2.0 Bearer Token Usage). + + .PARAMETER token + The Bearer token provided by the client. + + .PARAMETER result + The decoded token result containing user and scope information. + + .PARAMETER options + A hashtable containing the following keys: + - Scopes: An array of required scopes for authorization. + + .OUTPUTS + A hashtable containing the following keys based on the validation result: + - Message: Error or success message. + - Code: HTTP response code. + - Challenge: HTTP response challenge in case of errors. + + .NOTES + The function adheres to RFC 6750, which mandates: + - 401 Unauthorized for missing or invalid authentication credentials. + - 403 Forbidden for insufficient scopes. + #> return { param($token, $result, $options) - # if there's no user, fail with challenge + # Validate user presence in the token if (($null -eq $result) -or ($null -eq $result.User)) { + $message = 'User authentication failed' return @{ - Message = 'User not found' - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_token) + Message = $message + Challenge = (New-PodeAuthChallenge -Scopes $options.Scopes -ErrorType invalid_token -ErrorDescription $message ) Code = 401 } } - # check for an error and description + # Check for token error and return appropriate response if (![string]::IsNullOrWhiteSpace($result.Error)) { return @{ - Message = 'Authorization failed' - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType $result.Error -ErrorDescription $result.ErrorDescription) + Message = $result.ErrorDescription + Challenge = (New-PodeAuthChallenge -Scopes $options.Scopes -ErrorType $result.Error -ErrorDescription $result.ErrorDescription) Code = 401 } } - # check the scopes + # Scope validation $hasAuthScopes = (($null -ne $options.Scopes) -and ($options.Scopes.Length -gt 0)) - $hasTokenScope = ![string]::IsNullOrWhiteSpace($result.Scope) + $hasTokenScope = (($null -ne $result.Scope) -and ($result.Scope.Length -gt 0)) - # 403 if we have auth scopes but no token scope + # Return 403 if authorization scopes exist but token lacks scopes if ($hasAuthScopes -and !$hasTokenScope) { + $message = 'Token scope is missing or invalid' return @{ - Message = 'Invalid Scope' - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType insufficient_scope) + Message = $message + Challenge = (New-PodeAuthChallenge -Scopes $options.Scopes -ErrorType insufficient_scope -ErrorDescription $message ) Code = 403 } } - # 403 if we have both, but token not in auth scope - if ($hasAuthScopes -and $hasTokenScope -and ($options.Scopes -notcontains $result.Scope)) { + # Return 403 if token scopes do not intersect with required auth scopes + if ($hasAuthScopes -and $hasTokenScope -and (-not ($options.Scopes | Where-Object { $_ -in $result.Scope }))) { + $message = 'Token scope is insufficient' return @{ - Message = 'Invalid Scope' - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType insufficient_scope) + Message = $message + Challenge = (New-PodeAuthChallenge -Scopes $options.Scopes -ErrorType insufficient_scope -ErrorDescription $message ) Code = 403 } } - # return result + # Return the validated token result return $result } } -function New-PodeAuthBearerChallenge { - param( - [Parameter()] - [string[]] - $Scopes, - - [Parameter()] - [ValidateSet('', 'invalid_request', 'invalid_token', 'insufficient_scope')] - [string] - $ErrorType, - - [Parameter()] - [string] - $ErrorDescription - ) - - $items = @() - if (($null -ne $Scopes) -and ($Scopes.Length -gt 0)) { - $items += "scope=`"$($Scopes -join ' ')`"" - } - - if (![string]::IsNullOrWhiteSpace($ErrorType)) { - $items += "error=`"$($ErrorType)`"" - } - if (![string]::IsNullOrWhiteSpace($ErrorDescription)) { - $items += "error_description=`"$($ErrorDescription)`"" - } - - return ($items -join ', ') -} function Get-PodeAuthDigestType { + <# + .SYNOPSIS + Validates the Digest token in the Authorization header. + + .DESCRIPTION + This function processes the Authorization header, verifies the presence of a Digest token, + and optionally decodes it. It returns appropriate HTTP response codes + as per RFC 7616 (HTTP Digest Access Authentication). + + .PARAMETER $options + A hashtable containing the following keys: + - Realm: The authentication realm. + - Nonce: A unique value provided by the server to prevent replay attacks. + - HeaderTag: The expected Authorization header tag (e.g., 'Digest'). + + .OUTPUTS + A hashtable containing the following keys based on the validation result: + - Message: Error or success message. + - Code: HTTP response code. + - Challenge: Optional authentication challenge. + + .NOTES + The function adheres to RFC 7616, which mandates: + - 401 Unauthorized for missing or invalid authentication credentials. + - 400 Bad Request for malformed requests. + + - RFC 7616 HTTP Status Code Usage + | Scenario | Recommended Status Code | + |-------------------------------------------|-------------------------| + | No Authorization header provided | 401 Unauthorized | + | Incorrect Authorization header format | 401 Unauthorized | + | Wrong authentication scheme used | 401 Unauthorized | + | Token is empty or malformed | 400 Bad Request | + | Invalid digest response | 401 Unauthorized | + + #> return { param($options) - + $nonce = (New-PodeGuid -Secure -NoDashes) # get the auth header - send challenge if missing $header = (Get-PodeHeader -Name 'Authorization') if ($null -eq $header) { + $message = 'No Authorization header found' return @{ - Message = 'No Authorization header found' - Challenge = (New-PodeAuthDigestChallenge) + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorDescription $message -Nonce $nonce) Code = 401 } } @@ -543,16 +798,19 @@ function Get-PodeAuthDigestType { # if auth header isn't digest send challenge $atoms = $header -isplit '\s+' if ($atoms.Length -lt 2) { + $message = 'Invalid Authorization header format' return @{ - Message = 'Invalid Authorization header' - Code = 400 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorDescription $message -Nonce $nonce ) + Code = 401 # RFC 7616: Invalid credentials format should return 401 } } if ($atoms[0] -ine $options.HeaderTag) { + $message = "Authorization header is not $($options.HeaderTag)" return @{ - Message = "Authorization header is not $($options.HeaderTag)" - Challenge = (New-PodeAuthDigestChallenge) + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorDescription $message -Nonce $nonce) Code = 401 } } @@ -560,26 +818,31 @@ function Get-PodeAuthDigestType { # parse the other atoms of the header (after the scheme), return 400 if none $params = ConvertFrom-PodeAuthDigestHeader -Parts ($atoms[1..$($atoms.Length - 1)]) if ($params.Count -eq 0) { + $message = 'Invalid Authorization header' return @{ - Message = 'Invalid Authorization header' - Code = 400 + Message = $message + Code = 400 + Challenge = (New-PodeAuthChallenge -ErrorDescription $message -Nonce $nonce) } } # if no username then 401 and challenge if ([string]::IsNullOrWhiteSpace($params.username)) { + $message = 'Authorization header is missing username' return @{ - Message = 'Authorization header is missing username' - Challenge = (New-PodeAuthDigestChallenge) + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorDescription $message )# -Nonce $nonce) Code = 401 } } # return 400 if domain doesnt match request domain if ($WebEvent.Path -ine $params.uri) { + $message = 'Invalid Authorization header' return @{ - Message = 'Invalid Authorization header' - Code = 400 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorDescription $message)# -Nonce $nonce ) + Code = 400 } } @@ -588,15 +851,100 @@ function Get-PodeAuthDigestType { } } +<# +.SYNOPSIS + Validates digest authentication responses for incoming requests. + +.DESCRIPTION + The `Get-PodeAuthDigestPostValidator` function processes and validates HTTP digest + authentication responses by comparing the computed hash with the client's provided response. + It ensures the provided credentials are correct and returns appropriate challenges + if validation fails. + +.PARAMETER username + The username extracted from the client's authentication request. + +.PARAMETER params + A hashtable containing digest authentication parameters, including: + - `username`: The username provided in the request. + - `realm`: The authentication realm. + - `nonce`: A unique server-generated nonce value. + - `uri`: The requested resource URI. + - `nc`: Nonce count (tracking the number of requests). + - `cnonce`: Client-generated nonce value. + - `qop`: Quality of protection value. + - `response`: The client's hashed response to be verified. + +.PARAMETER result + A hashtable containing the user data retrieved from the authentication source. + This should include: + - `User`: The username. + - `Password`: The stored password or hash for verification. + +.PARAMETER options + Additional options for authentication processing, if required. + +.OUTPUTS + On successful validation, returns the user data with the password removed. + If authentication fails, returns an error response with a challenge and HTTP status code. + +.EXAMPLE + $params = @{ + username = "morty" + realm = "PodeRealm" + nonce = "abc123" + uri = "/protected" + nc = "00000001" + cnonce = "xyz456" + qop = "auth" + response = "expected-client-hash" + } + + $result = @{ + User = "morty" + Password = "pickle" + } + + Get-PodeAuthDigestPostValidator -username "morty" -params $params -result $result -options $null + + Returns: + @{'User'='morty'} + +.EXAMPLE + Get-PodeAuthDigestPostValidator -username "unknown" -params $params -result $null -options $null + + Returns: + @{ + Message = "Invalid credentials" + Challenge = "Digest realm=\"PodeRealm\", error_description=\"Invalid credentials\"" + Code = 401 + } + +.NOTES + This function performs digest authentication validation by: + - Generating an MD5 hash using the provided credentials and digest parameters. + - Comparing the computed hash with the client's provided response. + - Handling authentication failures by returning appropriate challenges. + + Possible HTTP response codes: + - 401 Unauthorized: When credentials are missing, incorrect, or authentication fails. + + Digest authentication elements included: + - `qop="auth"` + - `algorithm="MD5"` + - `nonce=""` + +#> function Get-PodeAuthDigestPostValidator { return { param($username, $params, $result, $options) # if there's no user or password, fail with challenge if (($null -eq $result) -or ($null -eq $result.User) -or [string]::IsNullOrWhiteSpace($result.Password)) { + $message = 'Invalid credentials' return @{ - Message = 'User not found' - Challenge = (New-PodeAuthDigestChallenge) + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -Nonce $params.nonce -ErrorDescription $message) Code = 401 } } @@ -612,9 +960,10 @@ function Get-PodeAuthDigestPostValidator { # compare final hash to client response if ($final -ne $params.response) { + $message = 'Invalid authentication response' return @{ - Message = 'Hashes failed to match' - Challenge = (New-PodeAuthDigestChallenge) + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -Nonce $params.nonce -ErrorDescription $message) Code = 401 } } @@ -625,6 +974,7 @@ function Get-PodeAuthDigestPostValidator { } } + function ConvertFrom-PodeAuthDigestHeader { param( [Parameter()] @@ -648,12 +998,59 @@ function ConvertFrom-PodeAuthDigestHeader { return $obj } -function New-PodeAuthDigestChallenge { - $items = @('qop="auth"', 'algorithm="MD5"', "nonce=`"$(New-PodeGuid -Secure -NoDashes)`"") - return ($items -join ', ') -} - function Get-PodeAuthFormType { + <# + .SYNOPSIS + Processes form-based authentication requests. + + .DESCRIPTION + The `Get-PodeAuthFormType` function extracts and validates user credentials from + an incoming HTTP form submission. It verifies the presence and format of the + provided username and password and optionally converts them to secure credentials. + + .PARAMETER $options + A hashtable containing configuration options for the authentication process. + Expected keys: + - `Fields.Username`: The key used to extract the username from the request data. + - `Fields.Password`: The key used to extract the password from the request data. + - `AsCredential`: (Boolean) If true, converts credentials into a [PSCredential] object. + + .OUTPUTS + [array] + Returns an array containing the validated username and password. + If `AsCredential` is set to `$true`, returns a `[PSCredential]` object. + + .EXAMPLE + $options = @{ + Fields = @{ Username = 'user'; Password = 'pass' } + AsCredential = $false + } + $result = Get-PodeAuthFormType -options $options + + Returns: + @('user123', 'securePassword') + + .EXAMPLE + $options = @{ + Fields = @{ Username = 'user'; Password = 'pass' } + AsCredential = $true + } + $result = Get-PodeAuthFormType -options $options + + Returns: + [PSCredential] object containing username and password. + + .NOTES + This function performs several checks, including: + - Ensuring both username and password are provided. + - Validating the username format (only alphanumeric, dot, underscore, and dash allowed). + - Returning HTTP status codes and error messages in case of validation failures. + + Possible HTTP response codes: + - 401 Unauthorized: When credentials are missing or incomplete. + - 400 Bad Request: When the username format is invalid. + + #> return { param($options) @@ -665,11 +1062,41 @@ function Get-PodeAuthFormType { $username = $WebEvent.Data.$userField $password = $WebEvent.Data.$passField - # if either are empty, fail auth - if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) { + # Handle cases where fields are missing or empty + if ([string]::IsNullOrWhiteSpace($username) -and [string]::IsNullOrWhiteSpace($password)) { + $message = 'Username and password must be provided' + return @{ + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 401 + } + } + + if ([string]::IsNullOrWhiteSpace($username)) { + $message = 'Username is required' return @{ - Message = 'Username or Password not supplied' - Code = 401 + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 401 + } + } + + if ([string]::IsNullOrWhiteSpace($password)) { + $message = 'Password is required' + return @{ + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 401 + } + } + + # Validate username format + if ($username -notmatch '^[a-zA-Z0-9._-]{3,20}$') { + $message = 'Invalid username format' + return @{ + Message = $message + Challenge = (New-PodeAuthChallenge -ErrorType invalid_request -ErrorDescription $message) + Code = 400 } } @@ -1280,9 +1707,9 @@ function Test-PodeAuthValidation { $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.ScriptBlock.Script -Arguments $_args -Return -Splat) } - # Remove the Middleware processed data if code is 400 - no token - if ($NoMiddlewareAuthentication -and ($result.Code -eq 400)) { - $result = '' + # Handle NoMiddlewareAuthentication case + if ($NoMiddlewareAuthentication) { + return Invoke-PodeAuthNoMiddleware -Result $result -Auth $auth } # If authentication script returns a non-hashtable, perform further validation @@ -1308,30 +1735,7 @@ function Test-PodeAuthValidation { } } - # Handle results when invoked from a route script - if ($NoMiddlewareAuthentication -and ($null -ne $result) -and ($result -is [hashtable])) { - if ($result.Success -is [bool]) { - $success = $result.Success - } - else { - $success = $false - [System.Exception]::new("The authentication Scriptblock must return an hashtable with a key named 'Success'") | Write-PodeErrorLog - } - $ret = @{ - Success = $success - User = '' - Headers = '' - IsAuthenticated = $success - IsAuthorised = $success - Store = !$auth.Sessionless - Name = $Name - } - foreach ($key in $result.Keys) { - $ret[$key] = $result[$key] # Overwrites if key exists - } - return $ret - } # Authentication failure handling if (($null -eq $result) -or ($result.Count -eq 0) -or (Test-PodeIsEmpty $result.User)) { @@ -1385,6 +1789,7 @@ function Test-PodeAuthValidation { } + function Get-PodeAuthMiddlewareScript { return { param($opts) @@ -1509,6 +1914,7 @@ function Test-PodeAuthInternal { Store = !$auth.Sessionless Name = $result.Auth } + # successful auth $authName = $null if ($auth.Merged -and !$auth.PassOne) { @@ -2401,4 +2807,229 @@ function Get-PodeAuthRedirectUrl { } return $Url +} + + +<# +.SYNOPSIS + Generates the WWW-Authenticate challenge header for failed authentication attempts. + +.DESCRIPTION + The `New-PodeAuthChallenge` function constructs a formatted authentication challenge + string to be included in HTTP responses when authentication fails. + It supports optional parameters such as scopes, error types, descriptions, + and digest authentication mechanisms. + +.PARAMETER Scopes + An array of required scopes to be included in the challenge response. + Scopes define the level of access required for the requested resource. + +.PARAMETER ErrorType + Specifies the type of error to include in the challenge response. + Accepted values are: + - 'invalid_request' : The request is missing a required parameter. + - 'invalid_token' : The provided token is expired, revoked, or invalid. + - 'insufficient_scope' : The provided token lacks necessary privileges. + +.PARAMETER ErrorDescription + Provides a descriptive error message in the challenge response to explain + the reason for the authentication failure. + +.PARAMETER Digest + A switch parameter that, when specified, includes digest authentication elements + such as quality of protection (qop), algorithm, and a unique nonce value. + +.OUTPUTS + [string] + Returns a formatted challenge string to be used in the HTTP response header. + +.EXAMPLE + New-PodeAuthChallenge -Scopes @('read', 'write') -ErrorType 'invalid_token' -ErrorDescription 'Token has expired' + + Returns: + scope="read write", error="invalid_token", error_description="Token has expired" + +.EXAMPLE + New-PodeAuthChallenge -Digest + + Returns: + qop="auth", algorithm="MD5", nonce="generated_nonce" + +.EXAMPLE + New-PodeAuthChallenge -Scopes @('admin') -ErrorType 'insufficient_scope' + + Returns: + scope="admin", error="insufficient_scope" + +.NOTES + This function is used to generate the `WWW-Authenticate` response header + when authentication attempts fail. It helps inform clients of the authentication + requirements and reasons for failure. +#> + +function New-PodeAuthChallenge { + param( + [Parameter()] + [string[]] + $Scopes, + + [Parameter()] + [ValidateSet('invalid_request', 'invalid_token', 'insufficient_scope')] + [string] + $ErrorType = 'invalid_request', + + [Parameter()] + [string] + $ErrorDescription, + + [Parameter()] + [string] + $Nonce + ) + + $items = @() + + if (![string]::IsNullOrWhiteSpace($Nonce)) { + $items += 'qop="auth"', 'algorithm="MD5"', "nonce=`"$Nonce`"" + } + + if (($null -ne $Scopes) -and ($Scopes.Length -gt 0)) { + $items += "scope=`"$($Scopes -join ' ')`"" + } + + if (![string]::IsNullOrWhiteSpace($ErrorType)) { + $items += "error=`"$($ErrorType)`"" + } + + if (![string]::IsNullOrWhiteSpace($ErrorDescription)) { + $items += "error_description=`"$($ErrorDescription)`"" + } + + return ($items -join ', ') +} + +<# +.SYNOPSIS + Executes authentication validation without middleware intervention. + +.DESCRIPTION + The `Invoke-PodeAuthNoMiddleware` function processes an authentication request + by executing the provided authentication script and optional post-validation script. + It constructs an authentication result and handles potential redirections and errors. + +.PARAMETER Result + A [psobject] containing the initial authentication result from a previous process. + It may include fields such as: + - `Code`: The HTTP status code from a previous authentication attempt. + - `Headers`: Authentication headers to be included in the response. + - `Challenge`: An authentication challenge string to return to the client. + +.PARAMETER Auth + A [hashtable] containing the authentication scheme configuration. + Expected keys include: + - `ScriptBlock`: The main authentication script to execute. + - `Scheme`: An object containing authentication scheme details such as: + - `PostValidator.Script`: A post-authentication validation script block. + - `Arguments`: Arguments to be passed to authentication scripts. + - `Sessionless`: A boolean indicating whether to store authentication state. + +.OUTPUTS + A hashtable with the authentication result containing: + - `Success` : Indicates whether authentication was successful. + - `User` : The authenticated user details (if available). + - `Headers` : Authentication headers for the response. + - `Challenge` : The challenge for failed authentication attempts. + - `IsAuthenticated` : Boolean indicating authentication success. + - `IsAuthorised` : Boolean indicating authorization status. + - `Store` : Boolean indicating if the session should be stored. + - `Name` : The authentication method name. + - `StatusCode` : HTTP status code for failed authentication attempts. + - `Exception` : Error message in case of server errors. + +.NOTES + This function is primarily used in authentication scenarios where middleware processing + is either bypassed or unnecessary, allowing direct execution of the authentication logic. + + If the provided `Result` parameter contains a `Code` field with a value of 400 or greater, + the function processes authentication failure and returns appropriate headers and challenges. + + Possible HTTP response codes: + - 401 Unauthorized: When authentication fails. + - 500 Internal Server Error: When an unexpected server error occurs. +#> +function Invoke-PodeAuthNoMiddleware { + param( + [psobject] + $Result, + + [hashtable] + $Auth + ) + + if (($Result -is [hashtable]) -and ($Result.Code -ge 400)) { + $headers = $Result.Headers + $challenge = $Result.Challenge + #$code = $Result.Code + $_args = @($auth.Arguments) + } + else { + $_args = @($Result) + @($auth.Arguments) + } + + $original = $Result + + # Run the authentication script with the updated arguments + $Result = (Invoke-PodeScriptBlock -ScriptBlock $auth.ScriptBlock -Arguments $_args -UsingVariables $auth.UsingVariables -Return -Splat) + + # Run post-authentication validation if applicable + if ([string]::IsNullOrEmpty($Result.Code) -and ($null -ne $auth.Scheme.PostValidator.Script)) { + $_args = @($original) + @($Result) + @($auth.Scheme.Arguments) + $Result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.PostValidator.Script -Arguments $_args -UsingVariables $auth.Scheme.PostValidator.UsingVariables -Return -Splat) + } + # Handle results when invoked from a route script + if ( ($null -ne $Result) -and ($Result -is [hashtable])) { + if ($result.IsRedirected) { + $success = $false # Handle authentication redirection scenarios (e.g., OAuth) + }elseif ($Result.Success -is [bool]) { + $success = $Result.Success + } + else { + $success = $false + [System.Exception]::new("The authentication Scriptblock must return an hashtable with a key named 'Success'") | Write-PodeErrorLog + } + + $ret = @{ + Success = $success + User = '' + Headers = @{} + Challenge = $challenge + IsAuthenticated = $success + IsAuthorised = $success + Store = !$auth.Sessionless + Name = $Name + } + foreach ($key in $Result.Keys) { + $ret[$key] = $Result[$key] # Overwrites if key exists + } + + if ($headers -and $headers.Count -gt 0) { + $ret.Headers += $headers + } + if (!([string]::IsNullOrWhiteSpace($ret.Challenge)) -and !([string]::IsNullOrWhiteSpace($auth.Scheme.Name)) -and !($ret.Headers.ContainsKey('WWW-Authenticate'))) { + $authHeader = Get-PodeAuthWwwHeaderValue -Name $auth.Scheme.Name -Realm $auth.Scheme.Realm -Challenge $ret.Challenge + $ret.Headers['WWW-Authenticate'] = $authHeader + + } + if ($ret.Headers.Count -gt 0) { + Add-PodeHeaderBulk -Values $ret.Headers + } + return $ret + } + else { + return @{ + Success = $false + StatusCode = 500 + Exception = 'Server error' + } + } } \ No newline at end of file diff --git a/src/Public/Authentication.ps1 b/src/Public/Authentication.ps1 index 8c748cb14..4a9dd8af0 100644 --- a/src/Public/Authentication.ps1 +++ b/src/Public/Authentication.ps1 @@ -1215,6 +1215,9 @@ function Invoke-PodeAuth { try { # Perform authentication validation $WebEvent.Auth = Test-PodeAuthValidation -Name $Name -NoMiddlewareAuthentication + write-podehost (Get-PodeHeader -name 'WWW-Authenticate') + # Add-PodeHeader -Name 'WWW-Authenticate' -Value $WebEvent.Auth.Headers['WWW-Authenticate'] + # write-podehost (Get-PodeHeader -name 'WWW-Authenticate') } catch { $_ | Write-PodeErrorLog From da691240a90b123d9b48f309845517e2ce30e2e7 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 24 Jan 2025 10:02:26 -0800 Subject: [PATCH 13/13] fix test --- my modified authentication - Private.ps1 | 2544 ------------------ my modified authentication - Public.ps1 | 2690 -------------------- tests/integration/Authentication.Tests.ps1 | 2 +- 3 files changed, 1 insertion(+), 5235 deletions(-) delete mode 100644 my modified authentication - Private.ps1 delete mode 100644 my modified authentication - Public.ps1 diff --git a/my modified authentication - Private.ps1 b/my modified authentication - Private.ps1 deleted file mode 100644 index 5979dc87d..000000000 --- a/my modified authentication - Private.ps1 +++ /dev/null @@ -1,2544 +0,0 @@ -function Get-PodeAuthBasicType { - return { - param($options) - - # get the auth header - $header = (Get-PodeHeader -Name 'Authorization') - if ($null -eq $header) { - return @{ - Message = 'No Authorization header found' - Code = 401 - Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`"" } - } - } - - # ensure the first atom is basic (or opt override) - $atoms = $header -isplit '\s+' - if ($atoms.Length -lt 2) { - return @{ - Message = 'Invalid Authorization header' - Code = 400 - Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`"" } - } - } - - if ($atoms[0] -ine $options.HeaderTag) { - return @{ - Message = "Header is not for $($options.HeaderTag) Authorization" - Code = 400 - Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`"" } - } - } - - # decode the auth header - try { - $enc = [System.Text.Encoding]::GetEncoding($options.Encoding) - } - catch { - return @{ - Message = 'Invalid encoding specified for Authorization' - Code = 400 - Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`"" } - } - } - - try { - $decoded = $enc.GetString([System.Convert]::FromBase64String($atoms[1])) - } - catch { - return @{ - Message = 'Invalid Base64 string found in Authorization header' - Code = 400 - Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`"" } - } - } - - # validate and return user/result - $index = $decoded.IndexOf(':') - $username = $decoded.Substring(0, $index) - $password = $decoded.Substring($index + 1) - - # build the result - $result = @($username, $password) - - # convert to credential? - if ($options.AsCredential) { - $passSecure = ConvertTo-SecureString -String $password -AsPlainText -Force - $creds = [pscredential]::new($username, $passSecure) - $result = @($creds) - } - - # return data for calling validator - return $result - } -} - -function Get-PodeAuthOAuth2Type { - return { - param($options, $schemes) - - # set default scopes - if (($null -eq $options.Scopes) -or ($options.Scopes.Length -eq 0)) { - $options.Scopes = @('openid', 'profile', 'email') - } - - $scopes = ($options.Scopes -join ' ') - - # if there's an error, fail - if (![string]::IsNullOrWhiteSpace($WebEvent.Query['error'])) { - return @{ - Message = $WebEvent.Query['error'] - Code = 401 - IsErrored = $true - } - } - - # set grant type - $hasInnerScheme = (($null -ne $schemes) -and ($schemes.Length -gt 0)) - $grantType = 'authorization_code' - if ($hasInnerScheme) { - $grantType = 'password' - } - - # if there's a code query param, or inner scheme, get access token - if ($hasInnerScheme -or ![string]::IsNullOrWhiteSpace($WebEvent.Query['code'])) { - try { - # ensure the state is valid - if ((Test-PodeSessionsInUse) -and ($WebEvent.Query['state'] -ne $WebEvent.Session.Data['__pode_oauth_state__'])) { - return @{ - Message = 'OAuth2 state returned is invalid' - Code = 401 - IsErrored = $true - } - } - - # build tokenUrl query with client info - $body = "client_id=$($options.Client.ID)" - $body += "&grant_type=$($grantType)" - - if (![string]::IsNullOrEmpty($options.Client.Secret)) { - $body += "&client_secret=$([System.Web.HttpUtility]::UrlEncode($options.Client.Secret))" - } - - # add PKCE code verifier - if ($options.PKCE.Enabled) { - $body += "&code_verifier=$($WebEvent.Session.Data['__pode_oauth_code_verifier__'])" - } - - # if there's an inner scheme, get the username/password, and set query - if ($hasInnerScheme) { - $body += "&username=$($schemes[-1][0])" - $body += "&password=$($schemes[-1][1])" - $body += "&scope=$([System.Web.HttpUtility]::UrlEncode($scopes))" - } - - # otherwise, set query for auth_code - else { - $redirectUrl = Get-PodeOAuth2RedirectHost -RedirectUrl $options.Urls.Redirect - $body += "&code=$($WebEvent.Query['code'])" - $body += "&redirect_uri=$([System.Web.HttpUtility]::UrlEncode($redirectUrl))" - } - - # POST the tokenUrl - try { - $result = Invoke-RestMethod -Method Post -Uri $options.Urls.Token -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop - } - catch [System.Net.WebException], [System.Net.Http.HttpRequestException] { - $response = Read-PodeWebExceptionInfo -ErrorRecord $_ - $result = ($response.Body | ConvertFrom-Json) - } - - # was there an error? - if (![string]::IsNullOrWhiteSpace($result.error)) { - return @{ - Message = "$($result.error): $($result.error_description)" - Code = 401 - IsErrored = $true - } - } - - # get user details - if url supplied - if (![string]::IsNullOrWhiteSpace($options.Urls.User.Url)) { - try { - $user = Invoke-RestMethod -Method $options.Urls.User.Method -Uri $options.Urls.User.Url -Headers @{ Authorization = "Bearer $($result.access_token)" } - } - catch [System.Net.WebException], [System.Net.Http.HttpRequestException] { - $response = Read-PodeWebExceptionInfo -ErrorRecord $_ - $user = ($response.Body | ConvertFrom-Json) - } - - if (![string]::IsNullOrWhiteSpace($user.error)) { - return @{ - Message = "$($user.error): $($user.error_description)" - Code = 401 - IsErrored = $true - } - } - } - elseif (![string]::IsNullOrWhiteSpace($result.id_token)) { - try { - $user = ConvertFrom-PodeJwt -Token $result.id_token -IgnoreSignature - } - catch { - $user = @{ Provider = 'OAuth2' } - } - } - else { - $user = @{ Provider = 'OAuth2' } - } - - # return the user for the validator - return @($user, $result.access_token, $result.refresh_token, $result) - } - finally { - if ($null -ne $WebEvent.Session.Data) { - # clear state - $WebEvent.Session.Data.Remove('__pode_oauth_state__') - - # clear PKCE - if ($options.PKCE.Enabled) { - $WebEvent.Session.Data.Remove('__pode_oauth_code_verifier__') - } - } - } - } - - # redirect to the authUrl - only if no inner scheme supplied - if (!$hasInnerScheme) { - # get the redirectUrl - $redirectUrl = Get-PodeOAuth2RedirectHost -RedirectUrl $options.Urls.Redirect - - # add authUrl query params - $query = "client_id=$($options.Client.ID)" - $query += '&response_type=code' - $query += "&redirect_uri=$([System.Web.HttpUtility]::UrlEncode($redirectUrl))" - $query += '&response_mode=query' - $query += "&scope=$([System.Web.HttpUtility]::UrlEncode($scopes))" - - # add csrf state - if (Test-PodeSessionsInUse) { - $guid = New-PodeGuid - $WebEvent.Session.Data['__pode_oauth_state__'] = $guid - $query += "&state=$($guid)" - } - - # build a code verifier for PKCE, and add to query - if ($options.PKCE.Enabled) { - $guid = New-PodeGuid - $codeVerifier = "$($guid)-$($guid)" - $WebEvent.Session.Data['__pode_oauth_code_verifier__'] = $codeVerifier - - $codeChallenge = $codeVerifier - if ($options.PKCE.CodeChallenge.Method -ieq 'S256') { - $codeChallenge = ConvertTo-PodeBase64UrlValue -Value (Invoke-PodeSHA256Hash -Value $codeChallenge) -NoConvert - } - - $query += "&code_challenge=$($codeChallenge)" - $query += "&code_challenge_method=$($options.PKCE.CodeChallenge.Method)" - } - - # are custom parameters already on the URL? - $url = $options.Urls.Authorise - if (!$url.Contains('?')) { - $url += '?' - } - else { - $url += '&' - } - - # redirect to OAuth2 endpoint - Move-PodeResponseUrl -Url "$($url)$($query)" - return @{ IsRedirected = $true } - } - - # hmm, this is unexpected - return @{ - Message = 'Well, this is awkward...' - Code = 500 - IsErrored = $true - } - } -} - -function Get-PodeOAuth2RedirectHost { - param( - [Parameter()] - [string] - $RedirectUrl - ) - - if ($RedirectUrl.StartsWith('/')) { - if ($PodeContext.Server.IsIIS -or $PodeContext.Server.IsHeroku) { - $protocol = Get-PodeHeader -Name 'X-Forwarded-Proto' - if ([string]::IsNullOrWhiteSpace($protocol)) { - $protocol = 'https' - } - - $domain = "$($protocol)://$($WebEvent.Request.Host)" - } - else { - $domain = Get-PodeEndpointUrl - } - - $RedirectUrl = "$($domain.TrimEnd('/'))$($RedirectUrl)" - } - - return $RedirectUrl -} - -function Get-PodeAuthClientCertificateType { - return { - param($options) - $cert = $WebEvent.Request.ClientCertificate - - # ensure we have a client cert - if ($null -eq $cert) { - $message = 'No client certificate supplied' - return @{ - Message = $message - Code = 400 - Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`", error=`"invalid_request`", error_description=`"$message`"" } - } - } - - # ensure the cert has a thumbprint - if ([string]::IsNullOrWhiteSpace($cert.Thumbprint)) { - $message = 'Invalid client certificate supplied: missing thumbprint' - return @{ - Message = $message - Code = 400 - Headers = @{'WWW-Authenticate' = "TLS realm=`"$($options.Realm)`", error=`"invalid_token`", error_description=`"$message`"" } - } - } - - # ensure the cert hasn't expired, or has it even started - $now = [datetime]::UtcNow - if (($cert.NotAfter -lt $now) -or ($cert.NotBefore -gt $now)) { - $message = 'Client certificate supplied is expired or not valid yet' - return @{ - Message = $message - Code = 403 - Headers = @{'WWW-Authenticate' = "TLS realm=`"$($options.Realm)`", error=`"invalid_token`", error_description=`"$message`"" } - } - } - - # return data for calling validator - return @($cert, $WebEvent.Request.ClientCertificateErrors) - } -} - -function Get-PodeAuthApiKeyType { - return { - param($options) - - # get api key from appropriate location - $apiKey = [string]::Empty - - switch ($options.Location.ToLowerInvariant()) { - 'header' { - $apiKey = Get-PodeHeader -Name $options.LocationName - } - - 'query' { - $apiKey = $WebEvent.Query[$options.LocationName] - } - - 'cookie' { - $apiKey = Get-PodeCookieValue -Name $options.LocationName - } - } - # 400 if no key - if ([string]::IsNullOrWhiteSpace($apiKey)) { - $message = "No $($options.LocationName) $($options.Location) found" - return @{ - Message = $message - Code = 400 - Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`", error=`"invalid_request`", error_description=`"$message`"" } - } - } - - # build the result - $apiKey = $apiKey.Trim() - $result = @($apiKey) - - # convert as jwt? - if ($options.AsJWT) { - try { - $payload = ConvertFrom-PodeJwt -Token $apiKey -Secret $options.Secret - Test-PodeJwt -Payload $payload - } - catch { - if ($_.Exception.Message -ilike '*jwt*') { - return @{ - Message = $_.Exception.Message - Code = 400 - Headers = @{'WWW-Authenticate' = "Bearer realm=`"$($options.Realm)`", error=`"invalid_request`", error_description=`"$($_.Exception.Message)`"" } - } - } - - throw - } - - $result = @($payload) - } - - # return the result - return $result - } -} - -function Get-PodeAuthBearerType { - return { - <# - .SYNOPSIS - Validates the Bearer token in the Authorization header. - - .DESCRIPTION - This function processes the Authorization header, verifies the presence of a Bearer token, - and optionally decodes it as a JWT. It returns appropriate HTTP response codes - as per RFC 6750 (OAuth 2.0 Bearer Token Usage). - - .PARAMETER $options - A hashtable containing the following keys: - - Realm: The authentication realm. - - Scopes: Expected scopes for the token. - - HeaderTag: The expected Authorization header tag (e.g., 'Bearer'). - - AsJWT: Boolean indicating if the token should be processed as a JWT. - - Secret: Secret key for JWT verification. - - .OUTPUTS - A hashtable containing the following keys based on the validation result: - - Message: Error or success message. - - Code: HTTP response code. - - Header: HTTP response header for authentication challenges. - - Challenge: Optional authentication challenge. - - .NOTES - The function adheres to RFC 6750, which mandates: - - 401 Unauthorized for missing or invalid authentication credentials. - - 400 Bad Request for malformed requests. - - RFC 6750 HTTP Status Code Usage - # | Scenario | Recommended Status Code | - # |-------------------------------------------|-------------------------| - # | No Authorization header provided | 401 Unauthorized | - # | Incorrect Authorization header format | 401 Unauthorized | - # | Wrong authentication scheme used | 401 Unauthorized | - # | Token is empty or malformed | 400 Bad Request | - # | Invalid JWT signature | 401 Unauthorized | - #> - - param($options) - write-podehost "I'm here" - # Define common WWW-Authenticate header with placeholders - $authHeaderBase = "Bearer realm=`"$($options.Realm)`", error=`"{0}`", error_description=`"{1}`"" - - # Get the Authorization header - $header = (Get-PodeHeader -Name 'Authorization') - - # If no Authorization header is provided, return 401 Unauthorized - if ($null -eq $header) { - $message = 'No Authorization header found' - return @{ - Message = $message - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request) - Code = 401 # RFC 6750: Missing credentials should return 401 - Header = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } - } - } - - # Ensure the first part of the header is 'Bearer' - $atoms = $header -isplit '\s+' - if ($atoms.Length -lt 2) { - $message = 'Invalid Authorization header format' - return @{ - Message = $message - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request) - Code = 401 # RFC 6750: Invalid credentials format should return 401 - Header = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } - } - } - - if ($atoms[0] -ine $options.HeaderTag) { - $message = "Authorization header is not $($options.HeaderTag)" - return @{ - Message = $message - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_request) - Code = 401 # RFC 6750: Wrong authentication scheme should return 401 - Header = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } - } - } - - # 400 Bad Request if no token is provided - $token = $atoms[1] - if ([string]::IsNullOrWhiteSpace($token)) { - $message = 'No Bearer token found' - return @{ - Message = $message - Code = 400 # RFC 6750: Malformed request should return 400 - Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } - } - } - - # Trim and build the result - $token = $token.Trim() - $result = @($token) - - # Convert to JWT if required - if ($options.AsJWT) { - try { - $payload = ConvertFrom-PodeJwt -Token $token -Secret $options.Secret - Test-PodeJwt -Payload $payload - } - catch { - if ($_.Exception.Message -ilike '*jwt*') { - return @{ - Message = $_.Exception.Message - Code = 401 # RFC 6750: Invalid token should return 401 - Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_token', $_.Exception.Message } - } - } - - throw - } - - $result = @($payload) - } - write-podehost "I'm here2" - # Return the validated result - return $result - } -} - - -function Get-PodeAuthBearerPostValidator { - return { - param($token, $result, $options) - - # if there's no user, fail with challenge - if (($null -eq $result) -or ($null -eq $result.User)) { - return @{ - Message = 'User not found' - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType invalid_token) - Code = 401 - } - } - - # check for an error and description - if (![string]::IsNullOrWhiteSpace($result.Error)) { - return @{ - Message = 'Authorization failed' - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType $result.Error -ErrorDescription $result.ErrorDescription) - Code = 401 - } - } - - # check the scopes - $hasAuthScopes = (($null -ne $options.Scopes) -and ($options.Scopes.Length -gt 0)) - $hasTokenScope = ![string]::IsNullOrWhiteSpace($result.Scope) - - # 403 if we have auth scopes but no token scope - if ($hasAuthScopes -and !$hasTokenScope) { - return @{ - Message = 'Invalid Scope' - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType insufficient_scope) - Code = 403 - } - } - - # 403 if we have both, but token not in auth scope - if ($hasAuthScopes -and $hasTokenScope -and ($options.Scopes -notcontains $result.Scope)) { - return @{ - Message = 'Invalid Scope' - Challenge = (New-PodeAuthBearerChallenge -Scopes $options.Scopes -ErrorType insufficient_scope) - Code = 403 - } - } - - # return result - return $result - } -} - -function New-PodeAuthBearerChallenge { - param( - [Parameter()] - [string[]] - $Scopes, - - [Parameter()] - [ValidateSet('', 'invalid_request', 'invalid_token', 'insufficient_scope')] - [string] - $ErrorType, - - [Parameter()] - [string] - $ErrorDescription - ) - - $items = @() - if (($null -ne $Scopes) -and ($Scopes.Length -gt 0)) { - $items += "scope=`"$($Scopes -join ' ')`"" - } - - if (![string]::IsNullOrWhiteSpace($ErrorType)) { - $items += "error=`"$($ErrorType)`"" - } - - if (![string]::IsNullOrWhiteSpace($ErrorDescription)) { - $items += "error_description=`"$($ErrorDescription)`"" - } - - return ($items -join ', ') -} - -<# -.SYNOPSIS - Validates the Digest token in the Authorization header. - -.DESCRIPTION - This function processes the Authorization header, verifies the presence of a Digest token, - and optionally decodes it. It returns appropriate HTTP response codes - as per RFC 7616 (HTTP Digest Access Authentication). - -.PARAMETER $options - A hashtable containing the following keys: - - Realm: The authentication realm. - - Nonce: A unique value provided by the server to prevent replay attacks. - - HeaderTag: The expected Authorization header tag (e.g., 'Digest'). - - Algorithm: The hashing algorithm used (e.g., MD5, SHA-256). - -.OUTPUTS - A hashtable containing the following keys based on the validation result: - - Message: Error or success message. - - Code: HTTP response code. - - Header: HTTP response header for authentication challenges. - - Challenge: Optional authentication challenge. - -.NOTES - The function adheres to RFC 7616, which mandates: - - 401 Unauthorized for missing or invalid authentication credentials. - - 400 Bad Request for malformed requests. - - - RFC 7616 HTTP Status Code Usage - | Scenario | Recommended Status Code | - |-------------------------------------------|-------------------------| - | No Authorization header provided | 401 Unauthorized | - | Incorrect Authorization header format | 401 Unauthorized | - | Wrong authentication scheme used | 401 Unauthorized | - | Token is empty or malformed | 400 Bad Request | - | Invalid digest response | 401 Unauthorized | - - #> -function Get-PodeAuthDigestType { - return { - param($options) - write-podehost "I'm here1" - # Define common WWW-Authenticate header with placeholders - $authHeaderBase = "Digest realm=`"$($options.Realm)`", nonce=`"$($options.Nonce)`", algorithm=`"$($options.Algorithm)`", error=`"{0}`", error_description=`"{1}`"" - - # Get the Authorization header - send challenge if missing - $header = (Get-PodeHeader -Name 'Authorization') - if ($null -eq $header) { - $message = 'No Authorization header found' - write-podehost $message - return @{ - Message = $message - Code = 401 # RFC 7616: Missing credentials should return 401 - Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } - } - } - - # If auth header isn't digest, send challenge - $atoms = $header -isplit '\s+' - if ($atoms.Length -lt 2) { - $message = 'Invalid Authorization header format' - write-podehost $message - return @{ - Message = $message - Code = 401 # RFC 7616: Invalid credentials format should return 401 - Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } - } - } - - if ($atoms[0] -ine $options.HeaderTag) { - $message = "Authorization header is not $($options.HeaderTag)" - write-podehost $message - return @{ - Message = $message - Code = 401 # RFC 7616: Wrong authentication scheme should return 401 - Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } - } - } - - # Parse the other atoms of the header (after the scheme), return 400 if none - $params = ConvertFrom-PodeAuthDigestHeader -Parts ($atoms[1..$($atoms.Length - 1)]) - if ($params.Count -eq 0) { - $message = 'Invalid Authorization header' - write-podehost $message - return @{ - Message = $message - Code = 400 - Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } - } - } - - # If no username then 401 and challenge - if ([string]::IsNullOrWhiteSpace($params.username)) { - $message = 'Authorization header is missing username' - write-podehost $message - return @{ - Message = $message - Challenge = (New-PodeAuthDigestChallenge) - Code = 401 - Header = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } - } - } - - # Return 400 if domain doesn't match request domain - if ($WebEvent.Path -ine $params.uri) { - $message = 'Invalid Authorization header' - write-podehost $message - return @{ - Message = $message - Code = 400 - Headers = @{'WWW-Authenticate' = $authHeaderBase -f 'invalid_request', $message } - } - } - - write-podehost "I'm here2" - # Return data for calling validator - return @($params.username, $params) - } -} - - -function Get-PodeAuthDigestPostValidator { - return { - param($username, $params, $result, $options) - - # if there's no user or password, fail with challenge - if (($null -eq $result) -or ($null -eq $result.User) -or [string]::IsNullOrWhiteSpace($result.Password)) { - return @{ - Message = 'User not found' - Challenge = (New-PodeAuthDigestChallenge) - Code = 401 - } - } - - # generate the first hash - $hash1 = Invoke-PodeMD5Hash -Value "$($params.username):$($params.realm):$($result.Password)" - - # generate the second hash - $hash2 = Invoke-PodeMD5Hash -Value "$($WebEvent.Method.ToUpperInvariant()):$($params.uri)" - - # generate final hash - $final = Invoke-PodeMD5Hash -Value "$($hash1):$($params.nonce):$($params.nc):$($params.cnonce):$($params.qop):$($hash2)" - - # compare final hash to client response - if ($final -ne $params.response) { - return @{ - Message = 'Hashes failed to match' - Challenge = (New-PodeAuthDigestChallenge) - Code = 401 - } - } - - # hashes are valid, remove password and return result - $null = $result.Remove('Password') - return $result - } -} - -function ConvertFrom-PodeAuthDigestHeader { - param( - [Parameter()] - [string[]] - $Parts - ) - - if (($null -eq $Parts) -or ($Parts.Length -eq 0)) { - return @{} - } - - $obj = @{} - $value = ($Parts -join ' ') - - @($value -isplit ',(?=(?:[^"]|"[^"]*")*$)') | ForEach-Object { - if ($_ -imatch '(?\w+)=["]?(?[^"]+)["]?$') { - $obj[$Matches['name']] = $Matches['value'] - } - } - - return $obj -} - -function New-PodeAuthDigestChallenge { - $items = @('qop="auth"', 'algorithm="MD5"', "nonce=`"$(New-PodeGuid -Secure -NoDashes)`"") - return ($items -join ', ') -} - -function Get-PodeAuthFormType { - return { - param($options) - - # get user/pass keys to get from payload - $userField = $options.Fields.Username - $passField = $options.Fields.Password - - # get the user/pass - $username = $WebEvent.Data.$userField - $password = $WebEvent.Data.$passField - - # if either are empty, fail auth - if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) { - return @{ - Message = 'Username or Password not supplied' - Code = 401 - } - } - - # build the result - $result = @($username, $password) - - # convert to credential? - if ($options.AsCredential) { - $passSecure = ConvertTo-SecureString -String $password -AsPlainText -Force - $creds = [pscredential]::new($username, $passSecure) - $result = @($creds) - } - - # return data for calling validator - return $result - } -} - -<# -.SYNOPSIS - Authenticates a user based on a username and password provided as parameters. - -.DESCRIPTION - This function finds a user whose username matches the provided username, and checks the user's password. - If the password is correct, it converts the user into a hashtable and checks if the user is valid for any users/groups specified by the options parameter. If the user is valid, it returns a hashtable containing the user object. If the user is not valid, it returns a hashtable with a message indicating that the user is not authorized to access the website. - -.PARAMETER username - The username of the user to authenticate. - -.PARAMETER password - The password of the user to authenticate. - -.PARAMETER options - A hashtable containing options for the function. It can include the following keys: - - FilePath: The path to the JSON file containing user data. - - HmacSecret: The secret key for computing a HMAC-SHA256 hash of the password. - - Users: A list of valid users. - - Groups: A list of valid groups. - - ScriptBlock: A script block for additional validation. - -.EXAMPLE - Get-PodeAuthUserFileMethod -username "admin" -password "password123" -options @{ FilePath = "C:\Users.json"; HmacSecret = "secret"; Users = @("admin"); Groups = @("Administrators"); ScriptBlock = { param($user) $user.Name -eq "admin" } } - - This example authenticates a user with username "admin" and password "password123". It reads user data from the JSON file at "C:\Users.json", computes a HMAC-SHA256 hash of the password using "secret" as the secret key, and checks if the user is in the "admin" user or "Administrators" group. It also performs additional validation using a script block that checks if the user's name is "admin". -#> -function Get-PodeAuthUserFileMethod { - return { - param($username, $password, $options) - - # using pscreds? - if (($null -eq $options) -and ($username -is [pscredential])) { - $_username = ([pscredential]$username).UserName - $_password = ([pscredential]$username).GetNetworkCredential().Password - $_options = [hashtable]$password - } - else { - $_username = $username - $_password = $password - $_options = $options - } - - # load the file - $users = (Get-Content -Path $_options.FilePath -Raw | ConvertFrom-Json) - - # find the user by username - only use the first one - $user = @(foreach ($_user in $users) { - if ($_user.Username -ieq $_username) { - $_user - break - } - })[0] - - # fail if no user - if ($null -eq $user) { - return @{ Message = 'You are not authorised to access this website' } - } - - # check the user's password - if (![string]::IsNullOrWhiteSpace($_options.HmacSecret)) { - $hash = Invoke-PodeHMACSHA256Hash -Value $_password -Secret $_options.HmacSecret - } - else { - $hash = Invoke-PodeSHA256Hash -Value $_password - } - - if ($user.Password -ne $hash) { - return @{ Message = 'You are not authorised to access this website' } - } - - # convert the user to a hashtable - $user = @{ - Name = $user.Name - Username = $user.Username - Email = $user.Email - Groups = $user.Groups - Metadata = $user.Metadata - } - - # is the user valid for any users/groups? - if (!(Test-PodeAuthUserGroup -User $user -Users $_options.Users -Groups $_options.Groups)) { - return @{ Message = 'You are not authorised to access this website' } - } - - $result = @{ User = $user } - - # call additional scriptblock if supplied - if ($null -ne $_options.ScriptBlock.Script) { - $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $_options.ScriptBlock.Script -UsingVariables $_options.ScriptBlock.UsingVariables - } - - # return final result, this could contain a user obj, or an error message from custom scriptblock - return $result - } -} - -function Get-PodeAuthWindowsADMethod { - return { - param($username, $password, $options) - - # using pscreds? - if (($null -eq $options) -and ($username -is [pscredential])) { - $_username = ([pscredential]$username).UserName - $_password = ([pscredential]$username).GetNetworkCredential().Password - $_options = [hashtable]$password - } - else { - $_username = $username - $_password = $password - $_options = $options - } - - # parse username to remove domains - $_username = (($_username -split '@')[0] -split '\\')[-1] - - # validate and retrieve the AD user - $noGroups = $_options.NoGroups - $directGroups = $_options.DirectGroups - $keepCredential = $_options.KeepCredential - - $result = Get-PodeAuthADResult ` - -Server $_options.Server ` - -Domain $_options.Domain ` - -SearchBase $_options.SearchBase ` - -Username $_username ` - -Password $_password ` - -Provider $_options.Provider ` - -NoGroups:$noGroups ` - -DirectGroups:$directGroups ` - -KeepCredential:$keepCredential - - # if there's a message, fail and return the message - if (![string]::IsNullOrWhiteSpace($result.Message)) { - return $result - } - - # if there's no user, then, err, oops - if (Test-PodeIsEmpty $result.User) { - return @{ Message = 'An unexpected error occured' } - } - - # is the user valid for any users/groups - if not, error! - if (!(Test-PodeAuthUserGroup -User $result.User -Users $_options.Users -Groups $_options.Groups)) { - return @{ Message = 'You are not authorised to access this website' } - } - - # call additional scriptblock if supplied - if ($null -ne $_options.ScriptBlock.Script) { - $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $_options.ScriptBlock.Script -UsingVariables $_options.ScriptBlock.UsingVariables - } - - # return final result, this could contain a user obj, or an error message from custom scriptblock - return $result - } -} - -function Invoke-PodeAuthInbuiltScriptBlock { - param( - [Parameter(Mandatory = $true)] - [hashtable] - $User, - - [Parameter(Mandatory = $true)] - [scriptblock] - $ScriptBlock, - - [Parameter()] - $UsingVariables - ) - - return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Arguments $User -UsingVariables $UsingVariables -Return) -} - -function Get-PodeAuthWindowsLocalMethod { - return { - param($username, $password, $options) - - # using pscreds? - if (($null -eq $options) -and ($username -is [pscredential])) { - $_username = ([pscredential]$username).UserName - $_password = ([pscredential]$username).GetNetworkCredential().Password - $_options = [hashtable]$password - } - else { - $_username = $username - $_password = $password - $_options = $options - } - - $user = @{ - UserType = 'Local' - AuthenticationType = 'WinNT' - Username = $_username - Name = [string]::Empty - Fqdn = $PodeContext.Server.ComputerName - Domain = 'localhost' - Groups = @() - } - - Add-Type -AssemblyName System.DirectoryServices.AccountManagement -ErrorAction Stop - $context = [System.DirectoryServices.AccountManagement.PrincipalContext]::new('Machine', $PodeContext.Server.ComputerName) - $valid = $context.ValidateCredentials($_username, $_password) - - if (!$valid) { - return @{ Message = 'Invalid credentials supplied' } - } - - try { - $tmpUsername = $_username -replace '\\', '/' - if ($_username -inotlike "$($PodeContext.Server.ComputerName)*") { - $tmpUsername = "$($PodeContext.Server.ComputerName)/$($_username)" - } - - $ad = [adsi]"WinNT://$($tmpUsername)" - $user.Name = @($ad.FullName)[0] - - if (!$_options.NoGroups) { - $cmd = "`$ad = [adsi]'WinNT://$($tmpUsername)'; @(`$ad.Groups() | Foreach-Object { `$_.GetType().InvokeMember('Name', 'GetProperty', `$null, `$_, `$null) })" - $user.Groups = [string[]](powershell -c $cmd) - } - } - finally { - Close-PodeDisposable -Disposable $ad -Close - } - - # is the user valid for any users/groups - if not, error! - if (!(Test-PodeAuthUserGroup -User $user -Users $_options.Users -Groups $_options.Groups)) { - return @{ Message = 'You are not authorised to access this website' } - } - - $result = @{ User = $user } - - # call additional scriptblock if supplied - if ($null -ne $_options.ScriptBlock.Script) { - $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $_options.ScriptBlock.Script -UsingVariables $_options.ScriptBlock.UsingVariables - } - - # return final result, this could contain a user obj, or an error message from custom scriptblock - return $result - } -} - -function Get-PodeAuthWindowsADIISMethod { - return { - param($token, $options) - - # get the close handler - $win32Handler = Add-Type -Name Win32CloseHandle -PassThru -MemberDefinition @' - [DllImport("kernel32.dll", SetLastError = true)] - public static extern bool CloseHandle(IntPtr handle); -'@ - - try { - # parse the auth token and get the user - $winAuthToken = [System.IntPtr][Int]"0x$($token)" - $winIdentity = [System.Security.Principal.WindowsIdentity]::new($winAuthToken, 'Windows') - - # get user and domain - $username = ($winIdentity.Name -split '\\')[-1] - $domain = ($winIdentity.Name -split '\\')[0] - - # create base user object - $user = @{ - UserType = 'Domain' - Identity = @{ - AccessToken = $winIdentity.AccessToken - } - AuthenticationType = $winIdentity.AuthenticationType - DistinguishedName = [string]::Empty - Username = $username - Name = [string]::Empty - Email = [string]::Empty - Fqdn = [string]::Empty - Domain = $domain - Groups = @() - } - - # if the domain isn't local, attempt AD user - if (![string]::IsNullOrWhiteSpace($domain) -and (@('.', $PodeContext.Server.ComputerName) -inotcontains $domain)) { - # get the server's fdqn (and name/email) - try { - # Open ADSISearcher and change context to given domain - $searcher = [adsisearcher]'' - $searcher.SearchRoot = [adsi]"LDAP://$($domain)" - $searcher.Filter = "ObjectSid=$($winIdentity.User.Value.ToString())" - - # Query the ADSISearcher for the above defined SID - $ad = $searcher.FindOne() - - # Save it to our existing array for later usage - $user.DistinguishedName = @($ad.Properties.distinguishedname)[0] - $user.Name = @($ad.Properties.name)[0] - $user.Email = @($ad.Properties.mail)[0] - $user.Fqdn = (Get-PodeADServerFromDistinguishedName -DistinguishedName $user.DistinguishedName) - } - finally { - Close-PodeDisposable -Disposable $searcher - } - - try { - if (!$options.NoGroups) { - - # open a new connection - $result = (Open-PodeAuthADConnection -Server $user.Fqdn -Domain $domain -Provider $options.Provider) - if (!$result.Success) { - return @{ Message = "Failed to connect to Domain Server '$($user.Fqdn)' of $domain for $($user.DistinguishedName)." } - } - - # get the connection - $connection = $result.Connection - - # get the users groups - $directGroups = $options.DirectGroups - $user.Groups = (Get-PodeAuthADGroup -Connection $connection -DistinguishedName $user.DistinguishedName -Username $user.Username -Direct:$directGroups -Provider $options.Provider) - } - } - finally { - if ($null -ne $connection) { - Close-PodeDisposable -Disposable $connection.Searcher - Close-PodeDisposable -Disposable $connection.Entry -Close - $connection.Credential = $null - } - } - } - - # otherwise, get details of local user - else { - # get the user's name and groups - try { - $user.UserType = 'Local' - - if (!$options.NoLocalCheck) { - $localUser = $winIdentity.Name -replace '\\', '/' - $ad = [adsi]"WinNT://$($localUser)" - $user.Name = @($ad.FullName)[0] - - # dirty, i know :/ - since IIS runs using pwsh, the InvokeMember part fails - # we can safely call windows powershell here, as IIS is only on windows. - if (!$options.NoGroups) { - $cmd = "`$ad = [adsi]'WinNT://$($localUser)'; @(`$ad.Groups() | Foreach-Object { `$_.GetType().InvokeMember('Name', 'GetProperty', `$null, `$_, `$null) })" - $user.Groups = [string[]](powershell -c $cmd) - } - } - } - finally { - Close-PodeDisposable -Disposable $ad -Close - } - } - } - catch { - $_ | Write-PodeErrorLog - return @{ Message = 'Failed to retrieve user using Authentication Token' } - } - finally { - $win32Handler::CloseHandle($winAuthToken) - } - - # is the user valid for any users/groups - if not, error! - if (!(Test-PodeAuthUserGroup -User $user -Users $options.Users -Groups $options.Groups)) { - return @{ Message = 'You are not authorised to access this website' } - } - - $result = @{ User = $user } - - # call additional scriptblock if supplied - if ($null -ne $options.ScriptBlock.Script) { - $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $options.ScriptBlock.Script -UsingVariables $options.ScriptBlock.UsingVariables - } - - # return final result, this could contain a user obj, or an error message from custom scriptblock - return $result - } -} - -<# -.SYNOPSIS - Authenticates a user based on group membership or specific user authorization. - -.DESCRIPTION - This function checks if a given user is authorized based on supplied lists of users and groups. The user is considered authorized if their username is directly specified in the list of users, or if they are a member of any of the specified groups. - -.PARAMETER User - A hashtable representing the user, expected to contain at least the 'Username' and 'Groups' keys. - -.PARAMETER Users - An optional array of usernames. If specified, the function checks if the user's username exists in this list. - -.PARAMETER Groups - An optional array of group names. If specified, the function checks if the user belongs to any of these groups. - -.EXAMPLE - $user = @{ Username = 'john.doe'; Groups = @('Administrators', 'Users') } - $authorizedUsers = @('john.doe', 'jane.doe') - $authorizedGroups = @('Administrators') - - Test-PodeAuthUserGroup -User $user -Users $authorizedUsers -Groups $authorizedGroups - # Returns true if John Doe is either listed as an authorized user or is a member of an authorized group. -#> -function Test-PodeAuthUserGroup { - param( - [Parameter(Mandatory = $true)] - [hashtable] - $User, - - [Parameter()] - [string[]] - $Users, - - [Parameter()] - [string[]] - $Groups - ) - - $haveUsers = (($null -ne $Users) -and ($Users.Length -gt 0)) - $haveGroups = (($null -ne $Groups) -and ($Groups.Length -gt 0)) - - # if there are no groups/users supplied, return user is valid - if (!$haveUsers -and !$haveGroups) { - return $true - } - - # before checking supplied groups, is the user in the supplied list of authorised users? - if ($haveUsers -and (@($Users) -icontains $User.Username)) { - return $true - } - - # if there are groups supplied, check the user is a member of one - if ($haveGroups) { - foreach ($group in $Groups) { - if (@($User.Groups) -icontains $group) { - return $true - } - } - } - - return $false -} - -function Invoke-PodeAuthValidation { - param( - [Parameter(Mandatory = $true)] - [string] - $Name - ) - - # get auth method - $auth = $PodeContext.Server.Authentications.Methods[$Name] - - # if it's a merged auth, re-call this function and check against "succeed" value - if ($auth.Merged) { - $results = @{} - foreach ($authName in $auth.Authentications) { - $result = Invoke-PodeAuthValidation -Name $authName - - # if the auth is trying to redirect, we need to bubble the this back now - if ($result.Redirected) { - return $result - } - - # if the auth passed, and we only need one auth to pass, return current result - if ($result.Success -and $auth.PassOne) { - return $result - } - - # if the auth failed, but we need all to pass, return current result - if (!$result.Success -and !$auth.PassOne) { - return $result - } - - # remember result if we need all to pass - if (!$auth.PassOne) { - $results[$authName] = $result - } - } - # if the last auth failed, and we only need one auth to pass, set failure and return - if (!$result.Success -and $auth.PassOne) { - return $result - } - - # if the last auth succeeded, and we need all to pass, merge users/headers and return result - if ($result.Success -and !$auth.PassOne) { - # invoke scriptblock, or use result of merge default - if ($null -ne $auth.ScriptBlock.Script) { - $result = Invoke-PodeAuthInbuiltScriptBlock -User $results -ScriptBlock $auth.ScriptBlock.Script -UsingVariables $auth.ScriptBlock.UsingVariables - } - else { - $result = $results[$auth.MergeDefault] - } - - # reset default properties and return - $result.Success = $true - $result.Auth = $results.Keys - return $result - } - - # default failure - return @{ - Success = $false - StatusCode = 500 - } - } - - # main auth validation logic - $result = (Test-PodeAuthValidation -Name $Name) - $result.Auth = $Name - return $result -} - -<# -.SYNOPSIS - Tests the authentication validation for a specified authentication method. - -.DESCRIPTION - The `Test-PodeAuthValidation` function processes an authentication method by its name, - running the associated scripts, middleware, and validations to determine authentication success or failure. - -.PARAMETER Name - The name of the authentication method to validate. This parameter is mandatory. - -.PARAMETER NoMiddlewareAuthentication - A switch to indicate whether the function has to threat the authentication because no Middleware authentication has been executed. - -.OUTPUTS - A hashtable containing the authentication validation result, including success status, user details, - headers, and redirection information if applicable. - -.NOTES - This is an internal function and is subject to change in future versions of Pode. -#> -function Test-PodeAuthValidation { - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [switch] - $NoMiddlewareAuthentication - ) - - try { - # Retrieve authentication method configuration from Pode context - $auth = $PodeContext.Server.Authentications.Methods[$Name] - - # Initialize authentication result variable - $result = $null - - # Run pre-authentication middleware if defined - if ($null -ne $auth.Scheme.Middleware) { - if (!(Invoke-PodeMiddleware -Middleware $auth.Scheme.Middleware)) { - return @{ - Success = $false - } - } - } - - # Prepare arguments for the authentication scheme script - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $auth.Scheme.Arguments -UsingVariables $auth.Scheme.ScriptBlock.UsingVariables) - - # Handle inner authentication schemes (if any) - if ($null -ne $auth.Scheme.InnerScheme) { - $schemes = @() - $_scheme = $auth.Scheme - - # Traverse through the inner schemes to collect them - $_inner = @(while ($null -ne $_scheme.InnerScheme) { - $_scheme = $_scheme.InnerScheme - $_scheme - }) - - # Process inner schemes in reverse order - for ($i = $_inner.Length - 1; $i -ge 0; $i--) { - $_tmp_args = @(Merge-PodeScriptblockArguments -ArgumentList $_inner[$i].Arguments -UsingVariables $_inner[$i].ScriptBlock.UsingVariables) - $_tmp_args += , $schemes - - $result = (Invoke-PodeScriptBlock -ScriptBlock $_inner[$i].ScriptBlock.Script -Arguments $_tmp_args -Return -Splat) - if ($result -is [hashtable]) { - break # Exit if a valid result is returned - } - - $schemes += , $result - $result = $null - } - - $_args += , $schemes - } - - # Execute the primary authentication script if no result from inner schemes and not a route script - if ($null -eq $result) { - $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.ScriptBlock.Script -Arguments $_args -Return -Splat) - } - - # Remove the Middleware processed data if code is 400 - no token - if ($NoMiddlewareAuthentication -and (($result.Code -eq 400) -or ($result.Code -eq 401))) { - $headers = $result.Headers - $result = '' - $code = 401 - } - write-podehost $result -Explode - # If authentication script returns a non-hashtable, perform further validation - if ($result -isnot [hashtable]) { - $original = $result - $_args = @($result) + @($auth.Arguments) - - # Run main authentication validation script - $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.ScriptBlock -Arguments $_args -UsingVariables $auth.UsingVariables -Return -Splat) - - # Run post-authentication validation if applicable - if ([string]::IsNullOrEmpty($result.Code) -and ($null -ne $auth.Scheme.PostValidator.Script)) { - $_args = @($original) + @($result) + @($auth.Scheme.Arguments) - $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.PostValidator.Script -Arguments $_args -UsingVariables $auth.Scheme.PostValidator.UsingVariables -Return -Splat) - } - } - else { - # if ($result.Headers.ContainsKey('WWW-Authenticate')) { - # Add-PodeHeader -Name 'WWW-Authenticate' -Value $result.Headers['WWW-Authenticate'] - # } - } - write-podehost $result -Explode - # Handle authentication redirection scenarios (e.g., OAuth) - if ($result.IsRedirected) { - return @{ - Success = $false - Redirected = $true - } - } - - # Handle results when invoked from a route script - if ($NoMiddlewareAuthentication -and ($null -ne $result) -and ($result -is [hashtable])) { - if ($result.Success -is [bool]) { - $success = $result.Success - } - else { - $success = $false - [System.Exception]::new("The authentication Scriptblock must return an hashtable with a key named 'Success'") | Write-PodeErrorLog - } - - $ret = @{ - Success = $success - User = '' - Headers = $headers - IsAuthenticated = $success - IsAuthorised = $success - Store = !$auth.Sessionless - Name = $Name - } - foreach ($key in $result.Keys) { - $ret[$key] = $result[$key] # Overwrites if key exists - } - - return $ret - } - - # Authentication failure handling - if (($null -eq $result) -or ($result.Count -eq 0) -or (Test-PodeIsEmpty $result.User)) { - $code = (Protect-PodeValue -Value $result.Code -Default 401) - - # Set WWW-Authenticate header for appropriate HTTP response - $validCode = (($code -eq 401) -or ![string]::IsNullOrEmpty($result.Challenge)) - Write-podehost "validCode =$validCode" - if ($validCode) { - if ($null -eq $result) { - $result = @{} - } - - if ($null -eq $result.Headers) { - $result.Headers = @{} - } - - # Generate authentication challenge header - if (![string]::IsNullOrWhiteSpace($auth.Scheme.Name) -and !$result.Headers.ContainsKey('WWW-Authenticate')) { - write-podehost 'Get-PodeAuthWwwHeaderValue' - $authHeader = Get-PodeAuthWwwHeaderValue -Name $auth.Scheme.Name -Realm $auth.Scheme.Realm -Challenge $result.Challenge - $result.Headers['WWW-Authenticate'] = $authHeader - } - else { - $authHeader = Get-PodeAuthWwwHeaderValue -Name $auth.Scheme.Name -Realm $auth.Scheme.Realm -Challenge $result.Challenge - write-podehost $authHeader - $result.Headers['WWW-Authenticate'] = $authHeader - write-podehost 'no Get-PodeAuthWwwHeaderValue' - } - } - - return @{ - Success = $false - StatusCode = $code - Description = $result.Message - Headers = $result.Headers - FailureRedirect = [bool]$result.IsErrored - } - } - - # Authentication succeeded, return user and headers - return @{ - Success = $true - User = $result.User - Headers = $result.Headers - } - } - catch { - $_ | Write-PodeErrorLog - - # Handle unexpected errors and log them - return @{ - Success = $false - StatusCode = 500 - Exception = $_ - } - } -} - - -function Get-PodeAuthMiddlewareScript { - return { - param($opts) - - return Test-PodeAuthInternal ` - -Name $opts.Name ` - -Login:($opts.Login) ` - -Logout:($opts.Logout) ` - -AllowAnon:($opts.Anon) - } -} - -function Test-PodeAuthInternal { - [CmdletBinding()] - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [switch] - $Login, - - [switch] - $Logout, - - [switch] - $AllowAnon - ) - - # get the auth method - $auth = $PodeContext.Server.Authentications.Methods[$Name] - - # check for logout command - if ($Logout) { - Remove-PodeAuthSession - - if ($PodeContext.Server.Sessions.Info.UseHeaders) { - return Set-PodeAuthStatus ` - -StatusCode 401 ` - -Name $Name ` - -NoSuccessRedirect - } - else { - $auth.Failure.Url = (Protect-PodeValue -Value $auth.Failure.Url -Default $WebEvent.Request.Url.AbsolutePath) - return Set-PodeAuthStatus ` - -StatusCode 302 ` - -Name $Name ` - -NoSuccessRedirect - } - } - - # if the session already has a user/isAuth'd, then skip auth - or allow anon - if (Test-PodeSessionsInUse) { - # existing session auth'd - if (Test-PodeAuthUser) { - $WebEvent.Auth = $WebEvent.Session.Data.Auth - return Set-PodeAuthStatus ` - -Name $Name ` - -LoginRoute:($Login) ` - -NoSuccessRedirect - } - - # if we're allowing anon access, and using sessions, then stop here - as a session will be created from a login route for auth'ing users - if ($AllowAnon) { - if (!(Test-PodeIsEmpty $WebEvent.Session.Data.Auth)) { - Revoke-PodeSession - } - - return $true - } - } - - # check if the login flag is set, in which case just return and load a login get-page (allowing anon access) - if ($Login -and !$PodeContext.Server.Sessions.Info.UseHeaders -and ($WebEvent.Method -ieq 'get')) { - if (!(Test-PodeIsEmpty $WebEvent.Session.Data.Auth)) { - Revoke-PodeSession - } - - return $true - } - - try { - $result = Invoke-PodeAuthValidation -Name $Name - } - catch { - $_ | Write-PodeErrorLog - return Set-PodeAuthStatus ` - -StatusCode 500 ` - -Description $_.Exception.Message ` - -Name $Name - } - - # did the auth force a redirect? - if ($result.Redirected) { - $success = Get-PodeAuthSuccessInfo -Name $Name - Set-PodeAuthRedirectUrl -UseOrigin:($success.UseOrigin) - return $false - } - - # if auth failed, are we allowing anon access? - if (!$result.Success -and $AllowAnon) { - return $true - } - - # if auth failed, set appropriate response headers/redirects - if (!$result.Success) { - return Set-PodeAuthStatus ` - -StatusCode $result.StatusCode ` - -Description $result.Description ` - -Headers $result.Headers ` - -Name $Name ` - -LoginRoute:$Login ` - -NoFailureRedirect:($result.FailureRedirect) - } - - # if auth passed, assign the user to the session - $WebEvent.Auth = [ordered]@{ - User = $result.User - IsAuthenticated = $true - IsAuthorised = $true - Store = !$auth.Sessionless - Name = $result.Auth - } - # successful auth - $authName = $null - if ($auth.Merged -and !$auth.PassOne) { - $authName = $Name - } - else { - $authName = @($result.Auth)[0] - } - - return Set-PodeAuthStatus ` - -Headers $result.Headers ` - -Name $authName ` - -LoginRoute:$Login -} - -function Get-PodeAuthWwwHeaderValue { - param( - [Parameter()] - [string] - $Name, - - [Parameter()] - [string] - $Realm, - - [Parameter()] - [string] - $Challenge - ) - - if ([string]::IsNullOrWhiteSpace($Name)) { - return [string]::Empty - } - - $header = $Name - if (![string]::IsNullOrWhiteSpace($Realm)) { - $header += " realm=`"$($Realm)`"" - } - - if (![string]::IsNullOrWhiteSpace($Challenge)) { - $header += ", $($Challenge)" - } - - return $header -} - -function Remove-PodeAuthSession { - # blank out the auth - $WebEvent.Auth = @{} - - # if a session auth is found, blank it - if (!(Test-PodeIsEmpty $WebEvent.Session.Data.Auth)) { - $WebEvent.Session.Data.Remove('Auth') - } - - # Delete the current session (remove from store, blank it, and remove from Response) - Revoke-PodeSession -} - -function Get-PodeAuthFailureInfo { - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter()] - [hashtable] - $Info, - - [Parameter()] - [string] - $BaseName - ) - - # base name - if ([string]::IsNullOrEmpty($BaseName)) { - $BaseName = $Name - } - - # get auth method - $auth = $PodeContext.Server.Authentications.Methods[$Name] - - # cached failure? - if ($null -ne $auth.Cache.Failure) { - return $auth.Cache.Failure - } - - # find failure info - if ($null -eq $Info) { - $Info = @{ - Url = $auth.Failure.Url - Message = $auth.Failure.Message - } - } - - if ([string]::IsNullOrEmpty($Info.Url)) { - $Info.Url = $auth.Failure.Url - } - - if ([string]::IsNullOrEmpty($Info.Message)) { - $Info.Message = $auth.Failure.Message - } - - if ((![string]::IsNullOrEmpty($Info.Url) -and ![string]::IsNullOrEmpty($Info.Message)) -or [string]::IsNullOrEmpty($auth.Parent)) { - $PodeContext.Server.Authentications.Methods[$BaseName].Cache.Failure = $Info - return $Info - } - - return (Get-PodeAuthFailureInfo -Name $auth.Parent -Info $Info -BaseName $BaseName) -} - -function Get-PodeAuthSuccessInfo { - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter()] - [hashtable] - $Info, - - [Parameter()] - [string] - $BaseName - ) - - # base name - if ([string]::IsNullOrEmpty($BaseName)) { - $BaseName = $Name - } - - # get auth method - $auth = $PodeContext.Server.Authentications.Methods[$Name] - - # cached success? - if ($null -ne $auth.Cache.Success) { - return $auth.Cache.Success - } - - # find success info - if ($null -eq $Info) { - $Info = @{ - Url = $auth.Success.Url - UseOrigin = $auth.Success.UseOrigin - } - } - - if ([string]::IsNullOrEmpty($Info.Url)) { - $Info.Url = $auth.Success.Url - } - - if (!$Info.UseOrigin) { - $Info.UseOrigin = $auth.Success.UseOrigin - } - - if ((![string]::IsNullOrEmpty($Info.Url) -and $Info.UseOrigin) -or [string]::IsNullOrEmpty($auth.Parent)) { - $PodeContext.Server.Authentications.Methods[$BaseName].Cache.Success = $Info - return $Info - } - - return (Get-PodeAuthSuccessInfo -Name $auth.Parent -Info $Info -BaseName $BaseName) -} - -function Set-PodeAuthStatus { - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter()] - [int] - $StatusCode = 0, - - [Parameter()] - [string] - $Description, - - [Parameter()] - [hashtable] - $Headers, - - [switch] - $LoginRoute, - - [switch] - $NoSuccessRedirect, - - [switch] - $NoFailureRedirect - ) - - # if we have any headers, set them - if (($null -ne $Headers) -and ($Headers.Count -gt 0)) { - foreach ($key in $Headers.Keys) { - Set-PodeHeader -Name $key -Value $Headers[$key] - } - } - - # get auth method - $auth = $PodeContext.Server.Authentications.Methods[$Name] - - # get Success object from auth - $success = Get-PodeAuthSuccessInfo -Name $Name - - # if a statuscode supplied, assume failure - if ($StatusCode -gt 0) { - # get Failure object from auth - $failure = Get-PodeAuthFailureInfo -Name $Name - - # override description with the failureMessage if supplied - $Description = (Protect-PodeValue -Value $failure.Message -Default $Description) - - # add error to flash - if ($LoginRoute -and !$auth.Sessionless -and ![string]::IsNullOrWhiteSpace($Description)) { - Add-PodeFlashMessage -Name 'auth-error' -Message $Description - } - - # check if we have a failure url redirect - if (!$NoFailureRedirect -and ![string]::IsNullOrWhiteSpace($failure.Url)) { - Set-PodeAuthRedirectUrl -UseOrigin:($success.UseOrigin) - Move-PodeResponseUrl -Url $failure.Url - } - else { - Set-PodeResponseStatus -Code $StatusCode -Description $Description - } - - return $false - } - - # if no statuscode, success, so check if we have a success url redirect (but only for auto-login routes) - if (!$NoSuccessRedirect -or $LoginRoute) { - $url = Get-PodeAuthRedirectUrl -Url $success.Url -UseOrigin:($success.UseOrigin) - if (![string]::IsNullOrWhiteSpace($url)) { - Move-PodeResponseUrl -Url $url - return $false - } - } - - return $true -} - -function Get-PodeADServerFromDistinguishedName { - param( - [Parameter()] - [string] - $DistinguishedName - ) - - if ([string]::IsNullOrWhiteSpace($DistinguishedName)) { - return [string]::Empty - } - - $parts = @($DistinguishedName -split ',') - $name = @() - - foreach ($part in $parts) { - if ($part -imatch '^DC=(?.+)$') { - $name += $Matches['name'] - } - } - - return ($name -join '.') -} - -function Get-PodeAuthADResult { - param( - [Parameter()] - [string] - $Server, - - [Parameter()] - [string] - $Domain, - - [Parameter()] - [string] - $SearchBase, - - [Parameter()] - [string] - $Username, - - [Parameter()] - [string] - $Password, - - [Parameter()] - [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')] - [string] - $Provider, - - [switch] - $NoGroups, - - [switch] - $DirectGroups, - - [switch] - $KeepCredential - ) - - try { - # validate the user's AD creds - $result = (Open-PodeAuthADConnection -Server $Server -Domain $Domain -Username $Username -Password $Password -Provider $Provider) - if (!$result.Success) { - return @{ Message = 'Invalid credentials supplied' } - } - - # get the connection - $connection = $result.Connection - - # get the user - $user = (Get-PodeAuthADUser -Connection $connection -Username $Username -Provider $Provider) - if ($null -eq $user) { - return @{ Message = 'User not found in Active Directory' } - } - - # get the users groups - $groups = @() - if (!$NoGroups) { - $groups = (Get-PodeAuthADGroup -Connection $connection -DistinguishedName $user.DistinguishedName -Username $Username -Direct:$DirectGroups -Provider $Provider) - } - - # check if we want to keep the credentials in the User object - if ($KeepCredential) { - $credential = [pscredential]::new($($Domain + '\' + $Username), (ConvertTo-SecureString -String $Password -AsPlainText -Force)) - } - else { - $credential = $null - } - - # return the user - return @{ - User = @{ - UserType = 'Domain' - AuthenticationType = 'LDAP' - DistinguishedName = $user.DistinguishedName - Username = ($Username -split '\\')[-1] - Name = $user.Name - Email = $user.Email - Fqdn = $Server - Domain = $Domain - Groups = $groups - Credential = $credential - } - } - } - finally { - if ($null -ne $connection) { - switch ($Provider.ToLowerInvariant()) { - 'openldap' { - $connection.Username = $null - $connection.Password = $null - } - - 'activedirectory' { - $connection.Credential = $null - } - - 'directoryservices' { - Close-PodeDisposable -Disposable $connection.Searcher - Close-PodeDisposable -Disposable $connection.Entry -Close - } - } - } - } -} - -function Open-PodeAuthADConnection { - param( - [Parameter(Mandatory = $true)] - [string] - $Server, - - [Parameter()] - [string] - $Domain, - - [Parameter()] - [string] - $SearchBase, - - [Parameter()] - [string] - $Username, - - [Parameter()] - [string] - $Password, - - [Parameter()] - [ValidateSet('LDAP', 'WinNT')] - [string] - $Protocol = 'LDAP', - - [Parameter()] - [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')] - [string] - $Provider - ) - - $result = $true - $connection = $null - - # validate the user's AD creds - switch ($Provider.ToLowerInvariant()) { - 'openldap' { - if (![string]::IsNullOrWhiteSpace($SearchBase)) { - $baseDn = $SearchBase - } - else { - $baseDn = "DC=$(($Server -split '\.') -join ',DC=')" - } - - $query = (Get-PodeAuthADQuery -Username $Username) - $hostname = "$($Protocol)://$($Server)" - - $user = $Username - if (!$Username.StartsWith($Domain)) { - $user = "$($Domain)\$($Username)" - } - - $null = (ldapsearch -x -LLL -H "$($hostname)" -D "$($user)" -w "$($Password)" -b "$($baseDn)" -o ldif-wrap=no "$($query)" dn) - if (!$? -or ($LASTEXITCODE -ne 0)) { - $result = $false - } - else { - $connection = @{ - Hostname = $hostname - Username = $user - BaseDN = $baseDn - Password = $Password - } - } - } - - 'activedirectory' { - try { - $creds = [pscredential]::new($Username, (ConvertTo-SecureString -String $Password -AsPlainText -Force)) - $null = Get-ADUser -Identity $Username -Credential $creds -ErrorAction Stop - $connection = @{ - Credential = $creds - } - } - catch { - $result = $false - } - } - - 'directoryservices' { - if ([string]::IsNullOrWhiteSpace($Password)) { - $ad = [System.DirectoryServices.DirectoryEntry]::new("$($Protocol)://$($Server)") - } - else { - $ad = [System.DirectoryServices.DirectoryEntry]::new("$($Protocol)://$($Server)", "$($Username)", "$($Password)") - } - - if (Test-PodeIsEmpty $ad.distinguishedName) { - $result = $false - } - else { - $connection = @{ - Entry = $ad - } - } - } - } - - return @{ - Success = $result - Connection = $connection - } -} - -function Get-PodeAuthADQuery { - param( - [Parameter(Mandatory = $true)] - [string] - $Username - ) - - return "(&(objectCategory=person)(samaccountname=$($Username)))" -} - -function Get-PodeAuthADUser { - param( - [Parameter(Mandatory = $true)] - $Connection, - - [Parameter(Mandatory = $true)] - [string] - $Username, - - [Parameter()] - [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')] - [string] - $Provider - ) - - $query = (Get-PodeAuthADQuery -Username $Username) - $user = $null - - # generate query to find user - switch ($Provider.ToLowerInvariant()) { - 'openldap' { - $result = (ldapsearch -x -LLL -H "$($Connection.Hostname)" -D "$($Connection.Username)" -w "$($Connection.Password)" -b "$($Connection.BaseDN)" -o ldif-wrap=no "$($query)" name mail) - if (!$? -or ($LASTEXITCODE -ne 0)) { - return $null - } - - $user = @{ - DistinguishedName = (Get-PodeOpenLdapValue -Lines $result -Property 'dn') - Name = (Get-PodeOpenLdapValue -Lines $result -Property 'name') - Email = (Get-PodeOpenLdapValue -Lines $result -Property 'mail') - } - } - - 'activedirectory' { - $result = Get-ADUser -LDAPFilter $query -Credential $Connection.Credential -Properties mail - $user = @{ - DistinguishedName = $result.DistinguishedName - Name = $result.Name - Email = $result.mail - } - } - - 'directoryservices' { - $Connection.Searcher = [System.DirectoryServices.DirectorySearcher]::new($Connection.Entry) - $Connection.Searcher.filter = $query - - $result = $Connection.Searcher.FindOne().Properties - if (Test-PodeIsEmpty $result) { - return $null - } - - $user = @{ - DistinguishedName = @($result.distinguishedname)[0] - Name = @($result.name)[0] - Email = @($result.mail)[0] - } - } - } - - return $user -} - -function Get-PodeOpenLdapValue { - param( - [Parameter()] - [string[]] - $Lines, - - [Parameter()] - [string] - $Property, - - [switch] - $All - ) - - foreach ($line in $Lines) { - if ($line -imatch "^$($Property)\:\s+(?<$($Property)>.+)$") { - # return the first found - if (!$All) { - return $Matches[$Property] - } - - # return array of all - $Matches[$Property] - } - } -} -<# -.SYNOPSIS - Retrieves Active Directory (AD) group information for a user. - -.DESCRIPTION - This function retrieves AD group information for a specified user. It supports two modes of operation: - 1. Direct: Retrieves groups directly associated with the user. - 2. All: Retrieves all groups within the specified distinguished name (DN). - -.PARAMETER Connection - The AD connection object or credentials for connecting to the AD server. - -.PARAMETER DistinguishedName - The distinguished name (DN) of the user or group. If not provided, the default DN is used. - -.PARAMETER Username - The username for which to retrieve group information. - -.PARAMETER Provider - The AD provider to use (e.g., 'DirectoryServices', 'ActiveDirectory', 'OpenLDAP'). - -.PARAMETER Direct - Switch parameter. If specified, retrieves only direct group memberships for the user. - -.OUTPUTS - Returns AD group information as needed based on the mode of operation. - -.EXAMPLE - Get-PodeAuthADGroup -Connection $adConnection -Username "john.doe" - # Retrieves all AD groups for the user "john.doe". - - Get-PodeAuthADGroup -Connection $adConnection -Username "jane.smith" -Direct - # Retrieves only direct group memberships for the user "jane.smith". -#> -function Get-PodeAuthADGroup { - param( - [Parameter(Mandatory = $true)] - $Connection, - - [Parameter()] - [string] - $DistinguishedName, - - [Parameter()] - [string] - $Username, - - [Parameter()] - [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')] - [string] - $Provider, - - [switch] - $Direct - ) - - if ($Direct) { - return (Get-PodeAuthADGroupDirect -Connection $Connection -Username $Username -Provider $Provider) - } - - return (Get-PodeAuthADGroupAll -Connection $Connection -DistinguishedName $DistinguishedName -Provider $Provider) -} - -function Get-PodeAuthADGroupDirect { - param( - [Parameter(Mandatory = $true)] - $Connection, - - [Parameter()] - [string] - $Username, - - [Parameter()] - [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')] - [string] - $Provider - ) - - # create the query - $query = "(&(objectCategory=person)(samaccountname=$($Username)))" - $groups = @() - - # get the groups - switch ($Provider.ToLowerInvariant()) { - 'openldap' { - $result = (ldapsearch -x -LLL -H "$($Connection.Hostname)" -D "$($Connection.Username)" -w "$($Connection.Password)" -b "$($Connection.BaseDN)" -o ldif-wrap=no "$($query)" memberof) - $groups = (Get-PodeOpenLdapValue -Lines $result -Property 'memberof' -All) - } - - 'activedirectory' { - $groups = (Get-ADPrincipalGroupMembership -Identity $Username -Credential $Connection.Credential).distinguishedName - } - - 'directoryservices' { - if ($null -eq $Connection.Searcher) { - $Connection.Searcher = [System.DirectoryServices.DirectorySearcher]::new($Connection.Entry) - } - - $Connection.Searcher.filter = $query - $groups = @($Connection.Searcher.FindOne().Properties.memberof) - } - } - - $groups = @(foreach ($group in $groups) { - if ($group -imatch '^CN=(?.+?),') { - $Matches['group'] - } - }) - - return $groups -} - -function Get-PodeAuthADGroupAll { - param( - [Parameter(Mandatory = $true)] - $Connection, - - [Parameter()] - [string] - $DistinguishedName, - - [Parameter()] - [ValidateSet('DirectoryServices', 'ActiveDirectory', 'OpenLDAP')] - [string] - $Provider - ) - - # create the query - $query = "(member:1.2.840.113556.1.4.1941:=$($DistinguishedName))" - $groups = @() - - # get the groups - switch ($Provider.ToLowerInvariant()) { - 'openldap' { - $result = (ldapsearch -x -LLL -H "$($Connection.Hostname)" -D "$($Connection.Username)" -w "$($Connection.Password)" -b "$($Connection.BaseDN)" -o ldif-wrap=no "$($query)" samaccountname) - $groups = (Get-PodeOpenLdapValue -Lines $result -Property 'sAMAccountName' -All) - } - - 'activedirectory' { - $groups = (Get-ADObject -LDAPFilter $query -Credential $Connection.Credential).Name - } - - 'directoryservices' { - if ($null -eq $Connection.Searcher) { - $Connection.Searcher = [System.DirectoryServices.DirectorySearcher]::new($Connection.Entry) - } - - $null = $Connection.Searcher.PropertiesToLoad.Add('samaccountname') - $Connection.Searcher.filter = $query - $groups = @($Connection.Searcher.FindAll().Properties.samaccountname) - } - } - - return $groups -} - -function Get-PodeAuthDomainName { - $domain = $null - - if (Test-PodeIsMacOS) { - $domain = (scutil --dns | grep -m 1 'search domain\[0\]' | cut -d ':' -f 2) - } - elseif (Test-PodeIsUnix) { - $domain = (dnsdomainname) - if ([string]::IsNullOrWhiteSpace($domain)) { - $domain = (/usr/sbin/realm list --name-only) - } - } - else { - $domain = $env:USERDNSDOMAIN - if ([string]::IsNullOrWhiteSpace($domain)) { - $domain = (Get-CimInstance -Class Win32_ComputerSystem -Verbose:$false).Domain - } - } - - if (![string]::IsNullOrEmpty($domain)) { - $domain = $domain.Trim() - } - - return $domain -} - -function Find-PodeAuth { - param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string] - $Name - ) - - return $PodeContext.Server.Authentications.Methods[$Name] -} - -<# -.SYNOPSIS - Expands a list of authentication names, including merged authentication methods. - -.DESCRIPTION - The Expand-PodeAuthMerge function takes an array of authentication names and expands it by resolving any merged authentication methods - into their individual components. It is particularly useful in scenarios where authentication methods are combined or merged, and there - is a need to process each individual method separately. - -.PARAMETER Names - An array of authentication method names. These names can include both discrete authentication methods and merged ones. - -.EXAMPLE - $expandedAuthNames = Expand-PodeAuthMerge -Names @('BasicAuth', 'CustomMergedAuth') - - Expands the provided authentication names, resolving 'CustomMergedAuth' into its constituent authentication methods if it's a merged one. -#> -function Expand-PodeAuthMerge { - param ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string[]] - $Names - ) - - # Initialize a hashtable to store expanded authentication names - $authNames = @{} - - # Iterate over each authentication name - foreach ($authName in $Names) { - # Handle the special case of anonymous access - if ($authName -eq '%_allowanon_%') { - $authNames[$authName] = $true - } - else { - # Retrieve the authentication method from the Pode context - $_auth = $PodeContext.Server.Authentications.Methods[$authName] - - # Check if the authentication is a merged one and expand it - if ($_auth.merged) { - foreach ($key in (Expand-PodeAuthMerge -Names $_auth.Authentications)) { - $authNames[$key] = $true - } - } - else { - # If not merged, add the authentication name to the list - $authNames[$_auth.Name] = $true - } - } - } - - # Return the keys of the hashtable, which are the expanded authentication names - return $authNames.Keys -} - - -function Import-PodeAuthADModule { - if (!(Test-PodeIsWindows)) { - # Active Directory module only available on Windows - throw ($PodeLocale.adModuleWindowsOnlyExceptionMessage) - } - - if (!(Test-PodeModuleInstalled -Name ActiveDirectory)) { - # Active Directory module is not installed - throw ($PodeLocale.adModuleNotInstalledExceptionMessage) - } - - Import-Module -Name ActiveDirectory -Force -ErrorAction Stop - Export-PodeModule -Name ActiveDirectory -} - -function Get-PodeAuthADProvider { - param( - [switch] - $OpenLDAP, - - [switch] - $ADModule - ) - - # openldap (literal, or not windows) - if ($OpenLDAP -or !(Test-PodeIsWindows)) { - return 'OpenLDAP' - } - - # ad module - if ($ADModule) { - return 'ActiveDirectory' - } - - # ds - return 'DirectoryServices' -} - -function Set-PodeAuthRedirectUrl { - param( - [switch] - $UseOrigin - ) - - if ($UseOrigin -and ($WebEvent.Method -ieq 'get')) { - $null = Set-PodeCookie -Name 'pode.redirecturl' -Value $WebEvent.Request.Url.PathAndQuery - } -} - -function Get-PodeAuthRedirectUrl { - param( - [Parameter()] - [string] - $Url, - - [switch] - $UseOrigin - ) - - if (!$UseOrigin) { - return $Url - } - - $tmpUrl = Get-PodeCookieValue -Name 'pode.redirecturl' - Remove-PodeCookie -Name 'pode.redirecturl' - - if (![string]::IsNullOrWhiteSpace($tmpUrl)) { - $Url = $tmpUrl - } - - return $Url -} \ No newline at end of file diff --git a/my modified authentication - Public.ps1 b/my modified authentication - Public.ps1 deleted file mode 100644 index 2992520e2..000000000 --- a/my modified authentication - Public.ps1 +++ /dev/null @@ -1,2690 +0,0 @@ -<# -.SYNOPSIS -Create a new type of Authentication scheme. - -.DESCRIPTION -Create a new type of Authentication scheme, which is used to parse the Request for user credentials for validating. - -.PARAMETER Basic -If supplied, will use the inbuilt Basic Authentication credentials retriever. - -.PARAMETER Encoding -The Encoding to use when decoding the Basic Authorization header. - -.PARAMETER HeaderTag -The Tag name used in the Authorization header, ie: Basic, Bearer, Digest. - -.PARAMETER Form -If supplied, will use the inbuilt Form Authentication credentials retriever. - -.PARAMETER UsernameField -The name of the Username Field in the payload to retrieve the username. - -.PARAMETER PasswordField -The name of the Password Field in the payload to retrieve the password. - -.PARAMETER Custom -If supplied, will allow you to create a Custom Authentication credentials retriever. - -.PARAMETER ScriptBlock -The ScriptBlock is used to parse the request and retieve user credentials and other information. - -.PARAMETER ArgumentList -An array of arguments to supply to the Custom Authentication type's ScriptBlock. - -.PARAMETER Name -The Name of an Authentication type - such as Basic or NTLM. - -.PARAMETER Description -A short description for security scheme. CommonMark syntax MAY be used for rich text representation - -.PARAMETER Realm -The name of scope of the protected area. - -.PARAMETER Type -The scheme type for custom Authentication types. Default is HTTP. - -.PARAMETER Middleware -An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. - -.PARAMETER PostValidator -The PostValidator is a scriptblock that is invoked after user validation. - -.PARAMETER Digest -If supplied, will use the inbuilt Digest Authentication credentials retriever. - -.PARAMETER Bearer -If supplied, will use the inbuilt Bearer Authentication token retriever. - -.PARAMETER Algorithm: -The hashing algorithm used by Digest (e.g., MD5, SHA-256) default SHA-256. - -.PARAMETER ClientCertificate -If supplied, will use the inbuilt Client Certificate Authentication scheme. - -.PARAMETER ClientId -The Application ID generated when registering a new app for OAuth2. - -.PARAMETER ClientSecret -The Application Secret generated when registering a new app for OAuth2 (this is optional when using PKCE). - -.PARAMETER RedirectUrl -An optional OAuth2 Redirect URL (default: /oauth2/callback) - -.PARAMETER AuthoriseUrl -The OAuth2 Authorisation URL to authenticate a User. This is optional if you're using an InnerScheme like Basic/Form. - -.PARAMETER TokenUrl -The OAuth2 Token URL to acquire an access token. - -.PARAMETER UserUrl -An optional User profile URL to retrieve a user's details - for OAuth2 - -.PARAMETER UserUrlMethod -An optional HTTP method to use when calling the User profile URL - for OAuth2 (Default: Post) - -.PARAMETER CodeChallengeMethod -An optional method for sending a PKCE code challenge when calling the Authorise URL - for OAuth2 (Default: S256) - -.PARAMETER UsePKCE -If supplied, OAuth2 authentication will use PKCE code verifiers - for OAuth2 - -.PARAMETER OAuth2 -If supplied, will use the inbuilt OAuth2 Authentication scheme. - -.PARAMETER Scope -An optional array of Scopes for Bearer/OAuth2 Authentication. (These are case-sensitive) - -.PARAMETER ApiKey -If supplied, will use the inbuilt API key Authentication scheme. - -.PARAMETER Location -The Location to find an API key: Header, Query, or Cookie. (Default: Header) - -.PARAMETER LocationName -The Name of the Header, Query, or Cookie to find an API key. (Default depends on Location. Header/Cookie: X-API-KEY, Query: api_key) - -.PARAMETER InnerScheme -An optional authentication Scheme (from New-PodeAuthScheme) that will be called prior to this Scheme. - -.PARAMETER AsCredential -If supplied, username/password credentials for Basic/Form authentication will instead be supplied as a pscredential object. - -.PARAMETER AsJWT -If supplied, the token/key supplied for Bearer/API key authentication will be parsed as a JWT, and the payload supplied instead. - -.PARAMETER Secret -An optional Secret, used to sign/verify JWT signatures. - -.EXAMPLE -$basic_auth = New-PodeAuthScheme -Basic - -.EXAMPLE -$form_auth = New-PodeAuthScheme -Form -UsernameField 'Email' - -.EXAMPLE -$custom_auth = New-PodeAuthScheme -Custom -ScriptBlock { /* logic */ } -#> -function New-PodeAuthScheme { - [CmdletBinding(DefaultParameterSetName = 'Basic')] - [OutputType([hashtable])] - param( - [Parameter(ParameterSetName = 'Basic')] - [switch] - $Basic, - - [Parameter(ParameterSetName = 'Basic')] - [string] - $Encoding = 'ISO-8859-1', - - [Parameter(ParameterSetName = 'Basic')] - [Parameter(ParameterSetName = 'Bearer')] - [Parameter(ParameterSetName = 'Digest')] - [string] - $HeaderTag, - - [Parameter(ParameterSetName = 'Form')] - [switch] - $Form, - - [Parameter(ParameterSetName = 'Form')] - [string] - $UsernameField = 'username', - - [Parameter(ParameterSetName = 'Form')] - [string] - $PasswordField = 'password', - - [Parameter(ParameterSetName = 'Custom')] - [switch] - $Custom, - - [Parameter(Mandatory = $true, ParameterSetName = 'Custom')] - [ValidateScript({ - if (Test-PodeIsEmpty $_) { - # A non-empty ScriptBlock is required for the Custom authentication scheme - throw ($PodeLocale.nonEmptyScriptBlockRequiredForCustomAuthExceptionMessage) - } - - return $true - })] - [scriptblock] - $ScriptBlock, - - [Parameter(ParameterSetName = 'Custom')] - [hashtable] - $ArgumentList, - - [Parameter(ParameterSetName = 'Custom')] - [string] - $Name, - - [string] - $Description, - - [Parameter()] - [string] - $Realm, - - [Parameter(ParameterSetName = 'Custom')] - [ValidateSet('ApiKey', 'Http', 'OAuth2', 'OpenIdConnect')] - [string] - $Type = 'Http', - - [Parameter()] - [object[]] - $Middleware, - - [Parameter(ParameterSetName = 'Custom')] - [scriptblock] - $PostValidator = $null, - - [Parameter(ParameterSetName = 'Digest')] - [switch] - $Digest, - - [Parameter(ParameterSetName = 'Bearer')] - [switch] - $Bearer, - - [Parameter(ParameterSetName = 'Digest')] - [string] - $Algorithm = 'MD5', - - [Parameter(ParameterSetName = 'ClientCertificate')] - [switch] - $ClientCertificate, - - [Parameter(ParameterSetName = 'OAuth2', Mandatory = $true)] - [string] - $ClientId, - - [Parameter(ParameterSetName = 'OAuth2')] - [string] - $ClientSecret, - - [Parameter(ParameterSetName = 'OAuth2')] - [string] - $RedirectUrl, - - [Parameter(ParameterSetName = 'OAuth2')] - [string] - $AuthoriseUrl, - - [Parameter(ParameterSetName = 'OAuth2', Mandatory = $true)] - [string] - $TokenUrl, - - [Parameter(ParameterSetName = 'OAuth2')] - [string] - $UserUrl, - - [Parameter(ParameterSetName = 'OAuth2')] - [ValidateSet('Get', 'Post')] - [string] - $UserUrlMethod = 'Post', - - [Parameter(ParameterSetName = 'OAuth2')] - [ValidateSet('plain', 'S256')] - [string] - $CodeChallengeMethod = 'S256', - - [Parameter(ParameterSetName = 'OAuth2')] - [switch] - $UsePKCE, - - [Parameter(ParameterSetName = 'OAuth2')] - [switch] - $OAuth2, - - [Parameter(ParameterSetName = 'ApiKey')] - [switch] - $ApiKey, - - [Parameter(ParameterSetName = 'ApiKey')] - [ValidateSet('Header', 'Query', 'Cookie')] - [string] - $Location = 'Header', - - [Parameter(ParameterSetName = 'ApiKey')] - [string] - $LocationName, - - [Parameter(ParameterSetName = 'Bearer')] - [Parameter(ParameterSetName = 'OAuth2')] - [string[]] - $Scope, - - [Parameter(ValueFromPipeline = $true)] - [hashtable] - $InnerScheme, - - [Parameter(ParameterSetName = 'Basic')] - [Parameter(ParameterSetName = 'Form')] - [switch] - $AsCredential, - - [Parameter(ParameterSetName = 'Bearer')] - [Parameter(ParameterSetName = 'ApiKey')] - [switch] - $AsJWT, - - [Parameter(ParameterSetName = 'Bearer')] - [Parameter(ParameterSetName = 'ApiKey')] - [string] - $Secret - ) - begin { - $pipelineItemCount = 0 - } - - process { - $pipelineItemCount++ - } - - end { - if ($pipelineItemCount -gt 1) { - throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) - } - # default realm - $_realm = 'User' - - # convert any middleware into valid hashtables - $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState) - - # configure the auth scheme - switch ($PSCmdlet.ParameterSetName.ToLowerInvariant()) { - 'basic' { - return @{ - Name = (Protect-PodeValue -Value $HeaderTag -Default 'Basic') - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthBasicType) - UsingVariables = $null - } - PostValidator = $null - Middleware = $Middleware - InnerScheme = $InnerScheme - Scheme = 'http' - Arguments = @{ - Description = $Description - HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Basic') - Encoding = (Protect-PodeValue -Value $Encoding -Default 'ISO-8859-1') - AsCredential = $AsCredential - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - } - } - } - - 'clientcertificate' { - return @{ - Name = 'Mutual' - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthClientCertificateType) - UsingVariables = $null - } - PostValidator = $null - Middleware = $Middleware - InnerScheme = $InnerScheme - Scheme = 'http' - Arguments = @{ - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - } - } - } - - 'digest' { - return @{ - Name = 'Digest' - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthDigestType) - UsingVariables = $null - } - PostValidator = @{ - Script = (Get-PodeAuthDigestPostValidator) - UsingVariables = $null - } - Middleware = $Middleware - InnerScheme = $InnerScheme - Scheme = 'http' - Arguments = @{ - HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Digest') - # Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - # Algorithm = (Protect-PodeValue -Value $Algorithm -Default 'MD5') - } - } - } - - 'bearer' { - $secretBytes = $null - if (![string]::IsNullOrWhiteSpace($Secret)) { - $secretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret) - } - - return @{ - Name = 'Bearer' - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthBearerType) - UsingVariables = $null - } - PostValidator = @{ - Script = (Get-PodeAuthBearerPostValidator) - UsingVariables = $null - } - Middleware = $Middleware - Scheme = 'http' - InnerScheme = $InnerScheme - Arguments = @{ - Description = $Description - HeaderTag = (Protect-PodeValue -Value $HeaderTag -Default 'Bearer') - Scopes = $Scope - AsJWT = $AsJWT - Secret = $secretBytes - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - } - } - } - - 'form' { - return @{ - Name = 'Form' - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthFormType) - UsingVariables = $null - } - PostValidator = $null - Middleware = $Middleware - InnerScheme = $InnerScheme - Scheme = 'http' - Arguments = @{ - Description = $Description - Fields = @{ - Username = (Protect-PodeValue -Value $UsernameField -Default 'username') - Password = (Protect-PodeValue -Value $PasswordField -Default 'password') - } - AsCredential = $AsCredential - } - } - } - - 'oauth2' { - if (($null -ne $InnerScheme) -and ($InnerScheme.Name -inotin @('basic', 'form'))) { - # OAuth2 InnerScheme can only be one of either Basic or Form authentication, but got: {0} - throw ($PodeLocale.oauth2InnerSchemeInvalidExceptionMessage -f $InnerScheme.Name) - } - - if (($null -eq $InnerScheme) -and [string]::IsNullOrWhiteSpace($AuthoriseUrl)) { - # OAuth2 requires an Authorise URL to be supplied - throw ($PodeLocale.oauth2RequiresAuthorizeUrlExceptionMessage) - } - - if ($UsePKCE -and !(Test-PodeSessionsEnabled)) { - # Sessions are required to use OAuth2 with PKCE - throw ($PodeLocale.sessionsRequiredForOAuth2WithPKCEExceptionMessage) - } - - if (!$UsePKCE -and [string]::IsNullOrEmpty($ClientSecret)) { - # OAuth2 requires a Client Secret when not using PKCE - throw ($PodeLocale.oauth2ClientSecretRequiredExceptionMessage) - } - return @{ - Name = 'OAuth2' - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthOAuth2Type) - UsingVariables = $null - } - PostValidator = $null - Middleware = $Middleware - Scheme = 'oauth2' - InnerScheme = $InnerScheme - Arguments = @{ - Description = $Description - Scopes = $Scope - PKCE = @{ - Enabled = $UsePKCE - CodeChallenge = @{ - Method = $CodeChallengeMethod - } - } - Client = @{ - ID = $ClientId - Secret = $ClientSecret - } - Urls = @{ - Redirect = $RedirectUrl - Authorise = $AuthoriseUrl - Token = $TokenUrl - User = @{ - Url = $UserUrl - Method = (Protect-PodeValue -Value $UserUrlMethod -Default 'Post') - } - } - } - } - } - - 'apikey' { - # set default location name - if ([string]::IsNullOrWhiteSpace($LocationName)) { - $LocationName = (@{ - Header = 'X-API-KEY' - Query = 'api_key' - Cookie = 'X-API-KEY' - })[$Location] - } - - $secretBytes = $null - if (![string]::IsNullOrWhiteSpace($Secret)) { - $secretBytes = [System.Text.Encoding]::UTF8.GetBytes($Secret) - } - - return @{ - Name = 'ApiKey' - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - ScriptBlock = @{ - Script = (Get-PodeAuthApiKeyType) - UsingVariables = $null - } - PostValidator = $null - Middleware = $Middleware - InnerScheme = $InnerScheme - Scheme = 'apiKey' - Arguments = @{ - Description = $Description - Location = $Location - LocationName = $LocationName - AsJWT = $AsJWT - Secret = $secretBytes - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - } - } - } - - 'custom' { - $ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - - if ($null -ne $PostValidator) { - $PostValidator, $usingPostVars = Convert-PodeScopedVariables -ScriptBlock $PostValidator -PSSession $PSCmdlet.SessionState - } - - return @{ - Name = $Name - Realm = (Protect-PodeValue -Value $Realm -Default $_realm) - InnerScheme = $InnerScheme - Scheme = $Type.ToLowerInvariant() - ScriptBlock = @{ - Script = $ScriptBlock - UsingVariables = $usingScriptVars - } - PostValidator = @{ - Script = $PostValidator - UsingVariables = $usingPostVars - } - Middleware = $Middleware - Arguments = $ArgumentList - } - } - } - } -} - -<# -.SYNOPSIS -Create an OAuth2 auth scheme for Azure AD. - -.DESCRIPTION -A wrapper for New-PodeAuthScheme and OAuth2, which builds an OAuth2 scheme for Azure AD. - -.PARAMETER Tenant -The Directory/Tenant ID from registering a new app (default: common). - -.PARAMETER ClientId -The Client ID from registering a new app. - -.PARAMETER ClientSecret -The Client Secret from registering a new app (this is optional when using PKCE). - -.PARAMETER RedirectUrl -An optional OAuth2 Redirect URL (default: /oauth2/callback) - -.PARAMETER InnerScheme -An optional authentication Scheme (from New-PodeAuthScheme) that will be called prior to this Scheme. - -.PARAMETER Middleware -An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. - -.PARAMETER UsePKCE -If supplied, OAuth2 authentication will use PKCE code verifiers. - -.EXAMPLE -New-PodeAuthAzureADScheme -Tenant 123-456-678 -ClientId some_id -ClientSecret 1234.abc - -.EXAMPLE -New-PodeAuthAzureADScheme -Tenant 123-456-678 -ClientId some_id -UsePKCE -#> -function New-PodeAuthAzureADScheme { - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter()] - [ValidateNotNullOrEmpty()] - [string] - $Tenant = 'common', - - [Parameter(Mandatory = $true)] - [string] - $ClientId, - - [Parameter()] - [string] - $ClientSecret, - - [Parameter()] - [string] - $RedirectUrl, - - [Parameter(ValueFromPipeline = $true)] - [hashtable] - $InnerScheme, - - [Parameter()] - [object[]] - $Middleware, - - [switch] - $UsePKCE - ) - begin { - $pipelineItemCount = 0 - } - - process { - - $pipelineItemCount++ - } - - end { - if ($pipelineItemCount -gt 1) { - throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) - } - return New-PodeAuthScheme ` - -OAuth2 ` - -ClientId $ClientId ` - -ClientSecret $ClientSecret ` - -AuthoriseUrl "https://login.microsoftonline.com/$($Tenant)/oauth2/v2.0/authorize" ` - -TokenUrl "https://login.microsoftonline.com/$($Tenant)/oauth2/v2.0/token" ` - -UserUrl 'https://graph.microsoft.com/oidc/userinfo' ` - -RedirectUrl $RedirectUrl ` - -InnerScheme $InnerScheme ` - -Middleware $Middleware ` - -UsePKCE:$UsePKCE - } -} - -<# -.SYNOPSIS -Create an OAuth2 auth scheme for Twitter. - -.DESCRIPTION -A wrapper for New-PodeAuthScheme and OAuth2, which builds an OAuth2 scheme for Twitter apps. - -.PARAMETER ClientId -The Client ID from registering a new app. - -.PARAMETER ClientSecret -The Client Secret from registering a new app (this is optional when using PKCE). - -.PARAMETER RedirectUrl -An optional OAuth2 Redirect URL (default: /oauth2/callback) - -.PARAMETER Middleware -An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. - -.PARAMETER UsePKCE -If supplied, OAuth2 authentication will use PKCE code verifiers. - -.EXAMPLE -New-PodeAuthTwitterScheme -ClientId some_id -ClientSecret 1234.abc - -.EXAMPLE -New-PodeAuthTwitterScheme -ClientId some_id -UsePKCE -#> -function New-PodeAuthTwitterScheme { - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter(Mandatory = $true)] - [string] - $ClientId, - - [Parameter()] - [string] - $ClientSecret, - - [Parameter()] - [string] - $RedirectUrl, - - [Parameter()] - [object[]] - $Middleware, - - [switch] - $UsePKCE - ) - - return New-PodeAuthScheme ` - -OAuth2 ` - -ClientId $ClientId ` - -ClientSecret $ClientSecret ` - -AuthoriseUrl 'https://twitter.com/i/oauth2/authorize' ` - -TokenUrl 'https://api.twitter.com/2/oauth2/token' ` - -UserUrl 'https://api.twitter.com/2/users/me' ` - -UserUrlMethod 'Get' ` - -RedirectUrl $RedirectUrl ` - -Middleware $Middleware ` - -Scope 'tweet.read', 'users.read' ` - -UsePKCE:$UsePKCE -} - -<# -.SYNOPSIS -Adds a custom Authentication method for verifying users. - -.DESCRIPTION -Adds a custom Authentication method for verifying users. - -.PARAMETER Name -A unique Name for the Authentication method. - -.PARAMETER Scheme -The authentication Scheme to use for retrieving credentials (From New-PodeAuthScheme). - -.PARAMETER ScriptBlock -The ScriptBlock defining logic that retrieves and verifys a user. - -.PARAMETER ArgumentList -An array of arguments to supply to the Custom Authentication's ScriptBlock. - -.PARAMETER FailureUrl -The URL to redirect to when authentication fails. - -.PARAMETER FailureMessage -An override Message to throw when authentication fails. - -.PARAMETER SuccessUrl -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 SuccessUseOrigin -If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. - -.EXAMPLE -New-PodeAuthScheme -Form | Add-PodeAuth -Name 'Main' -ScriptBlock { /* logic */ } -#> -function Add-PodeAuth { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [hashtable] - $Scheme, - - [Parameter(Mandatory = $true)] - [ValidateScript({ - if (Test-PodeIsEmpty $_) { - # A non-empty ScriptBlock is required for the authentication method - throw ($PodeLocale.nonEmptyScriptBlockRequiredForAuthMethodExceptionMessage) - } - - return $true - })] - [scriptblock] - $ScriptBlock, - - [Parameter()] - [object[]] - $ArgumentList, - - [Parameter()] - [string] - $FailureUrl, - - [Parameter()] - [string] - $FailureMessage, - - [Parameter()] - [string] - $SuccessUrl, - - [switch] - $Sessionless, - - [switch] - $SuccessUseOrigin - ) - begin { - $pipelineItemCount = 0 - } - - process { - - $pipelineItemCount++ - } - - end { - if ($pipelineItemCount -gt 1) { - throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) - } - # ensure the name doesn't already exist - if (Test-PodeAuthExists -Name $Name) { - # Authentication method already defined: {0} - throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) - } - - # ensure the Scheme contains a scriptblock - if (Test-PodeIsEmpty $Scheme.ScriptBlock) { - # The supplied scheme for the '{0}' authentication validator requires a valid ScriptBlock - throw ($PodeLocale.schemeRequiresValidScriptBlockExceptionMessage -f $Name) - } - - # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { - # Sessions are required to use session persistent authentication - throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) - } - - # check for scoped vars - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - - # add auth method to server - $PodeContext.Server.Authentications.Methods[$Name] = @{ - Name = $Name - Scheme = $Scheme - ScriptBlock = $ScriptBlock - UsingVariables = $usingVars - Arguments = $ArgumentList - Sessionless = $Sessionless.IsPresent - Failure = @{ - Url = $FailureUrl - Message = $FailureMessage - } - Success = @{ - Url = $SuccessUrl - UseOrigin = $SuccessUseOrigin.IsPresent - } - Cache = @{} - Merged = $false - Parent = $null - } - - # if the scheme is oauth2, and there's no redirect, set up a default one - if (($Scheme.Name -ieq 'oauth2') -and ($null -eq $Scheme.InnerScheme) -and [string]::IsNullOrWhiteSpace($Scheme.Arguments.Urls.Redirect)) { - $path = '/oauth2/callback' - $Scheme.Arguments.Urls.Redirect = $path - Add-PodeRoute -Method Get -Path $path -Authentication $Name - } - } -} - -<# -.SYNOPSIS -Lets you merge multiple Authentication methods together, into a "single" Authentication method. - -.DESCRIPTION -Lets you merge multiple Authentication methods together, into a "single" Authentication method. -You can specify if only One or All of the methods need to pass to allow access, and you can also -merge other merged Authentication methods for more advanced scenarios. - -.PARAMETER Name -A unique Name for the Authentication method. - -.PARAMETER Authentication -Multiple Autentication method Names to be merged. - -.PARAMETER Valid -How many of the Authentication methods are required to be valid, One or All. (Default: One) - -.PARAMETER ScriptBlock -This is mandatory, and only used, when $Valid=All. A scriptblock to merge the mutliple users/headers returned by valid authentications into 1 user/header objects. -This scriptblock will receive a hashtable of all result objects returned from Authentication methods. The key for the hashtable will be the authentication names that passed. - -.PARAMETER Default -The Default Authentication method to use as a fallback for Failure URLs and other settings. - -.PARAMETER MergeDefault -The Default Authentication method's User details result object to use, when $Valid=All. - -.PARAMETER FailureUrl -The URL to redirect to when authentication fails. -This will be used as fallback for the merged Authentication methods if not set on them. - -.PARAMETER FailureMessage -An override Message to throw when authentication fails. -This will be used as fallback for the merged Authentication methods if not set on them. - -.PARAMETER SuccessUrl -The URL to redirect to when authentication succeeds when logging in. -This will be used as fallback for the merged Authentication methods if not set on them. - -.PARAMETER Sessionless -If supplied, authenticated users will not be stored in sessions, and sessions will not be used. -This will be used as fallback for the merged Authentication methods if not set on them. - -.PARAMETER SuccessUseOrigin -If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. -This will be used as fallback for the merged Authentication methods if not set on them. - -.EXAMPLE -Merge-PodeAuth -Name MergedAuth -Authentication ApiTokenAuth, BasicAuth -Valid All -ScriptBlock { ... } - -.EXAMPLE -Merge-PodeAuth -Name MergedAuth -Authentication ApiTokenAuth, BasicAuth -Valid All -MergeDefault BasicAuth - -.EXAMPLE -Merge-PodeAuth -Name MergedAuth -Authentication ApiTokenAuth, BasicAuth -FailureUrl 'http://localhost:8080/login' -#> -function Merge-PodeAuth { - [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')] - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter(Mandatory = $true)] - [Alias('Auth')] - [string[]] - $Authentication, - - [Parameter()] - [ValidateSet('One', 'All')] - [string] - $Valid = 'One', - - [Parameter(ParameterSetName = 'ScriptBlock')] - [scriptblock] - $ScriptBlock, - - [Parameter()] - [string] - $Default, - - [Parameter(ParameterSetName = 'MergeDefault')] - [string] - $MergeDefault, - - [Parameter()] - [string] - $FailureUrl, - - [Parameter()] - [string] - $FailureMessage, - - [Parameter()] - [string] - $SuccessUrl, - - [switch] - $Sessionless, - - [switch] - $SuccessUseOrigin - ) - - # ensure the name doesn't already exist - if (Test-PodeAuthExists -Name $Name) { - # Authentication method already defined: { 0 } - throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) - } - - # ensure all the auth methods exist - foreach ($authName in $Authentication) { - if (!(Test-PodeAuthExists -Name $authName)) { - throw ($PodeLocale.authMethodNotExistForMergingExceptionMessage -f $authName) #"Authentication method does not exist for merging: $($authName)" - } - } - - # ensure the merge default is in the auth list - if (![string]::IsNullOrEmpty($MergeDefault) -and ($MergeDefault -inotin @($Authentication))) { - throw ($PodeLocale.mergeDefaultAuthNotInListExceptionMessage -f $MergeDefault) # "the MergeDefault Authentication '$($MergeDefault)' is not in the Authentication list supplied" - } - - # ensure the default is in the auth list - if (![string]::IsNullOrEmpty($Default) -and ($Default -inotin @($Authentication))) { - throw ($PodeLocale.defaultAuthNotInListExceptionMessage -f $Default) # "the Default Authentication '$($Default)' is not in the Authentication list supplied" - } - - # set default - if ([string]::IsNullOrEmpty($Default)) { - $Default = $Authentication[0] - } - - # get auth for default - $tmpAuth = $PodeContext.Server.Authentications.Methods[$Default] - - # check sessionless from default - if (!$Sessionless) { - $Sessionless = $tmpAuth.Sessionless - } - - # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { - # Sessions are required to use session persistent authentication - throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) - } - - # check failure url from default - if ([string]::IsNullOrEmpty($FailureUrl)) { - $FailureUrl = $tmpAuth.Failure.Url - } - - # check failure message from default - if ([string]::IsNullOrEmpty($FailureMessage)) { - $FailureMessage = $tmpAuth.Failure.Message - } - - # check success url from default - if ([string]::IsNullOrEmpty($SuccessUrl)) { - $SuccessUrl = $tmpAuth.Success.Url - } - - # check success use origin from default - if (!$SuccessUseOrigin) { - $SuccessUseOrigin = $tmpAuth.Success.UseOrigin - } - - # deal with using vars in scriptblock - if (($Valid -ieq 'all') -and [string]::IsNullOrEmpty($MergeDefault)) { - if ($null -eq $ScriptBlock) { - # A Scriptblock for merging multiple authenticated users into 1 object is required When Valid is All - throw ($PodeLocale.scriptBlockRequiredForMergingUsersExceptionMessage) - } - - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - } - else { - if ($null -ne $ScriptBlock) { - Write-Warning -Message 'The Scriptblock for merged authentications, when Valid=One, will be ignored' - } - } - - # set parent auth - foreach ($authName in $Authentication) { - $PodeContext.Server.Authentications.Methods[$authName].Parent = $Name - } - - # add auth method to server - $PodeContext.Server.Authentications.Methods[$Name] = @{ - Name = $Name - Authentications = @($Authentication) - PassOne = ($Valid -ieq 'one') - ScriptBlock = @{ - Script = $ScriptBlock - UsingVariables = $usingVars - } - Default = $Default - MergeDefault = $MergeDefault - Sessionless = $Sessionless.IsPresent - Failure = @{ - Url = $FailureUrl - Message = $FailureMessage - } - Success = @{ - Url = $SuccessUrl - UseOrigin = $SuccessUseOrigin.IsPresent - } - Cache = @{} - Merged = $true - Parent = $null - } -} - -<# -.SYNOPSIS -Gets an Authentication method. - -.DESCRIPTION -Gets an Authentication method. - -.PARAMETER Name -The Name of an Authentication method. - -.EXAMPLE -Get-PodeAuth -Name 'Main' -#> -function Get-PodeAuth { - [CmdletBinding()] - [OutputType([hashtable])] - param( - [Parameter(Mandatory = $true)] - [string] - $Name - ) - - # ensure the name exists - if (!(Test-PodeAuthExists -Name $Name)) { - throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Name) # "Authentication method not defined: $($Name)" - } - - # get auth method - return $PodeContext.Server.Authentications.Methods[$Name] -} - -<# -.SYNOPSIS -Test if an Authentication method exists. - -.DESCRIPTION -Test if an Authentication method exists. - -.PARAMETER Name -The Name of the Authentication method. - -.EXAMPLE -if (Test-PodeAuthExists -Name BasicAuth) { ... } -#> -function Test-PodeAuthExists { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] - [CmdletBinding()] - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)] - [string] - $Name - ) - - return $PodeContext.Server.Authentications.Methods.ContainsKey($Name) -} - -<# -.SYNOPSIS -Test and invoke an Authentication method to verify a user. - -.DESCRIPTION -Test and invoke an Authentication method to verify a user. This will verify a user's credentials on the request. -When testing OAuth2 methods, the first attempt will trigger a redirect to the provider and $false will be returned. - -.PARAMETER Name -The Name of the Authentication method. - -.PARAMETER IgnoreSession -If supplied, authentication will be re-verified on each call even if a valid session exists on the request. - -.EXAMPLE -if (Test-PodeAuth -Name 'BasicAuth') { ... } - -.EXAMPLE -if (Test-PodeAuth -Name 'FormAuth' -IgnoreSession) { ... } -#> -function Test-PodeAuth { - [CmdletBinding()] - [OutputType([boolean])] - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [switch] - $IgnoreSession - ) - - if (! (Test-PodeAuthExists -Name $Name)) { - throw ($PodeLocale.authMethodDoesNotExistExceptionMessage -f $Name) - } - - # if the session already has a user/isAuth'd, then skip auth - or allow anon - if (!$IgnoreSession -and (Test-PodeSessionsInUse) -and (Test-PodeAuthUser)) { - return $true - } - - try { - $result = Invoke-PodeAuthValidation -Name $Name - } - catch { - $_ | Write-PodeErrorLog - return $false - } - - # did the auth force a redirect? - if ($result.Redirected) { - return $false - } - - # if auth failed, set appropriate response headers/redirects - if (!$result.Success) { - return $false - } - - # successful auth - return $true -} - -<# -.SYNOPSIS - Invokes an authentication method in Pode. - -.DESCRIPTION - This function attempts to invoke an authentication method by its name, - ensuring that it exists and has not been merged. If the authentication - method does not exist or is merged, it throws an exception. - -.PARAMETER Name - The name of the authentication method to invoke. This parameter is mandatory. - -.OUTPUTS - A hashtable containing the authentication result, including success status,user information, and headers. - -#> -function Invoke-PodeAuth { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string] - $Name - ) - - # Check if the authentication method exists - if (! (Test-PodeAuthExists -Name $Name)) { - # Authentication method doesn't exist: - throw ($PodeLocale.authMethodDoesNotExistExceptionMessage -f $Name) - } - - # Ensure the authentication method is not merged - if ($PodeContext.Server.Authentications.Methods[$Name].Merged) { - # Authentication method {0} is merged - throw ($PodeLocale.authenticationMethodMergedExceptionMessage -f $Name) - } - try { - # Perform authentication validation - $WebEvent.Auth = Test-PodeAuthValidation -Name $Name -NoMiddlewareAuthentication - Add-PodeHeader -Name 'WWW-Authenticate' -Value $WebEvent.Auth.Headers['WWW-Authenticate'] - } - catch { - $_ | Write-PodeErrorLog - } - - return $WebEvent.Auth -} - - - -<# -.SYNOPSIS -Adds the inbuilt Windows AD Authentication method for verifying users. - -.DESCRIPTION -Adds the inbuilt Windows AD Authentication method for verifying users. - -.PARAMETER Name -A unique Name for the Authentication method. - -.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) - -.PARAMETER Domain -(Unix Only) A custom NetBIOS domain name that is prepended onto usernames that are missing it (\). - -.PARAMETER SearchBase -(Unix Only) An optional searchbase to refine the LDAP query. This should be the full distinguished name. - -.PARAMETER Groups -An array of Group names to only allow access. - -.PARAMETER Users -An array of Usernames to only allow access. - -.PARAMETER FailureUrl -The URL to redirect to when authentication fails. - -.PARAMETER FailureMessage -An override Message to throw when authentication fails. - -.PARAMETER SuccessUrl -The URL to redirect to when authentication succeeds when logging in. - -.PARAMETER ScriptBlock -Optional ScriptBlock that is passed the found user object for further validation. - -.PARAMETER Sessionless -If supplied, authenticated users will not be stored in sessions, and sessions will not be used. - -.PARAMETER NoGroups -If supplied, groups will not be retrieved for the user in AD. - -.PARAMETER DirectGroups -If supplied, only a user's direct groups will be retrieved rather than all groups recursively. - -.PARAMETER OpenLDAP -If supplied, and on Windows, OpenLDAP will be used instead (this is the default for Linux/MacOS). - -.PARAMETER ADModule -If supplied, and on Windows, the ActiveDirectory module will be used instead. - -.PARAMETER SuccessUseOrigin -If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. - -.PARAMETER KeepCredential -If suplied pode will save the AD credential as a PSCredential object in $WebEvent.Auth.User.Credential - -.EXAMPLE -New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'WinAuth' - -.EXAMPLE -New-PodeAuthScheme -Basic | Add-PodeAuthWindowsAd -Name 'WinAuth' -Groups @('Developers') - -.EXAMPLE -New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'WinAuth' -NoGroups - -.EXAMPLE -New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'UnixAuth' -Server 'testdomain.company.com' -Domain 'testdomain' -#> -function Add-PodeAuthWindowsAd { - [CmdletBinding(DefaultParameterSetName = 'Groups')] - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [hashtable] - $Scheme, - - [Parameter()] - [Alias('Server')] - [string] - $Fqdn, - - [Parameter()] - [string] - $Domain, - - [Parameter()] - [string] - $SearchBase, - - [Parameter(ParameterSetName = 'Groups')] - [string[]] - $Groups, - - [Parameter()] - [string[]] - $Users, - - [Parameter()] - [string] - $FailureUrl, - - [Parameter()] - [string] - $FailureMessage, - - [Parameter()] - [string] - $SuccessUrl, - - [Parameter()] - [scriptblock] - $ScriptBlock, - - [switch] - $Sessionless, - - [Parameter(ParameterSetName = 'NoGroups')] - [switch] - $NoGroups, - - [Parameter(ParameterSetName = 'Groups')] - [switch] - $DirectGroups, - - [switch] - $OpenLDAP, - - [switch] - $ADModule, - - [switch] - $SuccessUseOrigin, - - [switch] - $KeepCredential - ) - begin { - $pipelineItemCount = 0 - } - - process { - - $pipelineItemCount++ - } - - end { - if ($pipelineItemCount -gt 1) { - throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) - } - # ensure the name doesn't already exist - if (Test-PodeAuthExists -Name $Name) { - # Authentication method already defined: {0} - throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) - } - - # ensure the Scheme contains a scriptblock - if (Test-PodeIsEmpty $Scheme.ScriptBlock) { - # The supplied Scheme for the '$($Name)' Windows AD authentication validator requires a valid ScriptBlock - throw ($PodeLocale.schemeRequiresValidScriptBlockExceptionMessage -f $Name) - } - - # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { - # Sessions are required to use session persistent authentication - throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) - } - - # if AD module set, ensure we're on windows and the module is available, then import/export it - if ($ADModule) { - Import-PodeAuthADModule - } - - # set server name if not passed - if ([string]::IsNullOrWhiteSpace($Fqdn)) { - $Fqdn = Get-PodeAuthDomainName - - if ([string]::IsNullOrWhiteSpace($Fqdn)) { - # No domain server name has been supplied for Windows AD authentication - throw ($PodeLocale.noDomainServerNameForWindowsAdAuthExceptionMessage) - } - } - - # set the domain if not passed - if ([string]::IsNullOrWhiteSpace($Domain)) { - $Domain = ($Fqdn -split '\.')[0] - } - - # if we have a scriptblock, deal with using vars - if ($null -ne $ScriptBlock) { - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - } - - # add Windows AD auth method to server - $PodeContext.Server.Authentications.Methods[$Name] = @{ - Name = $Name - Scheme = $Scheme - ScriptBlock = (Get-PodeAuthWindowsADMethod) - Arguments = @{ - Server = $Fqdn - Domain = $Domain - SearchBase = $SearchBase - Users = $Users - Groups = $Groups - NoGroups = $NoGroups - DirectGroups = $DirectGroups - KeepCredential = $KeepCredential - Provider = (Get-PodeAuthADProvider -OpenLDAP:$OpenLDAP -ADModule:$ADModule) - ScriptBlock = @{ - Script = $ScriptBlock - UsingVariables = $usingVars - } - } - Sessionless = $Sessionless - Failure = @{ - Url = $FailureUrl - Message = $FailureMessage - } - Success = @{ - Url = $SuccessUrl - UseOrigin = $SuccessUseOrigin - } - Cache = @{} - Merged = $false - Parent = $null - } - } -} - -<# -.SYNOPSIS -Adds the inbuilt Session Authentication method for verifying an authenticated session is present on Requests. - -.DESCRIPTION -Adds the inbuilt Session Authentication method for verifying an authenticated session is present on Requests. - -.PARAMETER Name -A unique Name for the Authentication method. - -.PARAMETER FailureUrl -The URL to redirect to when authentication fails. - -.PARAMETER FailureMessage -An override Message to throw when authentication fails. - -.PARAMETER SuccessUrl -The URL to redirect to when authentication succeeds when logging in. - -.PARAMETER ScriptBlock -Optional ScriptBlock that is passed the found user object for further validation. - -.PARAMETER Middleware -An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. - -.PARAMETER SuccessUseOrigin -If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. - -.EXAMPLE -Add-PodeAuthSession -Name 'SessionAuth' -FailureUrl '/login' -#> -function Add-PodeAuthSession { - [CmdletBinding(DefaultParameterSetName = 'Groups')] - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter()] - [string] - $FailureUrl, - - [Parameter()] - [string] - $FailureMessage, - - [Parameter()] - [string] - $SuccessUrl, - - [Parameter()] - [scriptblock] - $ScriptBlock, - - [Parameter()] - [object[]] - $Middleware, - - [switch] - $SuccessUseOrigin - ) - - # if sessions haven't been setup, error - if (!(Test-PodeSessionsEnabled)) { - # Sessions have not been configured - throw ($PodeLocale.sessionsNotConfiguredExceptionMessage) - } - - # ensure the name doesn't already exist - if (Test-PodeAuthExists -Name $Name) { - # Authentication method already defined: { 0 } - throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) - } - - # if we have a scriptblock, deal with using vars - if ($null -ne $ScriptBlock) { - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - } - - # create the auth scheme for getting the session - $scheme = New-PodeAuthScheme -Custom -Middleware $Middleware -ScriptBlock { - param($options) - - # 401 if sessions not used - if (!(Test-PodeSessionsInUse)) { - Revoke-PodeSession - return @{ - Message = 'Sessions are not being used' - Code = 401 - } - } - - # 401 if no authenticated user - if (!(Test-PodeAuthUser)) { - Revoke-PodeSession - return @{ - Message = 'Session not authenticated' - Code = 401 - } - } - - # return user - return @($WebEvent.Session.Data.Auth) - } - - # add a custom auth method to return user back - $method = { - param($user, $options) - $result = @{ User = $user } - - # call additional scriptblock if supplied - if ($null -ne $options.ScriptBlock.Script) { - $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $options.ScriptBlock.Script -UsingVariables $options.ScriptBlock.UsingVariables - } - - # return user back - return $result - } - - $scheme | Add-PodeAuth ` - -Name $Name ` - -ScriptBlock $method ` - -FailureUrl $FailureUrl ` - -FailureMessage $FailureMessage ` - -SuccessUrl $SuccessUrl ` - -SuccessUseOrigin:$SuccessUseOrigin ` - -ArgumentList @{ - ScriptBlock = @{ - Script = $ScriptBlock - UsingVariables = $usingVars - } - } -} - -<# -.SYNOPSIS -Remove a specific Authentication method. - -.DESCRIPTION -Remove a specific Authentication method. - -.PARAMETER Name -The Name of the Authentication method. - -.EXAMPLE -Remove-PodeAuth -Name 'Login' -#> -function Remove-PodeAuth { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [string] - $Name - ) - process { - $null = $PodeContext.Server.Authentications.Methods.Remove($Name) - } -} - -<# -.SYNOPSIS -Clear all defined Authentication methods. - -.DESCRIPTION -Clear all defined Authentication methods. - -.EXAMPLE -Clear-PodeAuth -#> -function Clear-PodeAuth { - [CmdletBinding()] - param() - - $PodeContext.Server.Authentications.Methods.Clear() -} - -<# -.SYNOPSIS -Adds an authentication method as global middleware. - -.DESCRIPTION -Adds an authentication method as global middleware. - -.PARAMETER Name -The Name of the Middleware. - -.PARAMETER Authentication -The Name of the Authentication method to use. - -.PARAMETER Route -A Route path for which Routes this Middleware should only be invoked against. - -.PARAMETER OADefinitionTag -An array of string representing the unique tag for the API specification. -This tag helps in distinguishing between different versions or types of API specifications within the application. -Use this tag to reference the specific API documentation, schema, or version that your function interacts with. - -.EXAMPLE -Add-PodeAuthMiddleware -Name 'GlobalAuth' -Authentication AuthName - -.EXAMPLE -Add-PodeAuthMiddleware -Name 'GlobalAuth' -Authentication AuthName -Route '/api/*' -#> -function Add-PodeAuthMiddleware { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter(Mandatory = $true)] - [Alias('Auth')] - [string] - $Authentication, - - [Parameter()] - [string] - $Route, - - [string[]] - $OADefinitionTag - ) - - $DefinitionTag = Test-PodeOADefinitionTag -Tag $OADefinitionTag - - if (!(Test-PodeAuthExists -Name $Authentication)) { - throw ($PodeLocale.authenticationMethodDoesNotExistExceptionMessage -f $Authentication) # "Authentication method does not exist: $($Authentication)" - } - - Get-PodeAuthMiddlewareScript | - New-PodeMiddleware -ArgumentList @{ Name = $Authentication } | - Add-PodeMiddleware -Name $Name -Route $Route - - Set-PodeOAGlobalAuth -DefinitionTag $DefinitionTag -Name $Authentication -Route $Route -} - -<# -.SYNOPSIS -Adds the inbuilt IIS Authentication method for verifying users passed to Pode from IIS. - -.DESCRIPTION -Adds the inbuilt IIS Authentication method for verifying users passed to Pode from IIS. - -.PARAMETER Name -A unique Name for the Authentication method. - -.PARAMETER Groups -An array of Group names to only allow access. - -.PARAMETER Users -An array of Usernames to only allow access. - -.PARAMETER FailureUrl -The URL to redirect to when authentication fails. - -.PARAMETER FailureMessage -An override Message to throw when authentication fails. - -.PARAMETER SuccessUrl -The URL to redirect to when authentication succeeds when logging in. - -.PARAMETER ScriptBlock -Optional ScriptBlock that is passed the found user object for further validation. - -.PARAMETER Middleware -An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. - -.PARAMETER Sessionless -If supplied, authenticated users will not be stored in sessions, and sessions will not be used. - -.PARAMETER NoGroups -If supplied, groups will not be retrieved for the user in AD. - -.PARAMETER DirectGroups -If supplied, only a user's direct groups will be retrieved rather than all groups recursively. - -.PARAMETER ADModule -If supplied, and on Windows, the ActiveDirectory module will be used instead. - -.PARAMETER NoLocalCheck -If supplied, Pode will not at attempt to retrieve local User/Group information for the authenticated user. - -.PARAMETER SuccessUseOrigin -If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. - -.EXAMPLE -Add-PodeAuthIIS -Name 'IISAuth' - -.EXAMPLE -Add-PodeAuthIIS -Name 'IISAuth' -Groups @('Developers') - -.EXAMPLE -Add-PodeAuthIIS -Name 'IISAuth' -NoGroups -#> -function Add-PodeAuthIIS { - [CmdletBinding(DefaultParameterSetName = 'Groups')] - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter(ParameterSetName = 'Groups')] - [string[]] - $Groups, - - [Parameter()] - [string[]] - $Users, - - [Parameter()] - [string] - $FailureUrl, - - [Parameter()] - [string] - $FailureMessage, - - [Parameter()] - [string] - $SuccessUrl, - - [Parameter()] - [scriptblock] - $ScriptBlock, - - [Parameter()] - [object[]] - $Middleware, - - [switch] - $Sessionless, - - [Parameter(ParameterSetName = 'NoGroups')] - [switch] - $NoGroups, - - [Parameter(ParameterSetName = 'Groups')] - [switch] - $DirectGroups, - - [switch] - $ADModule, - - [switch] - $NoLocalCheck, - - [switch] - $SuccessUseOrigin - ) - - # ensure we're on Windows! - if (!(Test-PodeIsWindows)) { - # IIS Authentication support is for Windows only - throw ($PodeLocale.iisAuthSupportIsForWindowsOnlyExceptionMessage) - } - - # ensure the name doesn't already exist - if (Test-PodeAuthExists -Name $Name) { - # Authentication method already defined: {0} - throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) - } - - # if AD module set, ensure we're on windows and the module is available, then import/export it - if ($ADModule) { - Import-PodeAuthADModule - } - - # if we have a scriptblock, deal with using vars - if ($null -ne $ScriptBlock) { - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - } - - # create the auth scheme for getting the token header - $scheme = New-PodeAuthScheme -Custom -Middleware $Middleware -ScriptBlock { - param($options) - - $header = 'MS-ASPNETCORE-WINAUTHTOKEN' - - # fail if no header - if (!(Test-PodeHeader -Name $header)) { - return @{ - Message = "No $($header) header found" - Code = 401 - } - } - - # return the header for validation - $token = Get-PodeHeader -Name $header - return @($token) - } - - # add a custom auth method to validate the user - $method = Get-PodeAuthWindowsADIISMethod - - $scheme | Add-PodeAuth ` - -Name $Name ` - -ScriptBlock $method ` - -FailureUrl $FailureUrl ` - -FailureMessage $FailureMessage ` - -SuccessUrl $SuccessUrl ` - -Sessionless:$Sessionless ` - -SuccessUseOrigin:$SuccessUseOrigin ` - -ArgumentList @{ - Users = $Users - Groups = $Groups - NoGroups = $NoGroups - DirectGroups = $DirectGroups - Provider = (Get-PodeAuthADProvider -ADModule:$ADModule) - NoLocalCheck = $NoLocalCheck - ScriptBlock = @{ - Script = $ScriptBlock - UsingVariables = $usingVars - } - } -} - -<# -.SYNOPSIS -Adds the inbuilt User File Authentication method for verifying users. - -.DESCRIPTION -Adds the inbuilt User File Authentication method for verifying users. - -.PARAMETER Name -A unique Name for the Authentication method. - -.PARAMETER Scheme -The Scheme to use for retrieving credentials (From New-PodeAuthScheme). - -.PARAMETER FilePath -A path to a users JSON file (Default: ./users.json) - -.PARAMETER Groups -An array of Group names to only allow access. - -.PARAMETER Users -An array of Usernames to only allow access. - -.PARAMETER HmacSecret -An optional secret if the passwords are HMAC SHA256 hashed. - -.PARAMETER FailureUrl -The URL to redirect to when authentication fails. - -.PARAMETER FailureMessage -An override Message to throw when authentication fails. - -.PARAMETER SuccessUrl -The URL to redirect to when authentication succeeds when logging in. - -.PARAMETER ScriptBlock -Optional ScriptBlock that is passed the found user object for further validation. - -.PARAMETER Sessionless -If supplied, authenticated users will not be stored in sessions, and sessions will not be used. - -.PARAMETER SuccessUseOrigin -If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. - -.EXAMPLE -New-PodeAuthScheme -Form | Add-PodeAuthUserFile -Name 'Login' - -.EXAMPLE -New-PodeAuthScheme -Form | Add-PodeAuthUserFile -Name 'Login' -FilePath './custom/path/users.json' -#> -function Add-PodeAuthUserFile { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [hashtable] - $Scheme, - - [Parameter()] - [string] - $FilePath, - - [Parameter()] - [string[]] - $Groups, - - [Parameter()] - [string[]] - $Users, - - [Parameter(ParameterSetName = 'Hmac')] - [string] - $HmacSecret, - - [Parameter()] - [string] - $FailureUrl, - - [Parameter()] - [string] - $FailureMessage, - - [Parameter()] - [string] - $SuccessUrl, - - [Parameter()] - [scriptblock] - $ScriptBlock, - - [switch] - $Sessionless, - - [switch] - $SuccessUseOrigin - ) - begin { - $pipelineItemCount = 0 - } - - process { - - $pipelineItemCount++ - } - - end { - if ($pipelineItemCount -gt 1) { - throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) - } - # ensure the name doesn't already exist - if (Test-PodeAuthExists -Name $Name) { - # Authentication method already defined: {0} - throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) - } - - # ensure the Scheme contains a scriptblock - if (Test-PodeIsEmpty $Scheme.ScriptBlock) { - # The supplied scheme for the '{0}' authentication validator requires a valid ScriptBlock. - throw ($PodeLocale.schemeRequiresValidScriptBlockExceptionMessage -f $Name) - } - - # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { - # Sessions are required to use session persistent authentication - throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) - } - - # set the file path if not passed - if ([string]::IsNullOrWhiteSpace($FilePath)) { - $FilePath = Join-PodeServerRoot -Folder '.' -FilePath 'users.json' - } - else { - $FilePath = Get-PodeRelativePath -Path $FilePath -JoinRoot -Resolve - } - - # ensure the user file exists - if (!(Test-PodePath -Path $FilePath -NoStatus -FailOnDirectory)) { - # The user file does not exist: {0} - throw ($PodeLocale.userFileDoesNotExistExceptionMessage -f $FilePath) - } - - # if we have a scriptblock, deal with using vars - if ($null -ne $ScriptBlock) { - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - } - - # add Windows AD auth method to server - $PodeContext.Server.Authentications.Methods[$Name] = @{ - Name = $Name - Scheme = $Scheme - ScriptBlock = (Get-PodeAuthUserFileMethod) - Arguments = @{ - FilePath = $FilePath - Users = $Users - Groups = $Groups - HmacSecret = $HmacSecret - ScriptBlock = @{ - Script = $ScriptBlock - UsingVariables = $usingVars - } - } - Sessionless = $Sessionless - Failure = @{ - Url = $FailureUrl - Message = $FailureMessage - } - Success = @{ - Url = $SuccessUrl - UseOrigin = $SuccessUseOrigin - } - Cache = @{} - Merged = $false - Parent = $null - } - } -} - -<# -.SYNOPSIS -Adds the inbuilt Windows Local User Authentication method for verifying users. - -.DESCRIPTION -Adds the inbuilt Windows Local User Authentication method for verifying users. - -.PARAMETER Name -A unique Name for the Authentication method. - -.PARAMETER Scheme -The Scheme to use for retrieving credentials (From New-PodeAuthScheme). - -.PARAMETER Groups -An array of Group names to only allow access. - -.PARAMETER Users -An array of Usernames to only allow access. - -.PARAMETER FailureUrl -The URL to redirect to when authentication fails. - -.PARAMETER FailureMessage -An override Message to throw when authentication fails. - -.PARAMETER SuccessUrl -The URL to redirect to when authentication succeeds when logging in. - -.PARAMETER ScriptBlock -Optional ScriptBlock that is passed the found user object for further validation. - -.PARAMETER Sessionless -If supplied, authenticated users will not be stored in sessions, and sessions will not be used. - -.PARAMETER NoGroups -If supplied, groups will not be retrieved for the user. - -.PARAMETER SuccessUseOrigin -If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. - -.EXAMPLE -New-PodeAuthScheme -Form | Add-PodeAuthWindowsLocal -Name 'WinAuth' - -.EXAMPLE -New-PodeAuthScheme -Basic | Add-PodeAuthWindowsLocal -Name 'WinAuth' -Groups @('Developers') - -.EXAMPLE -New-PodeAuthScheme -Form | Add-PodeAuthWindowsLocal -Name 'WinAuth' -NoGroups -#> -function Add-PodeAuthWindowsLocal { - [CmdletBinding(DefaultParameterSetName = 'Groups')] - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] - [hashtable] - $Scheme, - - [Parameter(ParameterSetName = 'Groups')] - [string[]] - $Groups, - - [Parameter()] - [string[]] - $Users, - - [Parameter()] - [string] - $FailureUrl, - - [Parameter()] - [string] - $FailureMessage, - - [Parameter()] - [string] - $SuccessUrl, - - [Parameter()] - [scriptblock] - $ScriptBlock, - - [switch] - $Sessionless, - - [Parameter(ParameterSetName = 'NoGroups')] - [switch] - $NoGroups, - - [switch] - $SuccessUseOrigin - ) - begin { - $pipelineItemCount = 0 - } - - process { - - $pipelineItemCount++ - } - - end { - if ($pipelineItemCount -gt 1) { - throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) - } - # ensure we're on Windows! - if (!(Test-PodeIsWindows)) { - # Windows Local Authentication support is for Windows only - throw ($PodeLocale.windowsLocalAuthSupportIsForWindowsOnlyExceptionMessage) - } - - # ensure the name doesn't already exist - if (Test-PodeAuthExists -Name $Name) { - # Authentication method already defined: {0} - throw ($PodeLocale.authMethodAlreadyDefinedExceptionMessage -f $Name) - } - - # ensure the Scheme contains a scriptblock - if (Test-PodeIsEmpty $Scheme.ScriptBlock) { - # The supplied scheme for the '{0}' authentication validator requires a valid ScriptBlock. - throw ($PodeLocale.schemeRequiresValidScriptBlockExceptionMessage -f $Name) - } - - # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { - # Sessions are required to use session persistent authentication - throw ($PodeLocale.sessionsRequiredForSessionPersistentAuthExceptionMessage) - } - - # if we have a scriptblock, deal with using vars - if ($null -ne $ScriptBlock) { - $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState - } - - # add Windows Local auth method to server - $PodeContext.Server.Authentications.Methods[$Name] = @{ - Name = $Name - Scheme = $Scheme - ScriptBlock = (Get-PodeAuthWindowsLocalMethod) - Arguments = @{ - Users = $Users - Groups = $Groups - NoGroups = $NoGroups - ScriptBlock = @{ - Script = $ScriptBlock - UsingVariables = $usingVars - } - } - Sessionless = $Sessionless - Failure = @{ - Url = $FailureUrl - Message = $FailureMessage - } - Success = @{ - Url = $SuccessUrl - UseOrigin = $SuccessUseOrigin - } - Cache = @{} - Merged = $false - Parent = $null - } - } -} - -<# -.SYNOPSIS -Convert a Header/Payload into a JWT. - -.DESCRIPTION -Convert a Header/Payload hashtable into a JWT, with the option to sign it. - -.PARAMETER Header -A Hashtable containing the Header information for the JWT. - -.PARAMETER Payload -A Hashtable containing the Payload information for the JWT. - -.PARAMETER Secret -An Optional Secret for signing the JWT, should be a string or byte[]. This is mandatory if the Header algorithm isn't "none". - -.EXAMPLE -ConvertTo-PodeJwt -Header @{ alg = 'none' } -Payload @{ sub = '123'; name = 'John' } - -.EXAMPLE -ConvertTo-PodeJwt -Header @{ alg = 'hs256' } -Payload @{ sub = '123'; name = 'John' } -Secret 'abc' -#> -function ConvertTo-PodeJwt { - [CmdletBinding()] - [OutputType([string])] - param( - [Parameter(Mandatory = $true)] - [hashtable] - $Header, - - [Parameter(Mandatory = $true)] - [hashtable] - $Payload, - - [Parameter()] - $Secret = $null - ) - - # validate header - if ([string]::IsNullOrWhiteSpace($Header.alg)) { - # No algorithm supplied in JWT Header - throw ($PodeLocale.noAlgorithmInJwtHeaderExceptionMessage) - } - - # convert the header - $header64 = ConvertTo-PodeBase64UrlValue -Value ($Header | ConvertTo-Json -Compress) - - # convert the payload - $payload64 = ConvertTo-PodeBase64UrlValue -Value ($Payload | ConvertTo-Json -Compress) - - # combine - $jwt = "$($header64).$($payload64)" - - # convert secret to bytes - if (($null -ne $Secret) -and ($Secret -isnot [byte[]])) { - $Secret = [System.Text.Encoding]::UTF8.GetBytes([string]$Secret) - } - - # make the signature - $sig = New-PodeJwtSignature -Algorithm $Header.alg -Token $jwt -SecretBytes $Secret - - # add the signature and return - $jwt += ".$($sig)" - return $jwt -} - -<# -.SYNOPSIS -Convert and return the payload of a JWT token. - -.DESCRIPTION -Convert and return the payload of a JWT token, verifying the signature by default with support to ignore the signature. - -.PARAMETER Token -The JWT token. - -.PARAMETER Secret -The Secret, as a string or byte[], to verify the token's signature. - -.PARAMETER IgnoreSignature -Skip signature verification, and return the decoded payload. - -.EXAMPLE -ConvertFrom-PodeJwt -Token "eyJ0eXAiOiJKV1QiLCJhbGciOiJoczI1NiJ9.eyJleHAiOjE2MjI1NTMyMTQsIm5hbWUiOiJKb2huIERvZSIsInN1YiI6IjEyMyJ9.LP-O8OKwix91a-SZwVK35gEClLZQmsORbW0un2Z4RkY" -#> -function ConvertFrom-PodeJwt { - [CmdletBinding(DefaultParameterSetName = 'Secret')] - [OutputType([pscustomobject])] - param( - [Parameter(Mandatory = $true)] - [string] - $Token, - - [Parameter(ParameterSetName = 'Signed')] - $Secret = $null, - - [Parameter(ParameterSetName = 'Ignore')] - [switch] - $IgnoreSignature - ) - - # get the parts - $parts = ($Token -isplit '\.') - - # check number of parts (should be 3) - if ($parts.Length -ne 3) { - # Invalid JWT supplied - throw ($PodeLocale.invalidJwtSuppliedExceptionMessage) - } - - # convert to header - $header = ConvertFrom-PodeJwtBase64Value -Value $parts[0] - if ([string]::IsNullOrWhiteSpace($header.alg)) { - # Invalid JWT header algorithm supplied - throw ($PodeLocale.invalidJwtHeaderAlgorithmSuppliedExceptionMessage) - } - - # convert to payload - $payload = ConvertFrom-PodeJwtBase64Value -Value $parts[1] - - # get signature - if ($IgnoreSignature) { - return $payload - } - - $signature = $parts[2] - - # check "none" signature, and return payload if no signature - $isNoneAlg = ($header.alg -ieq 'none') - - if ([string]::IsNullOrWhiteSpace($signature) -and !$isNoneAlg) { - # No JWT signature supplied for {0} - throw ($PodeLocale.noJwtSignatureForAlgorithmExceptionMessage -f $header.alg) - } - - if (![string]::IsNullOrWhiteSpace($signature) -and $isNoneAlg) { - # Expected no JWT signature to be supplied - throw ($PodeLocale.expectedNoJwtSignatureSuppliedExceptionMessage) - } - - if ($isNoneAlg -and ($null -ne $Secret) -and ($Secret.Length -gt 0)) { - # Expected no JWT signature to be supplied - throw ($PodeLocale.expectedNoJwtSignatureSuppliedExceptionMessage) - } - - if ($isNoneAlg) { - return $payload - } - - # otherwise, we have an alg for the signature, so we need to validate it - if (($null -ne $Secret) -and ($Secret -isnot [byte[]])) { - $Secret = [System.Text.Encoding]::UTF8.GetBytes([string]$Secret) - } - - $sig = "$($parts[0]).$($parts[1])" - $sig = New-PodeJwtSignature -Algorithm $header.alg -Token $sig -SecretBytes $Secret - - if ($sig -ne $parts[2]) { - # Invalid JWT signature supplied - throw ($PodeLocale.invalidJwtSignatureSuppliedExceptionMessage) - } - - # it's valid return the payload! - return $payload -} - -<# -.SYNOPSIS -Validates JSON Web Tokens (JWT) claims. - -.DESCRIPTION -Validates JSON Web Tokens (JWT) claims. Checks time related claims: 'exp' and 'nbf'. - -.PARAMETER Payload -Object containing JWT claims. Some of them are: - - exp (expiration time) - - nbf (not before) - -.EXAMPLE -Test-PodeJwt @{exp = 2696258821 } - -.EXAMPLE -Test-PodeJwt -Payload @{nbf = 1696258821 } -#> -function Test-PodeJwt { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [pscustomobject] - $Payload - ) - - $now = [datetime]::UtcNow - $unixStart = [datetime]::new(1970, 1, 1, 0, 0, [DateTimeKind]::Utc) - - # validate expiry - if (![string]::IsNullOrWhiteSpace($Payload.exp)) { - if ($now -gt $unixStart.AddSeconds($Payload.exp)) { - # The JWT has expired - throw ($PodeLocale.jwtExpiredExceptionMessage) - } - } - - # validate not-before - if (![string]::IsNullOrWhiteSpace($Payload.nbf)) { - if ($now -lt $unixStart.AddSeconds($Payload.nbf)) { - # The JWT is not yet valid for use - throw ($PodeLocale.jwtNotYetValidExceptionMessage) - } - } -} - -<# -.SYNOPSIS -Automatically loads auth ps1 files - -.DESCRIPTION -Automatically loads auth ps1 files from either a /auth folder, or a custom folder. Saves space dot-sourcing them all one-by-one. - -.PARAMETER Path -Optional Path to a folder containing ps1 files, can be relative or literal. - -.EXAMPLE -Use-PodeAuth - -.EXAMPLE -Use-PodeAuth -Path './my-auth' -#> -function Use-PodeAuth { - [CmdletBinding()] - param( - [Parameter()] - [string] - $Path - ) - - Use-PodeFolder -Path $Path -DefaultPath 'auth' -} - -<# -.SYNOPSIS -Builds an OAuth2 scheme using an OpenID Connect Discovery URL. - -.DESCRIPTION -Builds an OAuth2 scheme using an OpenID Connect Discovery URL. - -.PARAMETER Url -The OpenID Connect Discovery URL, this must end with '/.well-known/openid-configuration' (if missing, it will be automatically appended). - -.PARAMETER Scope -A list of optional Scopes to use during the OAuth2 request. (Default: the supported list returned) - -.PARAMETER ClientId -The Client ID from registering a new app. - -.PARAMETER ClientSecret -The Client Secret from registering a new app (this is optional when using PKCE). - -.PARAMETER RedirectUrl -An optional OAuth2 Redirect URL (Default: /oauth2/callback) - -.PARAMETER InnerScheme -An optional authentication Scheme (from New-PodeAuthScheme) that will be called prior to this Scheme. - -.PARAMETER Middleware -An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. - -.PARAMETER UsePKCE -If supplied, OAuth2 authentication will use PKCE code verifiers. - -.EXAMPLE -ConvertFrom-PodeOIDCDiscovery -Url 'https://accounts.google.com/.well-known/openid-configuration' -ClientId some_id -UsePKCE - -.EXAMPLE -ConvertFrom-PodeOIDCDiscovery -Url 'https://accounts.google.com' -ClientId some_id -UsePKCE -#> -function ConvertFrom-PodeOIDCDiscovery { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string] - $Url, - - [Parameter()] - [string[]] - $Scope, - - [Parameter(Mandatory = $true)] - [string] - $ClientId, - - [Parameter()] - [string] - $ClientSecret, - - [Parameter()] - [string] - $RedirectUrl, - - [Parameter(ValueFromPipeline = $true)] - [hashtable] - $InnerScheme, - - [Parameter()] - [object[]] - $Middleware, - - [switch] - $UsePKCE - ) - begin { - $pipelineItemCount = 0 - } - - process { - - $pipelineItemCount++ - } - - end { - if ($pipelineItemCount -gt 1) { - throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) - } - # get the discovery doc - if (!$Url.EndsWith('/.well-known/openid-configuration')) { - $Url += '/.well-known/openid-configuration' - } - - $config = Invoke-RestMethod -Method Get -Uri $Url - - # check it supports the code response_type - if ($config.response_types_supported -inotcontains 'code') { - # The OAuth2 provider does not support the 'code' response_type - throw ($PodeLocale.oauth2ProviderDoesNotSupportCodeResponseTypeExceptionMessage) - } - - # can we have an InnerScheme? - if (($null -ne $InnerScheme) -and ($config.grant_types_supported -inotcontains 'password')) { - # The OAuth2 provider does not support the 'password' grant_type required by using an InnerScheme - throw ($PodeLocale.oauth2ProviderDoesNotSupportPasswordGrantTypeExceptionMessage) - } - - # scopes - $scopes = $config.scopes_supported - - if (($null -ne $Scope) -and ($Scope.Length -gt 0)) { - $scopes = @(foreach ($s in $Scope) { - if ($s -iin $config.scopes_supported) { - $s - } - }) - } - - # pkce code challenge method - $codeMethod = 'S256' - if ($config.code_challenge_methods_supported -inotcontains $codeMethod) { - $codeMethod = 'plain' - } - - return New-PodeAuthScheme ` - -OAuth2 ` - -ClientId $ClientId ` - -ClientSecret $ClientSecret ` - -AuthoriseUrl $config.authorization_endpoint ` - -TokenUrl $config.token_endpoint ` - -UserUrl $config.userinfo_endpoint ` - -RedirectUrl $RedirectUrl ` - -Scope $scopes ` - -InnerScheme $InnerScheme ` - -Middleware $Middleware ` - -CodeChallengeMethod $codeMethod ` - -UsePKCE:$UsePKCE - } -} - -<# -.SYNOPSIS -Test whether the current WebEvent or Session has an authenticated user. - -.DESCRIPTION -Test whether the current WebEvent or Session has an authenticated user. Returns true if there is an authenticated user. - -.PARAMETER IgnoreSession -If supplied, only the Auth object in the WebEvent will be checked and the Session will be skipped. - -.EXAMPLE -if (Test-PodeAuthUser) { ... } -#> -function Test-PodeAuthUser { - [CmdletBinding()] - [OutputType([boolean])] - param( - [switch] - $IgnoreSession - ) - - # auth middleware - if (($null -ne $WebEvent.Auth) -and $WebEvent.Auth.IsAuthenticated) { - $auth = $WebEvent.Auth - } - - # session? - elseif (!$IgnoreSession -and ($null -ne $WebEvent.Session.Data.Auth) -and $WebEvent.Session.Data.Auth.IsAuthenticated) { - $auth = $WebEvent.Session.Data.Auth - } - - # null? - if (($null -eq $auth) -or ($null -eq $auth.User)) { - return $false - } - - return ($null -ne $auth.User) -} - -<# -.SYNOPSIS -Get the authenticated user from the WebEvent or Session. - -.DESCRIPTION -Get the authenticated user from the WebEvent or Session. This is similar to calling $Webevent.Auth.User. - -.PARAMETER IgnoreSession -If supplied, only the Auth object in the WebEvent will be used and the Session will be skipped. - -.EXAMPLE -$user = Get-PodeAuthUser -#> -function Get-PodeAuthUser { - [CmdletBinding()] - param( - [switch] - $IgnoreSession - ) - - # auth middleware - if (($null -ne $WebEvent.Auth) -and $WebEvent.Auth.IsAuthenticated) { - $auth = $WebEvent.Auth - } - - # session? - elseif (!$IgnoreSession -and ($null -ne $WebEvent.Session.Data.Auth) -and $WebEvent.Session.Data.Auth.IsAuthenticated) { - $auth = $WebEvent.Session.Data.Auth - } - - # null? - if (($null -eq $auth) -or ($null -eq $auth.User)) { - return $null - } - - return $auth.User -} \ No newline at end of file diff --git a/tests/integration/Authentication.Tests.ps1 b/tests/integration/Authentication.Tests.ps1 index 67f4147c3..9a4fad6cd 100644 --- a/tests/integration/Authentication.Tests.ps1 +++ b/tests/integration/Authentication.Tests.ps1 @@ -147,7 +147,7 @@ Describe 'Authentication Requests' { } It 'bearer - returns 400 for no token' { - { Invoke-RestMethod -Uri "$($Endpoint)/auth/bearer" -Method Get -Headers @{ Authorization = 'Bearer' } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*400*' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/bearer" -Method Get -Headers @{ Authorization = 'Bearer' } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*401*' }