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
        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($KeyValueSeparator, 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, $_.Value }
    $serializedSignatureParameters = $paritallySerializedSignatureParameters -join '&'

    # Generate the signature
    $signature = '{0}&{1}&{2}' -f $Method, [System.Uri]::EscapeDataString($normalizedUri), [System.Uri]::EscapeDataString($serializedSignatureParameters)
    $signatureKey = '{0}&' -f [System.Uri]::EscapeDataString((ConvertFrom-PSAuthSecureString -SecureString $OauthConsumerSecret))

    if ($PSBoundParameters.ContainsKey('OauthAccessTokenSecret'))
    {
        $signatureKey += [System.Uri]::EscapeDataString((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 = [System.Uri]::EscapeDataString($oauthSignature)

    # Now assemble the authorization hash table including parameters
    $authorizationParameters = @{
        oauth_consumer_key     = [System.Uri]::EscapeDataString($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
    )

    $getPSAuthorizationString = @{ } + $PSBoundParameters
    @(
        'Body'
        'ContentType'
        'DisableKeepAlive'
        'Headers'
        'Proxy'
        'ProxyCredential'
        'ProxyUseDefaultCredentials'
     ) | 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