Calling Azure Function from PowerShell

Page content

This post is part of a series about Azure Functions and PowerShell. Check out the list of other posts in the series!

So far we have se up an Azure Function app and configured authentication and authorization using Azure Active Directory. Now everything works as long as we expect users to hit our function with a browser. I often use functions to build an API that is being called by code. In my case, the code is often a PowerShell script, but let’s call the code an application for the time being.

Registering a client application that calls an api

Since our application runs on a client computer in this scenario, we cannot rely on the identity of the application, because that would require the application to have some sort of secret, either a password or a certificate, and that secret would have to be embedded in code, which in turn can be extracted by the user of our application. As always, no secrets in code!

This also means that we cannot rely on Azure AD to send the token to a public endpoint specified in a RedirectURI, since our client most likely is not exposed to internet.

To solve this, there is something called a public client. Basically, all apps we register can be set up to act as clients if they are going to call another service. Regular apps used for web servers and API’s are called confidential clients since their code runs in a controlled environment where we securely can provide a secret (password or certificate). Desktop applications and script can not securely store a secret, and this makes them part of the category public clients.

In our case, we will need an application (or app registration) that is set up using the native client approach. A native client is just another word for an application like a desktop application or a script. Now we could add this configuration to our current application, but to keep things a bit more clean and separated, I’m going to cover how to create a separate client application.

Let’s look at an example app registration!

Just as before I’ll be running Azure CLI from PowerShell. We are going to need the application id (aka clientId) of our server application we created before. This time we’re setting the redirect uri to http://localhost.

With the id of our server application, we can get all oauth2Permissions from the application and use the permission user_impersonation to grant our client application permission to sign into our server application as the user.

Once the application is created we need to set the requestedAccessTokenVersion to 2 just as we did for the server application. This tells Azure AD that we want a version 2 token, which we need since we specified v2.0 in the end of our Issuer when setting up advanced mode authentication on our app service.

$ClientAppDisplayName = 'MySuperApiClient'
$ServerAppId = 'c491d3f8-0d15-4ea5-96fd-957601d579fa'
$RedirectUri = 'https://localhost'

# Get the server app
$ServerApp = az ad app show --id $ServerAppId | ConvertFrom-Json

# Get oAuthPermission for user_impersonation from server app
$oAuthPermissionId = az ad app show --id $ServerApp.AppId --query "oauth2Permissions[?value=='user_impersonation'].id" -o tsv

# Build part of a manifest for requiredResourceAccess
$requiredResourceAccess = @{
    resourceAppId  = $ServerApp.AppId
    resourceAccess = @(
        @{
            id   = $oAuthPermissionId
            type = 'Scope'
        }
    ) 
} | ConvertTo-Json -AsArray -Depth 4 -Compress | ConvertTo-Json
# Pipe to ConvertTo-Json twice to escape all quotes, or az cli will remove them when parsing

# Register client application
$ClientApp = az ad app create --display-name $ClientAppDisplayName --native-app --reply-urls $RedirectUri --required-resource-accesses $requiredResourceAccess | ConvertFrom-Json
# Create a service principal for the application
$null = az ad sp create --id $ClientApp.appId | ConvertFrom-Json

# Consent the application for all users
$null = az ad app permission grant --id $ClientApp.AppId --api $ServerApp.AppId

# Disable implicit flow, we don't need this for authcode or device code flows
$null = az ad app update --id $ClientApp.AppId --set oauth2AllowIdTokenImplicitFlow=false *>&1

# Set application to use V2 access tokens
$Body = @{
    api = @{
        requestedAccessTokenVersion = 2
    }
} | ConvertTo-Json -Compress | ConvertTo-Json
# Pipe to ConvertTo-Json twice to escape all quotes, or az cli will remove them when parsing
$null = az rest --method PATCH --uri "https://graph.microsoft.com/v1.0/applications/$($ClientApp.objectId)" --body $Body --headers "Content-Type=application/json"

# Output the ClientId for use later
Write-Output $ClientApp.AppId

Now we need to set our client application to require users being assigned a role just like we did our server application.

$ClientAppId = '479fe3c1-a9a4-4098-b343-db98e7c6e81b'
az ad sp update --id $ClientAppId --set appRoleAssignmentRequired=true

And again we can assign the default access role to users just like we did with the server app above, this time using our clent appId instead of the server.

$AppId = '479fe3c1-a9a4-4098-b343-db98e7c6e81b'
$UserPrincipalName = '[email protected]'

$App = az ad sp show --id $AppId | ConvertFrom-Json
$principalId = az ad user show --id $UserPrincipalName --query 'objectId' -o tsv

$Body = @{
    appRoleId = [Guid]::Empty.Guid
    principalId = $principalId
    resourceId = $App.objectId
} | ConvertTo-Json -Compress | ConvertTo-Json
az rest --method post --uri "https://graph.microsoft.com/v1.0/users/$UserPrincipalName/appRoleAssignments" --body $Body --headers "Content-Type=application/json"

And that is it! Now we have a server and a client application, we have set up them both to require users to be assigned a role. Getting access to the server application will allow users to browse to the server using a browser, granting access to the client application will allow a user to access the server using the client application.

Now we can of course combine these applications into one by simply adding a native client platform to our server application, which would probably be the better way of doing it if you are building an API using Azure Functions, but I chose to use two applications in my example to cover both sides, sometimes we only need to set up a server application, some times we only need to set up a client application, and sometimes the client applications needs permissions to several API’s.

Let’s quickly cover the last part of this quite lengthy post. How do I access my API using PowerShell?

Get OAuth tokens from AzureAD with PowerShell

There are lots of ways to obtain oauth tokens from Azure AD. This time I’m going to use the PowerShell module MSAL.PS. MSAL.PS is a wrapper around the MSAL client librart for dotnet and it has everything we need baked in, ready to just consume!

Start by installing the module.

Install-Module MSAL.PS

Now we need the clientID for the application we are requesting a token for, the tenant id for our tenant and a list of scopes. The redirect URI needs to match a redirect URI configured on our application.

$ClientAppId = '479fe3c1-a9a4-4098-b343-db98e7c6e81b'
$TenantId = "b5fbba89-c11c-4ba3-baf3-67fb6d5fb61f"
$Scopes = 'https://mysuperapi.azurewebsites.net/user_impersonation'
$RedirectUri = 'http://localhost'

Import-Module 'MSAL.PS' -ErrorAction 'Stop'
$PublicClient = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientAppId).WithRedirectUri($RedirectUri).Build()
$token = Get-MsalToken -PublicClientApplication $PublicClient -TenantId $TenantId -Scopes $Scopes

Invoke-RestMethod -Uri "https://mysuperapi.azurewebsites.net/api/mysuperfunc" -Headers @{Authorization = "Bearer $($token.AccessToken)" }