{
"$type": "site.standard.document",
"canonicalUrl": "https://johnnyreilly.com/posts/azure-ad-claims-static-web-apps-azure-functions",
"description": "Authorization with Azure Static Web Apps linked to Azure Functions has an issue. Azure AD app role claims are not supplied; this post will demo a workaround.",
"path": "/posts/azure-ad-claims-static-web-apps-azure-functions",
"publishedAt": "2022-11-17T00:00:00.000Z",
"site": "at://did:plc:yy3apqjlms24kso7ahn7lbmb/site.standard.publication/3mova7c4nho2b",
"tags": [
"auth",
"azure functions",
"azure static web apps",
"azure",
"microsoft graph"
],
"textContent": "Authorization in Azure Functions is impaired by an issue with Azure Static Web Apps linked to Azure Functions. Azure AD app role claims are not supplied to Azure Functions. This post will demonstrate a workaround.\n\n\n\nUpdated 28th November 2022\n\nAfter I posted this, Thomas Gauvin (Product manager for Static Web Apps) was kind enough to tweet this:\n\n[](https://twitter.com/thomasgauvin/status/1596242773686079496)\n\nSo by the sounds of it, this blog post will not be required in the longer term, as support should to be added directly. Tremendous news!\n\nRelated posts\n\n- Graph API: getting users Active Directory group names and ids with the CSDK\n- Microsoft Graph client: how to filter by endswith\n\nWhere's my claims?\n\nThere is a limitation that affects authorization when you have a linked backend paired with an Azure Static Web App. Let's take the case of having an Azure Function App as the linked backend. Essentially the Azure Function app _does not_ receive the claims that the Static Web App receives. There's an issue tracking this on GitHub, and it seems that this is a general problem with Static Web Apps, Azure AD and linked backends.\n\nWe have a Static Web App, with an associated CFunction App (using the Bring Your Own Functions AKA \"linked backend\" approach). Both the Static Web App and Function App are associated with the same Azure AD App Registration.\n\nWhen we're authenticated with Azure AD and go to the auth endpoint in our Static Web App: /.auth/me we see:\n\nNote the claims in there. These include custom claims that we've configured against our Azure AD App Registration such as roles with OurApp.Read.\n\nSo we can access claims successfully in the Static Web App (the front end). However, the associated Function App does not have access to the claims.\n\nIt's possible to see this by implementing a function in our Azure Function App which surfaces roles:\n\nWhen this /api/GetRoles endpoint is accessed we see this:\n\nAt first look, this seems great; we have claims! But when we look again we realise that we have far less claims than we might have hoped for. Crucially, our custom claims / app roles like OurApp.Read are missing.\n\nMaybe they're hiding in x-ms-client-principal?\n\nIf we look directly at the x-ms-client-principal header, maybe we'll find what we need?\n\nAlas not. We have the user's email and some simple roles (\"authenticated\" and \"anonymous\"), but no sign of our custom claims / app roles:\n\nThis is the problem: we want our Azure Function App to be able to make use of the same custom claims / app roles that we use for authorization in the Static Web App. How can we achieve this?\n\nMicrosoft Graph API\n\nThe answer lies with the Microsoft Graph API. We can interrogate it to get the app role assignments for the user. This will give us the same information that we have in the Static Web App. (Well to be strictly accurate, it will be a slightly different set of claims. But what matters is it will be the app role assignment claims that we want to use for authorization.)\n\nWe already have an Azure AD app registration. In order that we can interrogate the Microsoft Graph API, we'll need the following permissions:\n\n- User.Read - to sign in\n- User.Read.All - for acquiring the app role assignments against a user\n- Application.Read.All - to get more information about the app role assignments - allowing us to translate the app role assignments into the claims that we want to use for authorization\n\nOf the above permissions, it's likely that you'll already have delegated User.Read in place; the other two you might need to add and ensure they're granted in Azure.\n\nInterrogating the Microsoft Graph API\n\nNow we have an Azure AD App Registration with sufficient permissions, we'll need a GraphClient to interrogate the Microsoft Graph API. To get that we're going to build an AuthenticatedGraphClientFactory:\n\nWhen we execute GetAuthenticatedGraphClientAndClientId we'll get back a GraphServiceClient that we can use to interrogate the Microsoft Graph API. We'll also get back the client ID of the Graph API App. We'll need this later. Note that the AuthenticatedGraphClientFactory requires the client ID, client secret and tenant ID of the Azure AD App Registration.\n\nNow we have the ability to interrogate the Microsoft Graph API, we can write a PrincipalService.cs class that will interrogate it and return the app role assignments for the user:\n\nQuite a lot of code! Let's walk through what it does:\n\n1. It takes the x-ms-client-principal header and deserializes it into a MsClientPrincipal object - this is the cut down version of the ClaimsPrincipal object that we saw earlier:\n\n2. It creates a new ClaimsIdentity using that information, but stripping out the anonymous role as it's superfluous.\n\n3. Using the userDetails (email address) from the MsClientPrincipal object, it gets the app role assignments for that user from the Graph API. (We needed User.Read.All to do this.)\n\n4. In a perfect world, we'd be able to use the AppRoleAssignments property on the User object to get the app role assignments for a user, but unfortunately that doesn't come with the human readable name you'd hope for; the MyApp.Read. So we have to interrogate the Graph API once more and use the Application that represents our Azure AD App Registration (we acquire this by filtering for an appId matching our clientId). Then we can get the human readable / MyApp.Read role assignment.\n\n5. It adds the app role assignments as role claims to the ClaimsIdentity object.\n\n6. It returns the ClaimsIdentity object wrapped in a ClaimsPrincipal object.\n\nUsing the PrincipalService\n\nIn order that we can make use of our PrincipalService we need to configure it and the AuthenticatedGraphClientFactory in our Startup class:\n\nWith that in place, we can now use the IPrincipalService in a function:\n\nThe above class has 2 functions:\n\n- GetPrincipal - returns the ClaimsPrincipal object as JSON\n- AmIInRole - takes a role query parameter, tests if a user has that role and returns a 403 if they don't and a 200 with a welcome message if they do\n\nGetPrincipal - what claims do we have?\n\nLet's try out the GetPrincipal function, when I go to the /api/get-principal endpoint I see this:\n\nThis isn't the _same_ information as the Static Web Apps principal, but it's close enough for our purposes. Crucially, we can see the AppRoleAssignment OurApp.Read that we assigned to our user in the Azure Portal. That is the key information that we need, and that we are missing by default.\n\nCrucially this is enough information for us to be able to apply authorization to our functions.\n\nAmIInRole - test IsInRole functionality\n\nWe can demonstrate applying authorization by using the AmIInRole function. This internally uses the inbuilt IsInRole functionality of the ClaimsPrincipal object, and returns an appropriate API result accordingly.\n\nIf I go to the /api/am-i-in-role?role=OurApp.Read endpoint I get a 200 status code and the message: Welcome johnny_reilly@hotmail.com - you have role OurApp.Read!. This makes sense, my user account has the OurApp.Read role.\n\nLet's test that we also deny access appropriately. There is an OurApp.Write role; my account does not have this. If I go to the /api/am-i-in-role?role=OurApp.Write endpoint I get a 403 status code and the message: Forbidden for OurApp.Write.\n\nIt works!\n\nConclusion\n\nWe've demonstrated a way to acquire a ClaimsPrincipal object that contains the AppRoleAssignments for a user. This is enough information for us to be able to apply authorization to our functions.\n\nIt would be ideal if this wasn't required, and I'm hoping that the Static Web Apps team will be able to provide a solution for this in the future. Keep an eye on this GitHub issue. In the meantime, this is a workable solution.\n\nThanks to Warren Joubert for his help with this post.",
"title": "Azure AD Claims with Static Web Apps and Azure Functions"
}