Azure Functions

Function concept with PowerShell

Azure Function Apps is a "Function as a Service" solution and offers serverless execution of highly scalable code. This function can then be executed via HTTP requests and provide corresponding feedback in the HTTP response. However, this technology only makes sense if several requests are to be executed simultaneously (e.g. all devices are to fetch or save information). See chapter "Decision Support Technology (Comparison of Variants)".

Function

The Functions are elements in the Azure Function Apps that contain the code and are then executed. Functions can have different triggers. Functions can be named according to the naming convention.

PowerShell Modules

In order to use PowerShell modules, they must first be added to a config file in the Azure Function App. This can be achieved in the Function App via "App files" → "requirements.psd1" → Add module name and version to PowerShell list. However, adding the module to the Azure Function can take several 10 minutes.

image.png

Import PowerShell Modules into scripts

Import-Module AzureAD

Authentication to Azure Function App

Authentication on the Azure function is done via the function keys, which are specified as a query. These function keys can be created on the function itself via → "Function Keys":image.png

I don't use App Keys because I want to map multiple functions in an Azure Function App and then I wouldn't be able to use the security benefits of multiple keys.

The naming convention for Azure Function Keys is as follows:

FKEY-<Technology>-<KGTag>-<Object>-<Description>-<Status>-<Location>.
Example: FKEY-RS-ALL-KEY-SetLanguageByUPN-PROD-WE

Triggers

Triggers mean types to call the Azure Function and execute the code.

Manual

The Azure Function can be started manually via the Azure Portal.

image.png

Schedules

Azure Functions can be executed regularly using Schedules. However, since Azure Runbook also offers this function, in most cases it makes more sense to create an Azure Runbook and not functions with schedules.

General REST Call

The function can be triggered via an HTTP request. The return can then also be found in the response body.image.png

PowerShell

Via PowerShell, this can be achieved directly using the standard module "Invoke-Restmethod" with the corresponding URL and the function key.

$url="https://<azurefunctionappname>.azurewebsites.net/api/<azurefunction>?code=<azurefunctionkey>"
$Body = @"
{
    "InputString":"$TestVariable",
}
"@
 
$Output = (Invoke-Restmethod -uri $url -Body $Body -Method POST -ContentType "application/json").value

How to Function

This is a short guide for creating an Azure Function. This is a high-level concept in order to be able to make the preparations correctly and completely so that no unwanted surprises occur later during implementation. 

Prerequisites

Defined goal

A defined goal is necessary that we can check if our end product is doing what we are expecting. This can be as simple as one phrase.

Example:

My Azure Function should receive logs via HTTP and stores them into one log analytics. The response should indicate if the process was executed successfully.

Defined input

To know how to start the code, we first need to define the input(s) we need to get with the request to reach our goal. This can be static values or multiple dynamic values.

Example:

{
	"logtype":"<logtypetoseperate>",
  	"logbody":{
    	"dynamictag1":"dynamicvalue1"
      	...
    }
}

In this case the object member "logtype" is static and only the value can be filled in on this specific field. On the other hand, "logbody" is dynamic. It should be possible to have one or one hundred members in the sub object "logbody".

Defined output

The output defines if the Function needs to return a value or which HTTP status code is expected.

Example:

Response: 200 OK

{
	"language":"<languagecode>"
}

For the Function to work as intended, it should respond with the HTTP status code 200 and return this JSON structure.

Trigger method

The method is important that we know what oder which object(s) are triggering this function.

Example:

This function should be called from every Windows device via PowerShell. The trigger should be over HTTP.

Logic model

The logic model defines the logic of the code in a very high-level draft.

Example:

  1. Get input
  2. Check input values
  3. Parse data for Log Analytics API 
  4. Send data to Log Analytics API
  5. Get response from Log Analytics API
  6. Send response

Decisions

Coding language

Azure Function Apps can execute different code with different languages. This is defined per Azure Function App and cannot be changed. Azure Functions offers these languages for code execution:

Billing plan

The billing plan defines if your function runs consumed based billing or premium plan billing. The benefits of each solution can be found here: Pricing - Functions | Microsoft Azure
For most use cases the consumption plan will be more than enough. 

Procedure

Create Function App

First you need an Azure Function App. On this you have to define the parts defined under "Descisions" and you assign the function into an Azure resource group.

Create Function

The Function itself holds the code which gets executed. It also is the unique identifier in the API URL to call if you use HTTP as a trigger. While creating a Function you can set the name and the trigger. For triggers you can choose between these ones:

image.png

After the trigger and the name of the function is set you can choose the authorization level of the function. 

image.png

For testing purposes, the Anonymous option is probably the easiest. For production functions it is recommended to use the Function level of authorization.

Code

When created you can copy your code into the web editor of the Function or start to develop the code directly in the Azure Portal.

Testing

For testing you can use the built in Test/Run option. If this behaves as it should, you can get the function URL with the push of a button. For testing the built-in default key is enough.

Create Function Key

The best practice is to use Function Keys as authorization on the Function level for production ready APIs. This code is then provided in the URL Query. For each workload which uses the same function it is best practice to use a separate Function Key so the access can be revoked per consumer.

Things to consider

Maximal runtime

The maximal runtime of one Azure Function is 230 seconds. After this time the function code gets cancelled and returns a 503 error.

Centralize log collection with a custom REST API

All environments of a larger IT team write several thousand logs and status messages every day. These logs are usually only stored on the respective systems and cannot be centrally administered or managed. Alarms or monitoring on a log basis in a centralised solution is also not available.

This solution is intended to remedy these problems. On the one hand, a complex topic should be broken down and standardised in a simple solution. The installation of the solution should be as simple as possible and the evaluation of the collected data should be structured in an easily understandable dashboard. Standard components should be used whenever possible, based on standard configurations.

Use case

This API is ideal to create a middleware between the logging client and the logging storage. This brings advantages such as having full control over the secrets, access codes and to take the complexity out of the logging solution on the client. This means Azure Runbooks or Intune Remediation Scripts send the corresponding logs directly to a central log storage pot. This storage pot can then be evaluated and the logs stored and evaluated accordingly. There is also the possibility that monitoring can be built on the logs, as all logs can be stored centrally and for a longer period of time.

In an automated process, a log entry should be created for each important event. The log should be transmitted directly at this point in time, as otherwise the status of the process cannot be clearly traced in the event of a next potentially faulty step.

Function code

This code will run in the Azure Function. It receives two input types via an REST API Body. The LogType sets the table, in which the data will be saved to. The second object is dynamic and can contain as much elements as the creator needs.

Important: To get this to work you have to get the customer id and the shared key from the appropriate log analytics workspace. 

using namespace System.Net

# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)

$logtype = $Request.Body.logtype
$logbody = $Request.Body.logbody

$customerId = ""
$sharedKey = ""

Function Build-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource){
    $xHeaders = "x-ms-date:" + $date
    $stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource
 
    $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash)
    $keyBytes = [Convert]::FromBase64String($sharedKey)
 
    $sha256 = New-Object System.Security.Cryptography.HMACSHA256
    $sha256.Key = $keyBytes
    $calculatedHash = $sha256.ComputeHash($bytesToHash)
    $encodedHash = [Convert]::ToBase64String($calculatedHash)
    $authorization = 'SharedKey {0}:{1}' -f $customerId,$encodedHash
    return $authorization
}

Function Post-LogAnalyticsData ($customerId, $sharedKey, $body, $logType){
    $method = "POST"
    $contentType = "application/json"
    $resource = "/api/logs"
    $rfc1123date = ([DateTime]::UtcNow).ToString("r")
    $contentLength = $body.Length
    $signature = Build-Signature -customerId $customerId -sharedKey $sharedKey -date $rfc1123date -contentLength $contentLength -method $method -contentType $contentType -resource $resource

    $uri = "https://" + $customerId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01"
 
    $headers = @{
        "Authorization" = $signature;
        "Log-Type" = $logType;
        "x-ms-date" = $rfc1123date;
    }
 
    $response = Invoke-WebRequest -Uri $uri -Method $method -ContentType $contentType -Headers $headers -Body $body -UseBasicParsing
    return $response.StatusCode
}

$logbodyjson = ConvertTo-JSON $logbody -Depth 10

#Submit the data to the API endpoint
$params = @{
    CustomerId = $customerId
    SharedKey  = $sharedKey
    Body       = ([System.Text.Encoding]::UTF8.GetBytes($logbodyjson))
    LogType    = $LogType
}
$LogResponse = Post-LogAnalyticsData @params
$LogResponse

if($LogResponse -eq 200){
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
        StatusCode = [HttpStatusCode]::OK
        Body = $LogResponse
    })
}

This Azure Function returns the Code 200 if the POST Request and the storage in the Azure Log Analytics were successful. 

Send logs via PowerShell script

On the client side you can use this PowerShell function to send the logs to the previously created Azure Function. First the Function URL must be inserted into the variable "url". After that you can initialize the function at the very beginning of your PowerShell script. Afterwards you can set the values, that need to be logged, into the Hash Table "Logs". This Hash Table can then be sent via the Function Send-Logs() to the Azure Function API which then stores the code in the Azure Log Analytics Workspace. That it knows which table it should write the data to, you have to specify the LogType on the Send-Logs() Function. 

Function Send-Logs(){

    param (
        [String]$LogType,
        [Hashtable]$LogBodyList
    )
    $url="<functionurl>"
    
    $LogBodyJSON = @"
    {
        "logtype": "$LogType",
        "logbody": {}
    }
"@

    $LogBodyObject = ConvertFrom-JSON $LogBodyJSON
    Foreach($Log in $LogBodyList.GetEnumerator()){
        $LogBodyObject.logbody | Add-Member -MemberType NoteProperty -Name $log.key -Value $log.value
    }
    $Body = ConvertTo-JSON $LogBodyObject
    
    $Response = Invoke-Restmethod -uri $url -Body $Body -Method POST -ContentType "application/json"
    return $Response
}

$Logs = @{
    "<logmember1>"="<logvalue1>"
    "<logmember2>"="<logvalue2>"
}

Send-Logs -LogType "<logtype>" -LogBodyList $Logs

This Function then returns the HTTP status code from the LogCollection API. 

Credential handling with Azure KeyVault

With this option, the secrets for Azure Functions are stored in an Azure Key Vault. An access policy is placed on the Key Vault which only allows the Managed Identity of an Azure Function to read the secret. On the Azure Function side, a Managed Identity is set up as well as an Environment Variable that is linked to the Secret in the Key Vault. 

Activate managed identity

To authenticate against the Key Vault you can activate the Managed Identity of your Function App. Thus, permissions can be given to the function app and used within the function.

image.png

Create new Key Vault Secret

Create a new secret in the key vault and give this secret a unique name. This name will then be used to create a link from the Function App Variable to the Key Vault.

image.png

image.png

Activate access policies

Go to access policies to create a new permission container for the Azure Function.

image.png

Select the necessary permissions for your function app.

image.png

Select the appropriate Managed Identity from the function, which was created earlier.

image.png

Add environment variable link

Then you can get the Key Vault name and secret name. With this information you can create an Application setting which will create an environment variable to use in the Azure Functions code. Set the name of the Environment Variable and create a link to the corresponding secret in the Key Vault.

image.png

@Microsoft.KeyVault(SecretUri=https://<keyvaultname>.vault.azure.net/secrets/<secretname>) 

Usage in function code

When everything is implemented as described you can use the variable accordingly:

$testsecret = $env:testsecret

Access Azure Function App via OAuth 2.0 authentication

This is a guide to protect Azure Function executions using OAuth 2.0. So the execution of the code is not possible without Client ID and ClientSecret. This allows a much more secure authentication than just using function codes in the URL in the query.

Disable authentication

To use the function with OAuth 2.0, the authentication on the function itself must first be set to Anonymous.

image.png

 

Identity provider

Then a new identity provider must be added to the Azure Function App. This can be done by going into the blade "Authentication":

image.png

There you have to select "Microsoft" as the identity provider. There you can decide if you want to use an existing App Registration or want to create one.

image.png

It is also recommended to send a 401 Unauthorized Response for incorrectly authenticated requests.

Afterwards, the app registration has to be adjusted so that the token handling works properly. To adjust the URL, the identity provider must be adjusted using "Edit".

image.png

The issuer URL must be adjusted. The "/v.2.0" at the end must be removed 

image.png

Authentication via PowerShell

Then PowerShell can be used to authenticate against the app registration. The App Registration then has permissions to execute all Azure Functions in the Azure Function App.

$TenantId ="<yourtenantid>"
$ClientID = "<yourclientid>"
$ClientSecret = "<yourclientsecret>"

$FunctionAppId = "<yourfunctionappid>" 
$FunctionApiAuthUrl = "$functionuri/.auth/login/aad" 
$functionapi = "/api/HttpTrigger2"

# Authenticate against MEID to get access token with App Registration Client Secret
$Body = @{ 
    "tenant" = "$TenantId" 
    "client_id" = "$ClientID" 
    "scope" = "api://$functionappid/.default" 
    "grant_type" = "client_credentials" 
    "client_secret" = $ClientSecret 
} 

$Params = @{
    "Uri" = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
    "Method" = "Post"
    "Body" = $Body
    "ContentType" = "application/x-www-form-urlencoded"
}
    
$AuthResponse = Invoke-RestMethod @Params

Function execution via PowerShell

The second part of the authentication is to ask the function api for a token and then execute it using the token received:

# Authenticate against function with the MEID access token
$FunctionAuthBody = @{ 
    "access_token" = $AuthResponse.access_token 
}

$functionToken = Invoke-RestMethod -Method POST -Uri $FunctionApiAuthUrl -Body (ConvertTo-Json $FunctionAuthBody) -ContentType "application/json" 

$Header = @{ 
    "X-ZUMO-AUTH" = $functionToken.authenticationToken 
}

# Run Azure Function with OAuth2.0 Token Authentication
Invoke-RestMethod -Method POST -Uri $functionuri$functionapi -Headers $Header

Create reference to Azure Key Vault content from function code

Requirements: Basic Azure Function knowledge and access to an Azure Key Vault & Azure Function.

This topic shows you how to work with secrets from Azure Key Vault in your Azure Functions code without requiring any code changes. Azure Key Vault is a service that provides centralized secrets management, with full control over access policies and audit history. This article uses managed identities to access other resources.

Add your secret to Azure Key Vault

First, the secret must be created in the Azure Key Vault. For this, an Azure Key Vault must exist and the permissions to create a new item must be available. There you can insert the secret that you want to use later in the code.

image.png

Create managed identity of function

To be able to display the secret in the Function code, you have to activate the Managed Identity in the Function App. You can do this via the menu item "Identity" and then switch the status to "On" under "System assigned". Don't forget to save your selection.

image.png

Create access policy

Then you can go back to the Key Vault and create a new access policy under "Access policies" -> "Create".

image.png

There you have to select the desired permissions. The Azure Function then connects to the Key Vault with these permissions. To read only the secret content, only the "Get" permission under "Secret permissions" is used.

image.png

At the end you have to select the managed identity of the Azure Function and save the access policy.

Create environment variable link

Here you first enter the name of the variable that you want to address in the code. Under "Value" you have to insert the following content and complete it with your values:

@Microsoft.KeyVault(SecretUri=https://<keyvaultname>.vault.azure.net/secrets/<secretname>

image.png

Get environment variable content

Then you can read the variable in your function code using the environment as follows:

$env:<secretname>

image.png

Thus, the values can be stored securely without all user accounts needing authorization.

Run PowerShell Code from frontend on backend using Azure Function

This tutorial shows how to build a REST API that executes PowerShell code using an Azure Function. This code can then return values and objects as JSON responses using "return". 

Using Function

To use this function you need the following:

To execute the code on the server, you need to send a JSON to the function URL via POST request. It is important that the POST call goes to the correct URL. You can get this URL on the function level via this button:

image.png

The URL is structured as follows:

https://<yourfunctionappname>.azurewebsites.net/api/<yourfunctionname>?code=<yourfunctionkey>

The JSON which is needed consists of the main key "parameters", which then must have the following four keys. The keys starting with "microsoft" are needed for the authentication. The "powerShellCode"-key contains the final code which will be executed.

It is also useful to know that all other keys, which are created below "parameters", can be used as variables in the PowerShell script. As an example: If a new key named "UserLanguage" is added, the $UserLanguage variable can be used in PowerShell.

{
    "parameters":{
        "microsoftTenantID":"<yourtenantname>",
        "microsoftClientID":"<yourappregistrationclientid>",
        "microsoftClientSecret":"<yourappregistrationclientsecret>",
        "powerShellCode":"<yourpowershellcode>"
    }
}

Example Request

Here is a sample request that allows to fetch all user data from Microsoft Entra ID via Graph API.

POST https://<yourfunctionappname>.azurewebsites.net/api/<yourfunctionname>?code=<yourfunctionkey>

{
    "parameters":{
        "microsoftTenantID":"<yourtenantname>",
        "microsoftClientID":"<yourappregistrationclientid>",
        "microsoftClientSecret":"<yourappregistrationclientsecret>",
        "powerShellCode":"$Uri = \"https://graph.microsoft.com/v1.0/users\"\n$Result = Invoke-RestMethod -Uri $Uri -Body $requestBody -Headers @{Authorization = $Global:AzureADAccessToken; ConsistencyLevel = 'eventual' } -Method GET -ContentType 'application/json'\n$Members = $Result.value\nwhile ($Result.'@odata.nextLink') {\n    $Result = Invoke-RestMethod -Uri $Result.'@odata.nextLink' -Headers $Header\n    $Members += $Result.value\n}\nreturn $Members"
    }
}

Setup Function

For this Azure Function you will need an Azure Function App. It must be able to execute PowerShell code. In the Function App you have to create a Function with the HTTP triiger and FUNCTION Authorization level.

image.png

This function then acts as the container where the server-side code runs. The Azure Function framework then accepts the REST calls and does the load balancing and handling of the HTTP requests.

Server-side PowerShell Code

This code is used on the Function to handle the requests. It is important to know that this is specifically for handling the incoming JSON and responses.

using namespace System.Net

# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)

# Function for returning Values to Request
function Return-FunctionValue{
    param(
        [boolean] $Error,
        [string] $StatusCodeString,
        $OutputBody
    )
    switch -exact ($StatusCodeString.ToUpper()) {
        "OK" {
            $StatusCode = [System.Net.HttpStatusCode]::OK
        }
        "NOTFOUND" {
            $StatusCode = [System.Net.HttpStatusCode]::NotFound
        }
        "NOCONTENT" {
            $StatusCode = [System.Net.HttpStatusCode]::NoContent
        }
        "BADREQUEST" {
            $StatusCode = [System.Net.HttpStatusCode]::BadRequest
        }
        "INTERNALSERVERERROR" {
            $StatusCode = [System.Net.HttpStatusCode]::InternalServerError
        }
        default {
            $StatusCode = $null
        }
    }
    if($Error){
        $OutputBody = @{
            "statusCode" = $StatusCode
            "errorMessage" = $OutputBody
        }
    }
    Write-Output $StatusCode
    Write-Output $OutputBody
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
        StatusCode = $StatusCode
        Body = $OutputBody
    })
    Exit
}

try{
    $BodyParameters = [PSCustomObject]$Request.Body.parameters
    Foreach($BodyParameter in $Bodyparameters.PSObject.Properties) {
        New-Variable -Name $BodyParameter.Name -Value $BodyParameter.Value
    }
}
catch{
    Return-FunctionValue -StatusCodeString "InternalServerError" -OutputBody "Parameters provided could not be converted to variable" -Error $True
}
 
$powerShellCode = $powerShellCode.Replace('\n', "`n")

# Function for getting Azure AD Access Header
Function Build-MicrosoftEntraIDApplicationAccessHeader(){
    param(
        [Parameter(Mandatory=$true)]
        [string] $TenantID,
        [string] $ClientID,
        [string] $ClientSecret,
        [string] $refreshtoken
    )

    $authenticationurl = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token"

    if($refreshtoken -and $TenantID){
        $tokenBodySource = @{
            grant_type = "refresh_token"
            scope = "https://graph.microsoft.com/.default"
            refresh_token  = $refreshtoken
        }
    }
    elseif($TenantID -and $ClientID -and $ClientSecret){
        $tokenBodySource = @{
            grant_type = "client_credentials"
            scope = "https://graph.microsoft.com/.default"
            client_id  = $ClientID
            client_secret = "$ClientSecret"
        }
    }
    else{
        Return-FunctionValue -StatusCodeString "BadRequest" -OutputBody "Authentication failed: Not all parameters provided for authentication" -Error $True
    }

    while ([string]::IsNullOrEmpty($AuthResponse.access_token)) {
        $AuthResponse = try {
            Invoke-RestMethod -Method POST -Uri $authenticationurl -Body $tokenBodySource
        }
        catch {
            $ErrorAuthResponse = $_.ErrorDetails.Message | ConvertFrom-Json
            if ($ErrorAuthResponse.error -ne "authorization_pending") {
                Write-Output "error"
                Return-FunctionValue -StatusCodeString "BadRequest" -OutputBody "Authentication failed: Error while posting body source: $($ErrorAuthResponse.error)" -Error $True
            }
        }
    }

    if($AuthResponse.token_type -and $AuthResponse.access_token){
        $global:MicrosoftEntraIDAccessToken = "$($AuthResponse.token_type) $($AuthResponse.access_token)"
        $global:Header = @{
            "Authorization" = "$global:MicrosoftEntraIDAccessToken"
        }
        Write-Output "Authorization successful! Token saved in variable."
    }
    else{
        Return-FunctionValue -StatusCodeString "BadRequest" -OutputBody "Authentication failed: Not all parameters provided for authentication" -Error $True
    }
}


# Authorization Header with ClientId & ClientSecret
try{
    if(($microsoftTenantID) -and ($microsoftClientID) -and ($microsoftClientSecret)){
        Build-MicrosoftEntraIDApplicationAccessHeader -tenantid $microsoftTenantID -clientid $microsoftClientID -clientSecret $microsoftClientSecret
    }
    else{
        Return-FunctionValue -StatusCodeString "BadRequest" -OutputBody "Authentication failed: Not all parameters provided for authentication" -Error $True
    }
} catch{
    Return-FunctionValue -StatusCodeString "InternalServerError" -OutputBody "Bearer token could not be created with provided parameters" -Error $True
}

# Run PowerShell Code
try{
    if($PowerShellCode){
        $OutputBody = Invoke-Expression $PowerShellCode
        Return-FunctionValue -StatusCodeString "OK" -OutputBody $OutputBody -Error $False
    }
    else{
        Return-FunctionValue -StatusCodeString "BadRequest" -OutputBody "PowerShell Code failed: No PowerShell Code provided for runtime" -Error $True
    }
}
catch{
    Return-FunctionValue -StatusCodeString "InternalServerError" -OutputBody "PowerShell Code provided did not run correctly" -Error $True
}

# Return-FunctionValue -StatusCodeString "NOCONTENT" -OutputBody ""