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($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, (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 ) $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 |