Public/Authentication.ps1

<#
.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 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: <host>/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 = '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
                    }
                }
            }

            '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     = @{}
                }
            }

            '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')
                    }
                }
            }

            '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
                    }
                }
            }

            '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
                    }
                }
            }

            '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: <host>/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: <host>/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 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
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 (<Domain>\<Username>).
 
.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: <host>/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
}