Azure Functions and Azure AD authorization
This post is part of a series about Azure Functions and PowerShell. Check out the list of other posts in the series!
In the last post on Azure Functions and Azure AD authentication we looked at how to require authentication for our Function App. Now we know who accessess our function, it’s time to have a look at Authorization, meaning now that we know who they are, should we let them in?
Restrict access to selected users
As the app is currently set up, whether we used express or advanced mode, any registered application, user or guest user of our tenant can currently sign in to our app. So far we have only configured the Authentication part. Now let’s have a look at some basic Authorization.
To further secure our Function App we won’t only require a user to authenticate, we’ll also require that user to be authorized, or in simple terms, we only want to admit selected users.
The first step when configuring authorization is simply to restrict access to a selected group of users. Azure AD has built in support for this and we only need to turn it on by configuring our application.
This can be done in the portal by going to Enterprise Applications in Azure AD, locate your application, go to properties and set the property “User assignment required?” to “Yes”.
It can also be done with Azure CLI:
$ServerAppId = 'c491d3f8-0d15-4ea5-96fd-957601d579fa'
az ad sp update --id $ServerAppId --set appRoleAssignmentRequired=true
Trying to browse my app should now give me the message “You do not have permission to view this directory or page.”
To get access again, add each user to the application. We can do this in the Azure Portal by going to Azure Active Directory -> Enterprise Applications, find our application and under “Users and Groups” add all the users that should be allowed to access our application.
We can also do this using the Microsoft Graph API. Again in this example we’re using Azure CLI to call Graph from PowerShell.
First we save the application id (clientId) of our application and the username of our user in variables. Then we use az ad sp show
to get our service principal (called Enterprise Application in the portal). We need to send three things to Microsoft Graph, the resourceId (objectID of service principal), the principalId (the objectId of our user) and the id of the role we want to assign.
Hang on, role? We haven’t created any roles?
No we haven’t created any roles, and if our service principal / Enterprice Application doesn’t have any roles, it has something called “Default Access” in the portal, we can reference this by using an empty guid ("00000000-0000-0000-0000-000000000000"
).
As a last note, we are once again piping our body to ConvertTo-Json twice to escape all quotes (") with backslash (\).
Enough talking, here is the code:
$AppId = 'c491d3f8-0d15-4ea5-96fd-957601d579fa'
$UserPrincipalName = 'simon@simonw.se'
$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"
Now that’s handy! But what if I want to create custom roles? Guess what? You can!
Authorization using custom roles
Let’s create some custom application roles! Why? These roles will be available in the access token and can be used in our function code to determine if a request is allowed or not.
If you don’t need any roles, you can skip this part and continue using the role “Default Access” which is represented of an empty GUID 00000000-0000-0000-0000-000000000000
as above. If you create any custom roles, the “Default Access” role will no longer be available. Trying to assign DefaultAcccess to an application with roles defined will result in an error message stating: Permission being assigned was not found on application
Time to create a custom role! We need to specify which kind of objects that can have the role, give the role a description, display name, id and a value. The value is the role name and the id has to be a unique GUID. I’m using [guid]::NewGuid().Guid
which will generate a new GUID.
Here is an example:
$AppId = 'c491d3f8-0d15-4ea5-96fd-957601d579fa'
# Add app role to an app
$AppRole = @{
allowedMemberTypes = @('User')
description = 'Example of custom role'
displayName = 'CustomRole'
id = [guid]::NewGuid().Guid
isEnabled = $true
value = 'Custom'
} | ConvertTo-Json -Compress | ConvertTo-Json
# Update enterprise application
az ad sp update --id $AppId --add appRoles $AppRole
How an AppRole is defined is documented here:
https://docs.microsoft.com/graph/api/resources/approle?view=graph-rest-1.0
Once we have a role set up we can assign it to a user. This is done in a similar manner as above. The only new thing is that we now look up the app role id in $App.appRoles
instead of using an empty GUID.
$AppId = 'c491d3f8-0d15-4ea5-96fd-957601d579fa'
$AppRoleValue = 'Custom'
$UserPrincipalName = 'simon@simonw.se'
$App = az ad sp show --id $AppId | ConvertFrom-Json
$principalId = az ad user show --id $UserPrincipalName --query 'objectId' -o tsv
$Body = @{
appRoleId = $App.appRoles.where{$_.Value -eq $AppRoleValue}.id
principalId = $principalId
resourceId = $App.objectId
} | ConvertTo-Json -Compress | ConvertTo-Json
az rest --method post --uri "https://graph.microsoft.com/beta/users/$UserPrincipalName/appRoleAssignments" --body $Body --headers "Content-Type=application/json"
For more information on how to protect a Web API, look at the scenarios Protected web API and A web API that calls web APIs.
Validating roles in your code
Using the easy auth feature of App Services, the token validation is done for us and we don’t need to handle much of it, but we need still need to decode the id token and extract the roles part if we want to validate separate roles.
This is not at all as hard as it seems. We are getting served the id token in a header called x-ms-token-aad-id-token. Easy auth makes sure that this header can’t be supplied by the user and only by easy auth so we can trust the header.
To decode a JWT token I’m using the function below.
function ConvertFrom-JwtToken {
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[string]
$Token
)
process {
$DecodedToken = $Token.Replace('-', '+').Replace('_', '/')
$Header,$Payload,$Signature = $DecodedToken.Split('.') | ForEach-Object -Process {
$String = $_
switch($String.Length % 4) {
0 {$String;break}
1 {throw 'Invalid token'}
2 {"$String==";break}
3 {"$String=";break}
}
}
[pscustomobject][ordered]@{
Header = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Header)) | ConvertFrom-Json
Payload = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload)) | ConvertFrom-Json
Signature = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Signature))
}
}
}
We can access headers in Azure Functions on the Request object, here is an example of accessing the id-token, decoding it and printing the roles of a user:
$IDToken = $Request.Headers.'x-ms-token-aad-id-token' | ConvertFrom-JwtToken
$Roles = $IDToken.Payload.Roles
Write-Output $Roles