Azure.Function.Tools.psm1
function ConvertTo-Base64 { <# .SYNOPSIS Converts input string to base 64. .DESCRIPTION Converts input string to base 64. .PARAMETER Text The text to encode. .PARAMETER Encoding The encoding of the input text. .EXAMPLE PS C:\> "Hello World" | ConvertTo-Base64 Converts the string "Hello World" to base 64. #> [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string[]] $Text, [System.Text.Encoding] $Encoding = [System.Text.Encoding]::UTF8 ) process { foreach ($entry in $Text) { $bytes = $Encoding.GetBytes($entry) [Convert]::ToBase64String($bytes) } } } function ConvertTo-SignedString { <# .SYNOPSIS Signs a string. .DESCRIPTION Signs a string. Used for certificate authentication. .PARAMETER Text The text to sign. .PARAMETER Certificate The certificate to sign with. Must have private key. .PARAMETER Padding The padding mechanism to use while signing. Defaults to "Pkcs1" .PARAMETER Algorithm The signing algorithm to use. Defaults to "SHA256" .PARAMETER Encoding Encoding of the source text. Defaults to UTF8 .EXAMPLE PS C:\> $token | ConvertTo-SignedString -Certificate $cert Signs the text stored in $token with the certificate stored in $cert #> [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string[]] $Text, [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, [Security.Cryptography.RSASignaturePadding] $Padding = [Security.Cryptography.RSASignaturePadding]::Pkcs1, [Security.Cryptography.HashAlgorithmName] $Algorithm = [Security.Cryptography.HashAlgorithmName]::SHA256, [System.Text.Encoding] $Encoding = [System.Text.Encoding]::UTF8 ) begin { $privateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate) } process { foreach ($entry in $Text) { $inBytes = $Encoding.GetBytes($entry) $outBytes = $privateKey.SignData($inBytes, $Algorithm, $Padding) [convert]::ToBase64String($outBytes) } } } function Connect-ClientCertificate { <# .SYNOPSIS Connect to Azure AD as an application using a certificate .DESCRIPTION Connect to Azure AD as an application using a certificate .PARAMETER Certificate The certificate to use for authentication. .PARAMETER TenantID The Guid of the tenant to connect to. .PARAMETER ClientID The ClientID / ApplicationID of the application to connect as. .PARAMETER Scope The scope to request. Used to identify the service authenticating to. Example: 'https://graph.microsoft.com/.default' .EXAMPLE PS C:\> $cert = Get-Item -Path 'Cert:\CurrentUser\My\082D5CB4BA31EED7E2E522B39992E34871C92BF5' PS C:\> Connect-ClientCertificate -TenantID '0639f07d-76e1-49cb-82ac-abcdefabcdefa' -ClientID '0639f07d-76e1-49cb-82ac-1234567890123' -Certificate $cert Connect to Azure AD with the specified cert stored in the current user's certificate store. .LINK https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateScript({ if (-not $_.HasPrivateKey) { throw "Certificate has no private key!" } $true })] [System.Security.Cryptography.X509Certificates.X509Certificate2] $Certificate, [Parameter(Mandatory = $true)] [string] $TenantID, [Parameter(Mandatory = $true)] [string] $ClientID, [Parameter(Mandatory = $true)] [string] $Scope ) $jwtHeader = @{ alg = "RS256" typ = "JWT" x5t = [Convert]::ToBase64String($Certificate.GetCertHash()) -replace '\+', '-' -replace '/', '_' -replace '=' } $encodedHeader = $jwtHeader | ConvertTo-Json | ConvertTo-Base64 $claims = @{ aud = "https://login.microsoftonline.com/$TenantID/v2.0" exp = ((Get-Date).AddMinutes(5) - (Get-Date -Date '1970.1.1')).TotalSeconds -as [int] iss = $ClientID jti = "$(New-Guid)" nbf = ((Get-Date) - (Get-Date -Date '1970.1.1')).TotalSeconds -as [int] sub = $ClientID } $encodedClaims = $claims | ConvertTo-Json | ConvertTo-Base64 $jwtPreliminary = $encodedHeader, $encodedClaims -join "." $jwtSigned = ($jwtPreliminary | ConvertTo-SignedString -Certificate $Certificate) -replace '\+', '-' -replace '/', '_' -replace '=' $jwt = $jwtPreliminary, $jwtSigned -join '.' $body = @{ client_id = $ClientID client_assertion = $jwt client_assertion_type = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' scope = $Scope grant_type = 'client_credentials' } $header = @{ Authorization = "Bearer $jwt" } $uri = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" try { Invoke-RestMethod -Uri $uri -Method Post -Body $body -Headers $header -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop } catch { $PSCmdlet.ThrowTerminatingError($_) } } function Connect-ClientSecret { <# .SYNOPSIS Connects to AzureAD using a client secret. .DESCRIPTION Connects to AzureAD using a client secret. .PARAMETER ClientID The ID of the registered app used with this authentication request. .PARAMETER TenantID The ID of the tenant connected to with this authentication request. .PARAMETER ClientSecret The actual secret used for authenticating the request. .PARAMETER Resource The resource the token grants access to. .EXAMPLE PS C:\> Connect-ClientSecret -ClientID $clientID -TenantID $tenantID -ClientSecret $secret -Resource $apiID Connects to the specified tenant using the specified client and secret. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $ClientID, [Parameter(Mandatory = $true)] [string] $TenantID, [Parameter(Mandatory = $true)] [securestring] $ClientSecret, [Parameter(Mandatory = $true)] [string] $Resource ) process { $body = @{ client_id = $ClientID client_secret = [PSCredential]::new('NoMatter', $ClientSecret).GetNetworkCredential().Password scope = $Scopes -join " " grant_type = 'client_credentials' resource = $Resource } try { $authResponse = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$TenantId/oauth2/token" -Body $body -ErrorAction Stop } catch { $PSCmdlet.ThrowTerminatingError($_) } $authResponse } } function Connect-DeviceCode { <# .SYNOPSIS Connects to Azure AD using the Device Code authentication workflow. .DESCRIPTION Connects to Azure AD using the Device Code authentication workflow. .PARAMETER ClientID The ID of the registered app used with this authentication request. .PARAMETER TenantID The ID of the tenant connected to with this authentication request. .PARAMETER Scopes The scopes to request. .EXAMPLE PS C:\> Connect-DeviceCode -ClientID $clientID -TenantID $tenantID -Scopes 'api://d9b68662-0add-46ec-aab2-0123456788910/.default' Connects to the specified tenant using the specified client, prompting the user to authorize via Browser. Requestss the default scopes for the specified custom API #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $ClientID, [Parameter(Mandatory = $true)] [string] $TenantID, [Parameter(Mandatory = $true)] [string[]] $Scopes ) try { $initialResponse = Invoke-RestMethod -Method POST -Uri "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/devicecode" -Body @{ client_id = $ClientID scope = $Scopes -join " " } -ErrorAction Stop } catch { throw } Write-Host $initialResponse.message $paramRetrieve = @{ Uri = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" Method = "POST" Body = @{ grant_type = "urn:ietf:params:oauth:grant-type:device_code" client_id = $ClientID device_code = $initialResponse.device_code } ErrorAction = 'Stop' } $limit = (Get-Date).AddSeconds($initialResponse.expires_in) while ($true) { if ((Get-Date) -gt $limit) { throw "Timelimit exceeded, device code authentication failed" } Start-Sleep -Seconds $initialResponse.interval try { $authResponse = Invoke-RestMethod @paramRetrieve } catch { if ($_ -match '"error":"authorization_pending"') { continue } $PSCmdlet.ThrowTerminatingError($_) } if ($authResponse) { break } } $authResponse } function Get-ConfigValue { <# .SYNOPSIS Returns a configured value. .DESCRIPTION Returns a configured value. Use Import-Config to define configuration values. Will write warnings if no data found. .PARAMETER Name The name of the setting to retrieve. .EXAMPLE PS C:\> Get-COnfigValue -Name VaultName Returns the value configured for the "VaultName" setting #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name ) if (-not $Global:config) { Write-Warning "[Get-ConfigValue] No configuration defined yet." return } if ($Global:config.Keys -notcontains $Name) { Write-Warning "[Get-ConfigValue] Configuration entry not found: $Name" return } $Global:config.$Name } function Get-RestParameter { <# .SYNOPSIS Parses the rest request parameters for all values matching parameters on the specified command. .DESCRIPTION Parses the rest request parameters for all values matching parameters on the specified command. Returns a hashtable ready for splatting. Does NOT assert mandatory parameters are specified, so command invocation may fail. .PARAMETER Request The original rest request object, containing the caller's information such as parameters. .PARAMETER Command The command to which to bind input parameters. .EXAMPLE PS C:\> Get-RestParameter -Request $Request -Command Get-AzUser Retrieves all parameters on the incoming request that match a parameter on Get-AzUser #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] [OutputType([hashtable])] [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] $Request, [Parameter(Mandatory = $true)] [string] $Command ) begin { $newRequest = [PSCustomObject]@{ Query = $Request.Query Body = $Request.Body } if ($newRequest.Body -and $newRequest.Body -is [string]) { try { $newRequest.Body = $newRequest.Body | ConvertFrom-Json -AsHashtable -ErrorAction Stop } catch { } } if ($newRequest.Query -and $newRequest.Query -is [string]) { try { $newRequest.Query = $newRequest.Query | ConvertFrom-Json -AsHashtable -ErrorAction Stop } catch { } } } process { $commandInfo = Get-Command -Name $Command $results = @{ } foreach ($parameter in $commandInfo.Parameters.Keys) { $value = Get-RestParameterValue -Request $newRequest -Name $parameter if ($null -ne $value) { $results[$parameter] = $value } } $results } } function Get-RestParameterValue { <# .SYNOPSIS Extract the exact value of a parameter provided by the user. .DESCRIPTION Extract the exact value of a parameter provided by the user. Expects either query or body parameters from the rest call to the http trigger. .PARAMETER Request The request object provided as part of the function call. .PARAMETER Name The name of the parameter to provide. .EXAMPLE PS C:\> Get-RestParameterValue -Request $Request -Name Type Returns the value of the parameter "Type", as provided by the caller #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $Request, [Parameter(Mandatory = $true)] [string] $Name ) if ($Request.Query.$Name) { return $Request.Query.$Name } $Request.Body.$Name } function Get-VaultCertificate { <# .SYNOPSIS Retrieve a certificate from Azure KeyVault. .DESCRIPTION Retrieve a certificate from Azure KeyVault. Expects to already be logged in using Connect-AzAccount. .PARAMETER SecretName The name of the secret under which the certificate is stored. .PARAMETER VaultName The name of the Key Vault from which to retrieve the certificate. Defaults to whatever is configured under the configuration entry "VaultName" (See Import-Config and Get-ConfigValue for details) .EXAMPLE PS C:\> Get-VaultCertificate -SecretName myCert Retrieves the certificate "myCert" from the configured default Key Vault #> [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [string] $SecretName, [string] $VaultName = (Get-ConfigValue -Name VaultName) ) if (-not $VaultName) { throw "No vault name found! Either use the -VaultName parameter or provide a configuration setting for 'VaultName'" } try { $secret = Get-AzKeyVaultSecret -VaultName $VaultName -Name $SecretName -ErrorAction Stop } catch { throw } $certString = [PSCredential]::New("irrelevant", $secret.SecretValue).GetNetworkCredential().Password $bytes = [convert]::FromBase64String($certString) [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes) } function Get-VaultCredential { <# .SYNOPSIS Reads a credential object from Azure KeyVault. .DESCRIPTION Reads a credential object from Azure KeyVault. There are two ways to provide a credential object through this command: + Inline: The credentials are stored in a single secret, separated by a pipe symbol: <username>|<password> + Dual: The credentials are stored in two secrets. The secret names must then both start with the specified -SecretName, then end in ".UserName" and ".Password". E.g. "MySecret.UserName" and "MySecret.Password" .PARAMETER SecretName The name of the secret storing the credentials. .PARAMETER Type The type of credentials to retrieve. This can be either "Inline" or "Dual". See the description for details. .PARAMETER VaultName The name of the Key Vault from which to retrieve the certificate. Defaults to whatever is configured under the configuration entry "VaultName" (See Import-Config and Get-ConfigValue for details) .EXAMPLE PS C:\> Get-VaultCredential -SecretName myCred -Type Inline Retrieves the credentials stored in the myCred secret within the configured default Key Vault. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] [OutputType([PSCredential])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [string] $SecretName, [Parameter(Mandatory = $true)] [ValidateSet('Inline','Dual')] [string] $Type, [string] $VaultName = (Get-ConfigValue -Name VaultName) ) if (-not $VaultName) { throw "No vault name found! Either use the -VaultName parameter or provide a configuration setting for 'VaultName'" } switch ($Type) { 'Inline' { try { $secret = Get-AzKeyVaultSecret -VaultName $VaultName -Name $SecretName -ErrorAction Stop } catch { throw } $value = [PSCredential]::New("Irrelevant", $secret.SecretValue).GetNetworkCredential().Password $name, $password = $value -split "\|",2 [PSCredential]::new($name, ($password | ConvertTo-SecureString -AsPlainText -Force)) } 'Dual' { try { $secretName = Get-AzKeyVaultSecret -VaultName $VaultName -Name "$SecretName.UserName" -ErrorAction Stop } catch { throw } try { $secretPassword = Get-AzKeyVaultSecret -VaultName $VaultName -Name "$SecretName.Password" -ErrorAction Stop } catch { throw } $userName = [PSCredential]::New("Irrelevant", $secretName.SecretValue).GetNetworkCredential().Password [PSCredential]::new($userName, $secretPassword.SecretValue) } } } function Import-Config { <# .SYNOPSIS Imports a set of data into a global config variable. .DESCRIPTION Imports a set of data into a global config variable. Used by other commands for default values. Use Get-ConfigValue to read config settings. Supports both Json and psd1 files, does not resolve any nesting of values. .PARAMETER Path Path to the config file to read .EXAMPLE PS C:\> Import-Config -Path "$PSScriptRoot\config.psd1" Loads the config.psd1 file from the folder of the calling file's. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Path ) $configData = if ($Path -like "*.psd1") { Import-PowerShellDataFile -Path $Path } else { Get-Content -Path $Path | ConvertFrom-Json } if (-not $global:config) { $global:config = @{ } } if ($configData -is [Hashtable]) { foreach ($pair in $configData.GetEnumerator()) { $global:config[$pair.Key] = $pair.Value } return } foreach ($property in $configData.PSObject.Properties) { $global:config[$property.Name] = $property.Value } } function Read-TokenScope { <# .SYNOPSIS Reads the scopes of the JWT token provided. .DESCRIPTION Reads the scopes of the JWT token provided. Use this to verify, whether a connecting user has the scopes required. .PARAMETER Token The JWT token to parse .PARAMETER Trigger The trigger object provided by an Azure Function Endpoint .EXAMPLE PS C:\> Read-TokenScope -Trigger $TriggerMetadata Returns the scopes of the user triggering the Azure Function App #> [cmdletbinding()] param( [Parameter(Mandatory=$true, ParameterSetName = 'Token')] [string] $Token, [Parameter(Mandatory=$true, ParameterSetName = 'Trigger')] $Trigger ) if ($Trigger) { $Token = $Trigger.Headers.Authorization -replace "^Bearer " } $tokenPayload = $Token.Split(".")[1].Replace('-', '+').Replace('_', '/') # Pad with "=" until string length modulus 4 reaches 0 while ($tokenPayload.Length % 4) { $tokenPayload += "=" } $bytes = [System.Convert]::FromBase64String($tokenPayload) ([System.Text.Encoding]::ASCII.GetString($bytes) | ConvertFrom-Json).roles } function Set-ConfigValue { <# .SYNOPSIS Set a config value. .DESCRIPTION Set a config value. Use Get-ConfigValue to later retrieve it. .PARAMETER Name Name of the setting to set .PARAMETER Value Value to set the setting to .EXAMPLE PS C:\> Set-ConfigValue -Name VaultName -Value myVault Set the config setting "VaultName" to the value "myVault" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Name, [Parameter(Mandatory = $true, Position = 1)] $Value ) if (-not $Global:config) { $Global:config = @{ } } $Global:config[$Name] = $Value } function Write-FunctionResult { <# .SYNOPSIS Reports back the output / result of the function app. .DESCRIPTION Reports back the output / result of the function app. .PARAMETER Status Whether the function succeeded or not. .PARAMETER Body Any data to include in the response. .EXAMPLE PS C:\> Write-FunctionResult -Status OK -Body $newUser Reports success while returning the content of $newUser as output #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Net.HttpStatusCode] $Status, [AllowNull()] $Body ) Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $Status Body = $Body }) } |