PSAuth.psm1

<#
.EXTERNALHELP PSAuth-help.xml
#>

#Requires -version 5.0

$moduleRoot = Split-Path `
    -Path $MyInvocation.MyCommand.Path `
    -Parent

#region LocalizedData
$culture = 'en-us'
if (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath $PSUICulture))
{
    $culture = $PSUICulture
}

Import-LocalizedData `
    -BindingVariable LocalizedData `
    -Filename 'PSAuth.strings.psd1' `
    -BaseDirectory $moduleRoot `
    -UICulture $culture
#endregion

#region Functions
<#
    .SYNOPSIS
        Convert a SecureString back to a string.
 
    .PARAMETER SecureString
        The SecureString to convert back to a string.
#>

function ConvertFrom-PSAuthSecureString
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Security.SecureString]
        $SecureString
    )

    $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
    return [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
}


<#
    .SYNOPSIS
        URL Encode a string using RFC 3986 standards.
 
    .PARAMETER String
        The string to URL encode.
 
    .NOTES
        This function is required because there is no standard
        library in .NET and .NET Core that URL encodes to RFC 3986.
#>

function ConvertTo-PSUrlEncodedString
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $String
    )

    $doNotEncodeCharacters = [char[]]'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~'
    $result = New-Object -Typename System.Text.StringBuilder

    foreach ($character in $String.ToCharArray())
    {
        if ($doNotEncodeCharacters -contains $character)
        {
            $null = $result.Append($character)
        }
        else
        {
            $null = $result.Append(('%{0:X2}' -f [System.Int32] $character))
        }
    }

    return $result.ToString()
}

<#
    .SYNOPSIS
        Generate a nonce for use with Oauth.
 
    .NOTES
        This function exists to make unit testing easier.
#>

function Get-PSAuthNonce
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param ()

    return [System.Guid]::NewGuid().Guid -replace '-'
}

<#
    .SYNOPSIS
        Normalize a URI for use in an Oauth signature.
 
    .DESCRIPTION
        Normalize a URI by converting the hostname into all lower case
        and ensure port is included if not HTTP/HTTPS.
 
    .PARAMETER Uri
        The URI to normalize.
#>

function Get-PSAuthNormalizedUri
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Uri]
        $Uri
    )

    $normalizedUri = ('{0}://{1}' -f $Uri.Scheme, $Uri.Host).ToLower()

    if (-not (($Uri.Scheme -eq 'http' -and $Uri.Port -eq 80) `
                -or ($Uri.Scheme -eq 'https' -and $Uri.Port -eq 443)))
    {
        $normalizedUri += ':' + $Uri.Port
    }

    $normalizedUri += $Uri.AbsolutePath

    return $normalizedUri
}

function Get-PSAuthorizationString
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Uri]
        $Uri,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $OauthConsumerKey,

        [Parameter(Mandatory = $true)]
        [System.Security.SecureString]
        $OauthConsumerSecret,

        [Parameter(Mandatory = $false)]
        [System.String]
        $OauthAccessToken,

        [Parameter(Mandatory = $false)]
        [System.Security.SecureString]
        $OauthAccessTokenSecret,

        [Parameter(Mandatory = $false)]
        [ValidateSet('HMAC-SHA1', 'HMAC-SHA256')]
        [System.String]
        $OauthSignatureMethod = 'HMAC-SHA1',

        [Parameter(Mandatory = $false)]
        [ValidateSet('1.0')]
        [System.String]
        $OauthVersion = '1.0',

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.Collections.Hashtable]
        $OauthParameters = @{ },

        [Parameter(Mandatory = $false)]
        [ValidateSet('Default', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [System.String]
        $Method = 'Get'
    )

    $Method = $Method.ToUpper()
    $normalizedUri = Get-PSAuthNormalizedUri -Uri $Uri
    $oauthTimestamp = Get-PSAuthTimestamp
    $oauthNonce = Get-PSAuthNonce

    # Create a hash table containing the parameters to include in the signature
    $signatureParameters = @{
        oauth_consumer_key     = $OauthConsumerKey
        oauth_signature_method = $OauthSignatureMethod
        oauth_timestamp        = $oauthTimestamp
        oauth_nonce            = $oauthNonce
        oauth_version          = $OauthVersion
    }

    # If an access token is specified add that to the signature parameters
    if ($PSBoundParameters.ContainsKey('OauthAccessToken'))
    {
        $signatureParameters += @{ oauth_token = $OauthAccessToken }
    }

    # Add any optional Oauth parameters to the signature parameters
    foreach ($oauthParameter in $OauthParameters.GetEnumerator())
    {
        $signatureParameters += @{ $oauthParameter.Name = $oauthParameter.Value }
    }

    # If any query string parameters are passed include these in the signature parameters
    if ($Uri.Query)
    {
        foreach ($queryItem in $Uri.Query.TrimStart('?').Split('&'))
        {
            $key, $value = $queryItem.split('=', 2)
            $signatureParameters += @{
                $key = $value
            }
        }
    }

    # Serialize all the signature parameters into a string ordered by Name
    $orderedSignatureParameters = $signatureParameters.GetEnumerator() | Sort-Object -Property Name
    $paritallySerializedSignatureParameters = $orderedSignatureParameters | Foreach-Object -Process {
        '{0}={1}' -f $_.Name, (ConvertTo-PSUrlEncodedString -String ($_.Value))
    }
    $serializedSignatureParameters = $paritallySerializedSignatureParameters -join '&'

    # Generate the signature
    $urlEncodedNormalizedUri = ConvertTo-PSUrlEncodedString -String $normalizedUri
    $urlEncodedSerializedSignatureParameters = ConvertTo-PSUrlEncodedString -String $serializedSignatureParameters
    $signature = '{0}&{1}&{2}' -f $Method, $urlEncodedNormalizedUri, $urlEncodedSerializedSignatureParameters
    $signatureKey = '{0}&' -f (ConvertTo-PSUrlEncodedString -String (ConvertFrom-PSAuthSecureString -SecureString $OauthConsumerSecret))

    if ($PSBoundParameters.ContainsKey('OauthAccessTokenSecret'))
    {
        $signatureKey += (ConvertTo-PSUrlEncodedString -String (ConvertFrom-PSAuthSecureString -SecureString $OauthAccessTokenSecret))
    }

    # Select the Signature method
    switch ($OauthSignatureMethod)
    {
        'HMAC-SHA1'
        {
            $signatureHashGenerator = New-Object -TypeName System.Security.Cryptography.HMACSHA1
        }

        'HMAC-SHA256'
        {
            $signatureHashGenerator = New-Object -TypeName System.Security.Cryptography.HMACSHA256
        }

    }

    $signatureHashGenerator.Key = [System.Text.Encoding]::Ascii.GetBytes($signatureKey)
    $oauthSignature = [System.Convert]::ToBase64String($signatureHashGenerator.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($signature)))
    $escapedOauthSignature = ConvertTo-PSUrlEncodedString -String $oauthSignature

    # Now assemble the authorization hash table including parameters
    $authorizationParameters = @{
        oauth_consumer_key     = ConvertTo-PSUrlEncodedString -String $OauthConsumerKey
        oauth_nonce            = $oauthNonce
        oauth_signature        = $escapedOauthSignature
        oauth_signature_method = $OauthSignatureMethod
        oauth_timestamp        = $oauthTimestamp
        oauth_version          = $OauthVersion
    }

    if ($PSBoundParameters.ContainsKey('OauthAccessToken'))
    {
        $authorizationParameters += @{ oauth_token = $OauthAccessToken }
    }

    $orderedAuthorizationParameters = $authorizationParameters.GetEnumerator() | Sort-Object -Property Name
    $partiallySerializedAuthorizationParameters = $orderedAuthorizationParameters | Foreach-Object -Process {
        '{0}="{1}"' -f $_.Name, $_.Value
    }
    $serializedAuthorizationParameters = $partiallySerializedAuthorizationParameters -join ','
    $authorizationString = 'OAuth {0}' -f $serializedAuthorizationParameters

    Write-Verbose -Message ($LocalizedData.AuthorizationStringGeneratedMessage -f $authorizationString.Replace($escapedOauthSignature, [System.String]::new('*', 20)))

    return $authorizationString
}

<#
    .SYNOPSIS
        Generate a time stamp for use with Oauth.
 
    .NOTES
        This function exists to make unit testing easier.
#>

function Get-PSAuthTimestamp
{
    [CmdletBinding()]
    [OutputType([System.Int32])]
    param ()

    return [System.Int32] ((Get-Date).ToUniversalTime() - (Get-Date -Date '1/1/1970')).TotalSeconds
}

function Invoke-PSAuthRestMethod
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Uri]
        $Uri,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $OauthConsumerKey,

        [Parameter(Mandatory = $true)]
        [System.Security.SecureString]
        $OauthConsumerSecret,

        [Parameter(Mandatory = $false)]
        [System.String]
        $OauthAccessToken,

        [Parameter(Mandatory = $false)]
        [System.Security.SecureString]
        $OauthAccessTokenSecret,

        [Parameter(Mandatory = $false)]
        [ValidateSet('HMAC-SHA1', 'HMAC-SHA256')]
        [System.String]
        $OauthSignatureMethod = 'HMAC-SHA1',

        [Parameter(Mandatory = $false)]
        [ValidateSet('1.0')]
        [System.String]
        $OauthVersion = '1.0',

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [System.Collections.Hashtable]
        $OauthParameters = @{},

        [Parameter(Mandatory = $false)]
        [ValidateSet('Default', 'Delete', 'Get', 'Head', 'Merge', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [System.String]
        $Method = 'Get',

        [Parameter(Mandatory = $false)]
        [System.String]
        $ContentType,

        [Parameter(Mandatory = $false)]
        [System.String]
        $Body,

        [Parameter(Mandatory = $false)]
        [System.Collections.IDictionary]
        $Headers,

        [Parameter(Mandatory = $false)]
        [System.String]
        $Proxy,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $ProxyCredential,

        [Parameter(Mandatory = $false)]
        [Switch]
        $ProxyUseDefaultCredentials,

        [Parameter(Mandatory = $false)]
        [Switch]
        $DisableKeepAlive,

        [Parameter(Mandatory = $false)]
        [System.String]
        $OutFile
    )

    $getPSAuthorizationString = @{ } + $PSBoundParameters
    @(
        'Body'
        'ContentType'
        'DisableKeepAlive'
        'Headers'
        'Proxy'
        'ProxyCredential'
        'ProxyUseDefaultCredentials'
        'OutFile'
     ) | ForEach-Object -Process { $null = $getPSAuthorizationString.Remove($_) }

    $authorization = Get-PSAuthorizationString @getPSAuthorizationString

    # Take all the parameters passed to this function and pass them to
    $invokeRestMethodParameters = @{ } + $PSBoundParameters

    $headers += @{ 'Authorization' = $authorization }

    # Remove parameters that should not be passed to Invoke-RestMethod
    $null = $invokeRestMethodParameters.Remove('OauthConsumerKey')
    $null = $invokeRestMethodParameters.Remove('OauthConsumerSecret')
    $null = $invokeRestMethodParameters.Remove('OauthAccessToken')
    $null = $invokeRestMethodParameters.Remove('OauthAccessTokenSecret')
    $null = $invokeRestMethodParameters.Remove('OauthSignatureMethod')
    $null = $invokeRestMethodParameters.Remove('OauthVersion')
    $null = $invokeRestMethodParameters.Remove('OauthParameters')

    if ($method -notin 'POST', 'PUT')
    {
        # Remove Body parameter for all methods except POST and PUT
        $null = $invokeRestMethodParameters.Remove('Body')
    }

    $null = $invokeRestMethodParameters['Headers'] = $headers

    return Invoke-RestMethod @invokeRestMethodParameters
}


#endregion