Delegated Graph API calls in Azure Function!

Today we are from start to end going to learn how to create an Azure Function that you trigger by using an HTTP trigger. This HTTP trigger redirects you to a log-on page, and after successfully authenticating, the Azure Function can do a delegated Graph API call on your behalf. The Azure Function will also at the end output the information it got from Graph API as a HTML webpage.

First, you need to go to the Aure Portal and create a new Function App.

Fill in the information below. It needs to use PowerShell as a Runtime Stack.

After the Function App is created, open it.

Go to the Authentication menu, and click Add identity provider.

Choose the following settings.

Click Add permission if you need any other delegated permissions. For the demo in this blogpost, we only need User.Read.

Click Add.

Now open the App Registration of the Azure Function App, and Grant Admin consent under API permissions.

Open the Authentication menu of the App Registration, and click Add a Platform.

Click Web

Enter this address and click Save. (modify with your azure function app URL)
https://azurefunctiodemotest.azurewebsites.net/.auth/login/aad/callback

Go back to the Function App, and click Edit on the Microsoft identity provider.

Add https://graph.microsoft.com as an allowed token audience, and click save.

Thanks for this blogpost from Jeremy Brooks that pointed me in the correct direction on how to put together many of the steps below.

Go resource.azure.com, find your azure function, choose read/write, and click edit after finding authsettingsv2.

Add the following at the end of the config file:

,
"additionalLoginParams": [
"response_type=code id_token",
"https://graph.microsoft.com",
"(String)"
]

Click PUT.

Go back to the Function App

Click Create in Azure Portal. Select HTTP trigger, and click Create.

Open the Function.

Click Get Function Url, and copy the URL for later use.

Click Code+Test and paste the PowerShell script below, and click save.

using namespace System.Net
using namespace System.IO
# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)
# Write to the Azure Functions log stream.
Write-Host "PowerShell HTTP trigger function processed a request."
function Push-HttpBinding {
<#
.SYNOPSIS
Simplified Output for HTTP Trigger Responses with intelligent defaults
.DESCRIPTION
Push-OutputBinding takes a lot of syntax for a simple response. Since the examples use Response as an HTTP output, we can safely assume that as a default and standard practice, plus there is error checking to warn if this isn't accurate.
We can also assume the user wants to issue an "OK" response unless something otherwise was provided.
We can also "help" users by assuming string arrays and strings were meant as a single item, and detect if some text formats like XML/HTML/JSON were used and set the content type appropriately for easier consumption.
.EXAMPLE
Get-Variable | Push-HTTPBinding
Show all the variables in the current environment. Defaults to JSON output
.EXAMPLE
Get-ChildItem . | ConvertTo-HTML | Push-HTTPBinding
Shows the items in the current folder in HTML table format. Push-HTTPBinding detects XHTML and sets the type accordingly
.EXAMPLE
"<html>test</html>" | Push-HTTPBinding -ContentType "text/plain" -StatusCode Accepted -Header @{"X-MyCoolHeader"="its so cool"}
Outputs html text but overrides the content type to text/plain, statuscode to Accepted, and adds a custom header
#>
[CmdletBinding()]
param (
#The body of the message
[Parameter(Mandatory,ValueFromPipeline)]$Body,
#The output binding to send the HTTP response to. Default is Response
[String]$Name = 'Response',
#The response code for the message. Default is 200 OK
[HttpStatusCode]$StatusCode = 'OK',
#Specify the Content Type of the message. If this is specified, it will be assumed you want the raw object to be output
[string]$ContentType,
#Specify custom headers to include in the HTTP response.
[HashTable]$Headers
)
$Body = $input
#If a single object in a collection was provided, unwrap that object
if ($Body.count -eq 1) {$Body = $Body[0]}
#If a collection of strings was provided, consolidate them to a single string
if (-not $Body.Where{$PSItem -isnot [String]}) {$Body = $Body -join [Environment]::NewLine}
#region TypeDetection
#This really should be done in https://github.com/Azure/azure-functions-powershell-worker/blob/9f3179923deb5bb95702da1baaf1dab67fbcea7d/src/Utility/TypeExtensions.cs
#Passthru if it was any of the already handled types in TypeExtensions.cs
$typeIsDetected = $false
([double],[long],[int],[byte[]],[Stream]).foreach{if ($body -is $psitem) {$typeIsDetected=$true;break}}
function DetectStringType {
[CmdletBinding()]
param (
[string]$String,
[regex]$Regex,
$chars=1000
)
#Do a "magic numbers"-style file type detection. Designed to guess file types of large strings to maintain performance at the risk of a potential mismatch
$buffer = [Char[]]::New($chars)
([IO.StringReader]$String).Read($buffer,0,$chars) > $null
$headerToMatch = $buffer -join ''
if ($headerToMatch -match $Regex) {$true} else {$false}
}
if (-not $ContentType -and -not $typeIsDetected) {
#XHTML Hint Detection
#This is primarily assuming someone used ConvertTo-Html to create an HTML document and tried to send it out.
if (-not $typeIsDetected) {
if (DetectStringType -String ([String]$Body) -Regex ([Regex]::Escape('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">&#39;))) {
write-warning "Detected XHTML String, setting content type to application/html+xml"
$typeIsDetected = $true
$ContentType = 'application/xhtml+xml'
}
}
#HTML Hint Detection
if (-not $typeIsDetected) {
if (DetectStringType [String]$Body -regex '(?s)<html.*?>') {
write-warning "Detected HTML String, setting content type to text/html"
$typeIsDetected = $true
$ContentType = 'text/html'
}
}
#XML Hint Detection
#Try XML. This was tested against large XML and non-XML objects to ensure it both "failed fast and succeeded fast" and didn't try to process the entire file.
[switch]$isXml = $false
if ($Body -is [xml.xmlnode]) {
$isXml = $true
[String]$Body = $Body.OuterXml
} else {
#Very basic XML detection that looks for the first non-whitespace character to be a < then two sets of <> with a little extra criteria, this could certainly be a better regex maybe
$xmlDetectRegex = '(?s)^\s*?<\w+.+?>.*?<[\w\/]+.+?>.*?'
if (DetectStringType [String]$Body -regex $xmlDetectRegex) {
$isXml = $true
}
}
if ($isXml) {
write-warning "Detected XML String, setting content type to application/xml"
$typeIsDetected = $true
$ContentType = 'application/xml'
[String]$Body = [String]$Body
}
#JSON Hint Detection
if (-not $typeIsDetected) {
if ($Body -is [newtonsoft.json.linq.jtoken]) {
$isJson = $true
[String]$Body = [String]$Body
} else {
#Very basic XML detection that looks for the first non-whitespace character to be a < then two sets of <> with a little extra criteria, this could certainly be a better regex maybe
#Tested against a 180MB json string to make sure it "fails fast"
$jsonDetectRegex = '(?s)^\s*?[\{\[]+\s*?"\w.+?"\:'
if (DetectStringType [String]$Body -regex $jsonDetectRegex) {
$isJson = $true
[String]$Body = [String]$Body
}
}
if ($isJson) {
write-warning "Detected JSON String, setting content type to application/json"
$typeIsDetected = $true
$ContentType = 'application/json'
[String]$Body = [String]$Body
}
}
#For everything else set the type to application/json because downstream the parser will serialize any remaining objects to json
if (-not $typeIsDetected) {
write-warning "Did not detect any types, setting content type to application/json by default"
$ContentType = 'application/json'
}
}
Push-OutputBinding -Name $Name -Value ([HttpResponseContext]@{
Body = $Body
Headers = $Headers
ContentType = $ContentType
StatusCode = $StatusCode
})
}
#import-module Az.Accounts
# Write to the Azure Functions log stream.
Write-Host "PowerShell HTTP trigger function processed a request."
$accessToken = $Request.Headers["X-MS-TOKEN-AAD-ACCESS-TOKEN"]
$Header = @{"Authorization" = "Bearer $accessToken" }
$uri = "https://graph.microsoft.com/v1.0/me/&quot;
$me = Invoke-RestMethod -Uri $uri -Headers $Header -Method get
Write-Host $me.mail
$body = @"
<html>
<head>
<title>$($me.displayname)</title>
</head>
<body>
<div style="text-align: center;">
<h1>This is your email address: $($me.mail)</h1>
<h1>This is your AzureAD ObjectID: $($me.id)</h1>
<p>This is awesome!</p>
</div>
</body>
</html>
"@
$body | Push-HTTPBinding

Thanks to Justin Grote for the Push-HttpBinding PowerShell function to be able to push an HTML response.

Sometimes all the settings take some time to be applied, so it helps to go back to the Function App, and click restart.

Go back to the function, and click Monitor.

Open the Function URL you copied earlier in another browser profile, and you will be prompted to log on.

After you have entered the username/password, the function now has a delegated Graph API token. When the request is done, it now outputs some of the information from the Graph API call as an HTML page.

Here you can see the log of the run when we triggered the function.

The next steps could be looking at appending strings/variables at the end of the function URL to pass on to the PowerShell script in the Function.

Leave a comment