Public/Authentication.ps1

<#
.SYNOPSIS
Create a new type of Authentication.
 
.DESCRIPTION
Create a new type of Authentication, 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 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.
 
.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 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 $_) {
                throw "A non-empty ScriptBlock is required for the Custom authentication scheme"
            }

            return $true
        })]
        [scriptblock]
        $ScriptBlock,

        [Parameter(ParameterSetName='Custom')]
        [hashtable]
        $ArgumentList,

        [Parameter(ParameterSetName='Custom')]
        [string]
        $Name,

        [Parameter()]
        [string]
        $Realm,

        [Parameter(ParameterSetName='Custom')]
        [ValidateSet('ApiKey', 'Http', 'OAuth2', 'OpenIdConnect')]
        [string]
        $Type = 'Http',

        [Parameter()]
        [object[]]
        $Middleware,

        [Parameter(ParameterSetName='Custom')]
        [scriptblock]
        $PostValidator,

        [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', Mandatory=$true)]
        [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')]
        [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
    )

    # 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 = @{
                    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 = @{
                    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 = @{
                    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'))) {
                throw "OAuth2 InnerScheme can only be one of either Basic or Form authentication, but got: $($InnerScheme.Name)"
            }

            if (($null -eq $InnerScheme) -and [string]::IsNullOrWhiteSpace($AuthoriseUrl)) {
                throw "OAuth2 requires an Authorise URL to be supplied"
            }

            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 = @{
                    Scopes = $Scope
                    Client = @{
                        ID = $ClientId
                        Secret = $ClientSecret
                    }
                    Urls = @{
                        Redirect = $RedirectUrl
                        Authorise = $AuthoriseUrl
                        Token = $TokenUrl
                        User = $UserUrl
                    }
                }
            }
        }

        '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 = @{
                    Location = $Location
                    LocationName = $LocationName
                    AsJWT = $AsJWT
                    Secret = $secretBytes
                }
            }
        }

        'custom' {
            $ScriptBlock, $usingScriptVars = Invoke-PodeUsingScriptConversion -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
            $ScriptBlock = Invoke-PodeStateScriptConversion -ScriptBlock $ScriptBlock
            $ScriptBlock = Invoke-PodeSessionScriptConversion -ScriptBlock $ScriptBlock

            if (!(Test-PodeIsEmpty $PostValidator)) {
                $PostValidator, $usingPostVars = Invoke-PodeUsingScriptConversion -ScriptBlock $PostValidator -PSSession $PSCmdlet.SessionState
                $PostValidator = Invoke-PodeStateScriptConversion -ScriptBlock $PostValidator
                $PostValidator = Invoke-PodeSessionScriptConversion -ScriptBlock $PostValidator
            }

            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.
 
.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.
 
.EXAMPLE
New-PodeAuthAzureADScheme -Tenant 123-456-678 -ClientId abcdef -ClientSecret 1234.abc
#>

function New-PodeAuthAzureADScheme
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]
        $Tenant = 'common',

        [Parameter(Mandatory=$true)]
        [string]
        $ClientId,

        [Parameter(Mandatory=$true)]
        [string]
        $ClientSecret,

        [Parameter()]
        [string]
        $RedirectUrl,

        [Parameter(ValueFromPipeline=$true)]
        [hashtable]
        $InnerScheme,

        [Parameter()]
        [object[]]
        $Middleware
    )

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

<#
.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 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 $_) {
                throw "A non-empty ScriptBlock is required for the authentication method"
            }

            return $true
        })]
        [scriptblock]
        $ScriptBlock,

        [Parameter()]
        [object[]]
        $ArgumentList,

        [Parameter()]
        [string]
        $FailureUrl,

        [Parameter()]
        [string]
        $FailureMessage,

        [Parameter()]
        [string]
        $SuccessUrl,

        [switch]
        $Sessionless,

        [switch]
        $SuccessUseOrigin
    )

    # ensure the name doesn't already exist
    if (Test-PodeAuth -Name $Name) {
        throw "Authentication method already defined: $($Name)"
    }

    # ensure the Scheme contains a scriptblock
    if (Test-PodeIsEmpty $Scheme.ScriptBlock) {
        throw "The supplied Scheme for the '$($Name)' authentication validator requires a valid ScriptBlock"
    }

    # if we're using sessions, ensure sessions have been setup
    if (!$Sessionless -and !(Test-PodeSessionsConfigured)) {
        throw 'Sessions are required to use session persistent authentication'
    }

    # check if the scriptblock has any using vars
    $ScriptBlock, $usingVars = Invoke-PodeUsingScriptConversion -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

    # check for state/session vars
    $ScriptBlock = Invoke-PodeStateScriptConversion -ScriptBlock $ScriptBlock
    $ScriptBlock = Invoke-PodeSessionScriptConversion -ScriptBlock $ScriptBlock

    # add auth method to server
    $PodeContext.Server.Authentications[$Name] = @{
        Scheme = $Scheme
        ScriptBlock = $ScriptBlock
        UsingVariables = $usingVars
        Arguments = $ArgumentList
        Sessionless = $Sessionless
        Failure = @{
            Url = $FailureUrl
            Message = $FailureMessage
        }
        Success = @{
            Url = $SuccessUrl
            UseOrigin = $SuccessUseOrigin
        }
    }

    # 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
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()]
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Name
    )

    # ensure the name exists
    if (!(Test-PodeAuth -Name $Name)) {
        throw "Authentication method not defined: $($Name)"
    }

    # get auth method
    return $PodeContext.Server.Authentications[$Name]
}

<#
.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 OpenLDAP
If supplied, and on Windows, OpenLDAP 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.
 
.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,

        [switch]
        $OpenLDAP,

        [switch]
        $SuccessUseOrigin
    )

    # ensure the name doesn't already exist
    if (Test-PodeAuth -Name $Name) {
        throw "Windows AD Authentication method already defined: $($Name)"
    }

    # ensure the Scheme contains a scriptblock
    if (Test-PodeIsEmpty $Scheme.ScriptBlock) {
        throw "The supplied Scheme for the '$($Name)' Windows AD authentication validator requires a valid ScriptBlock"
    }

    # if we're using sessions, ensure sessions have been setup
    if (!$Sessionless -and !(Test-PodeSessionsConfigured)) {
        throw 'Sessions are required to use session persistent authentication'
    }

    # set server name if not passed
    if ([string]::IsNullOrWhiteSpace($Fqdn)) {
        $Fqdn = Get-PodeAuthDomainName

        if ([string]::IsNullOrWhiteSpace($Fqdn)) {
            throw 'No domain server name has been supplied for Windows AD authentication'
        }
    }

    # 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 = Invoke-PodeUsingScriptConversion -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

        # check for state/session vars
        $ScriptBlock = Invoke-PodeStateScriptConversion -ScriptBlock $ScriptBlock
        $ScriptBlock = Invoke-PodeSessionScriptConversion -ScriptBlock $ScriptBlock
    }

    # add Windows AD auth method to server
    $PodeContext.Server.Authentications[$Name] = @{
        Scheme = $Scheme
        ScriptBlock = (Get-PodeAuthWindowsADMethod)
        Arguments = @{
            Server = $Fqdn
            Domain = $Domain
            SearchBase = $SearchBase
            Users = $Users
            Groups = $Groups
            NoGroups = $NoGroups
            OpenLDAP = $OpenLDAP
            ScriptBlock = @{
                Script = $ScriptBlock
                UsingVariables = $usingVars
            }
        }
        Sessionless = $Sessionless
        Failure = @{
            Url = $FailureUrl
            Message = $FailureMessage
        }
        Success = @{
            Url = $SuccessUrl
            UseOrigin = $SuccessUseOrigin
        }
    }
}

<#
.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
    )

    $null = $PodeContext.Server.Authentications.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.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.
 
.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
    )

    if (!(Test-PodeAuth -Name $Authentication)) {
        throw "Authentication method does not exist: $($Authentication)"
    }

    Get-PodeAuthMiddlewareScript |
        New-PodeMiddleware -ArgumentList @{ Name = $Authentication } |
        Add-PodeMiddleware -Name $Name -Route $Route

    Set-PodeOAGlobalAuth -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 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,

        [switch]
        $NoLocalCheck,

        [switch]
        $SuccessUseOrigin
    )

    # ensure we're on Windows!
    if (!(Test-PodeIsWindows)) {
        throw "IIS Authentication support is for Windows only"
    }

    # ensure the name doesn't already exist
    if (Test-PodeAuth -Name $Name) {
        throw "IIS Authentication method already defined: $($Name)"
    }

    # if we have a scriptblock, deal with using vars
    if ($null -ne $ScriptBlock) {
        $ScriptBlock, $usingVars = Invoke-PodeUsingScriptConversion -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

        # check for state/session vars
        $ScriptBlock = Invoke-PodeStateScriptConversion -ScriptBlock $ScriptBlock
        $ScriptBlock = Invoke-PodeSessionScriptConversion -ScriptBlock $ScriptBlock
    }

    # 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
            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
    )

    # ensure the name doesn't already exist
    if (Test-PodeAuth -Name $Name) {
        throw "User File Authentication method already defined: $($Name)"
    }

    # ensure the Scheme contains a scriptblock
    if (Test-PodeIsEmpty $Scheme.ScriptBlock) {
        throw "The supplied Scheme for the '$($Name)' User File authentication validator requires a valid ScriptBlock"
    }

    # if we're using sessions, ensure sessions have been setup
    if (!$Sessionless -and !(Test-PodeSessionsConfigured)) {
        throw 'Sessions are required to use session persistent authentication'
    }

    # 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)) {
        throw "The user file does not exist: $($FilePath)"
    }

    # if we have a scriptblock, deal with using vars
    if ($null -ne $ScriptBlock) {
        $ScriptBlock, $usingVars = Invoke-PodeUsingScriptConversion -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

        # check for state/session vars
        $ScriptBlock = Invoke-PodeStateScriptConversion -ScriptBlock $ScriptBlock
        $ScriptBlock = Invoke-PodeSessionScriptConversion -ScriptBlock $ScriptBlock
    }

    # add Windows AD auth method to server
    $PodeContext.Server.Authentications[$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
        }
    }
}

<#
.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
    )

    # ensure we're on Windows!
    if (!(Test-PodeIsWindows)) {
        throw "Windows Local Authentication support is for Windows only"
    }

    # ensure the name doesn't already exist
    if (Test-PodeAuth -Name $Name) {
        throw "Windows Local Authentication method already defined: $($Name)"
    }

    # ensure the Scheme contains a scriptblock
    if (Test-PodeIsEmpty $Scheme.ScriptBlock) {
        throw "The supplied Scheme for the '$($Name)' Windows Local authentication validator requires a valid ScriptBlock"
    }

    # if we're using sessions, ensure sessions have been setup
    if (!$Sessionless -and !(Test-PodeSessionsConfigured)) {
        throw 'Sessions are required to use session persistent authentication'
    }

    # if we have a scriptblock, deal with using vars
    if ($null -ne $ScriptBlock) {
        $ScriptBlock, $usingVars = Invoke-PodeUsingScriptConversion -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState

        # check for state/session vars
        $ScriptBlock = Invoke-PodeStateScriptConversion -ScriptBlock $ScriptBlock
        $ScriptBlock = Invoke-PodeSessionScriptConversion -ScriptBlock $ScriptBlock
    }

    # add Windows Local auth method to server
    $PodeContext.Server.Authentications[$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
        }
    }
}

<#
.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()]
    param(
        [Parameter(Mandatory=$true)]
        [hashtable]
        $Header,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $Payload,

        [Parameter()]
        $Secret = $null
    )

    # validate header
    if ([string]::IsNullOrWhiteSpace($Header.alg)) {
        throw "No algorithm supplied in JWT Header"
    }

    # convert the header
    $header64 = ConvertTo-PodeJwtBase64Value -Value ($Header | ConvertTo-Json -Compress)

    # convert the payload
    $payload64 = ConvertTo-PodeJwtBase64Value -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')]
    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) {
        throw "Invalid JWT supplied"
    }

    # convert to header
    $header = ConvertFrom-PodeJwtBase64Value -Value $parts[0]
    if ([string]::IsNullOrWhiteSpace($header.alg)) {
        throw "Invalid JWT header algorithm supplied"
    }

    # 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) {
        throw "No JWT signature supplied for $($header.alg)"
    }

    if (![string]::IsNullOrWhiteSpace($signature) -and $isNoneAlg) {
        throw "Expected no JWT signature to be supplied"
    }

    if ($isNoneAlg -and ($null -ne $Secret) -and ($Secret.Length -gt 0)) {
        throw "Expected a signed JWT, 'none' algorithm is not allowed"
    }

    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]) {
        throw "Invalid JWT signature supplied"
    }

    # it's valid return the payload!
    return $payload
}

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