Azure Function App AuthN & AuthZ
I've spent the past few weeks working out how to do AAD auth to a Function App.
That's right; 3 weeks.
I search so much... and Bing... Bing is the most worthless search engine. I doesn't mark pages I've visited as visited. Sure... on that first search result it's all "yep, you clicked me". I search again... "What... this? Noooo.... You've never clicked it."
Fuck You, Bing.
More of my time was wasted due to that search engines idiotic link generation so I kept hitting the SAME DAMN PAGE... GAAAAAH...
Right... onto what you're here for... HI FUTURE ME!!! Or... stranger... sure... Hi.
OK... What is this about.
Function App Authentication and Authorization
My needs are to allow only authenticated Service Principals to my function app so I can use that identity to check "The List" and see what they are authorized to do. Or if I give them a giant 403.
Azure AAD with the MSI should make this super fucking simple.
Sure... It does. IF YOU KNOW ALL THE MAGIC!!!
Also helps if you don't get it working with USER accounts and then take a break and then realize that OH SHIT - You can't add SPNs like you do users... YOU CAN'T ADD THEM AT ALLLLLLLL!!!!!!!!!!!!!!!!
You may get that I'm REALLY FRUSTRATED with how this all went down.
It shouldn't take me 3 weeks to do what I knew had to be possible. Just... GAHHHHHH!!!
I'm going to make this a very very ... VERY wordy bullet point list of things to do for the various things I did.
Setup Step 0: Function App
Give yourself a function app. Follow other guides.
Setup Step 1: Give Your App An Identity
Follow Option 1 from this [link(https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-provider-aad?tabs=workforce-tenant#--option-1-create-a-new-app-registration-automatically) as it's the simplest way to enable the authentication in your app.
If you're reading this; you probably need to do step 4. But there's no great guides for the details. It's pretty straight forward; but depending on exactly what you want... maybe not.
In my use case; I don't want unauthenticated users. They MUST be authenticated so I have the callers identity.
My App Service Authentication Settings (step 4) look like this
The reason to require auth is that the host provides information about the caller in headers that it will not let anyone else set. See this article for more information.
The one I use is X-MS-CLIENT-PRINCIPAL-ID
so I can use the OID and check against stored permissions.
I could use claims; or roles; or a number of other, probably better, things. But I'm storing it in CosmosDB. DEAL!
Setup Step 2: What am I forgetting?
I don't think there's another set up stepup step. Though I'm sure I'm forgetting something.
I had a few version "working" that weren't what I needed... so I might have forgotten something. Some useful steps are
* In the Enterprise Application that gets generated for your function app (should be same name) or whatever one you selected... it's clickable from the "Authenitcation" blade; go to the Properties and set Assignment Required to 'Yes'
This is useful when you are having USERS call and you want to bypass the user auth flow. It requires you to go to the "Users and Groups" blade and add users there. Extra steps, but ... if you're thinking you can do that with just a few SPNs... would be no problem... but they aren't users... so... PROBLEM.
THE CODE
I think one of the biggest flaws is in getting the code to work. Mostly because there isn't anything very clear.
This guide for a daemon app is actually pretty good. I can see what they're going for after I got it working entirely different. Maybe it'll help you?
There's a few versions of the code that I had working. I'm just gonna go with the snippet being used to auth with our SPN from our build agents.
ClientSecretCredential credentials = new(input.TenantId, input.ClientId, input.ClientSecret);
AccessToken accessToken = await credentials.GetTokenAsync(new TokenRequestContext(new[] { input.FunctionAppResourceId + "/.default" }));
I'm using a DTO to have access to the appropriate values. The bit that took me the longest is that FunctionAppResourceId
WHAT THE FUCK IS THAT?!
It's the ApplicationId (or ClientId) of the Enterprise Application that your function app has configured as it's authorization source. The thing from "Setup Step 1".
I don't have a good dynamic way to get that value. I'm sure there is a great once... I don't have it for C# code, so I'll leave it up to the Azure CLI (which does have it) and then it gets passed into this console app to make some calls.
This is also retrievable from the ServicePrincipal page
That would have been REALLY nice for me to know early on.
The accessToken
in the code snippet has a value, Token
that gets put into a Bearer Auth header.
equestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token);
I'll be pasting in some full code snippets, no worries.
Then you make the call to your azure function. The system will auto handle validation of the token and provide the headers. These headers are; once again; only provided by the system if the caller is authorized.
I'll try faking the headers soon; just to see what it does; haven't tried that yet.
Simple
That's it. You are now ready to call via a SPN using the ClientId and ClientSecret.
I went through so many rabbit holes... gah.... frustrations.
What about Function App to Function App?
Yeah... what about that? If your function app has an MSI... it can identity itself to another function app... I mean... It SHOULD be able to...
This was the battle of week #3. The result of which is this snippet
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req)
{
HttpClient client = new HttpClient();
ManagedIdentityCredential credentials = new ManagedIdentityCredential();
AccessToken accessToken = await credentials.GetTokenAsync(new TokenRequestContext(new[] { "<ENT-APP-ID>" }));
using HttpRequestMessage requestMessage = new(HttpMethod.Get, "https://YOUR-APP.azurewebsites.net/api/YOUR-FUNCTION");
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token);
HttpResponseMessage echoResponse = await client.SendAsync(requestMessage);
return req.CreateResponse(HttpStatus.OK);
}
Note: Don't use HttpClient
like this.
The <ENT-APP-ID>
is the same one used in the earlier examples; the Application Id
. Took me days to figure that one out.
Then you call. That's it. Since the function app is an MSI; you can just use the explicit ManagedIdentityCredentials
class and retrieve the header and then make the call. Same simple flow.
I have some azure CLI that does a bunch of the configuration for me. These aren't my work notes, they are my research notes while I did things in powershell
# https://learn.microsoft.com/en-us/cli/azure/ad/app?view=azure-cli-latest#az-ad-app-list
az ad app list --display-name $aadAppName
# https://learn.microsoft.com/en-us/cli/azure/ad/app?view=azure-cli-latest#az-ad-app-create
az ad app create --display-name $aadAppName --sign-in-audience AzureADMyOrg --end-date "2222-12-31"
$raw = az ad app list --display-name "aadAppName
$json = $raw | ConvertFrom-Json
$appId = $json[0].appId
az ad sp create --id $appId
# https://learn.microsoft.com/en-us/cli/azure/ad/app/credential?view=azure-cli-latest#az-ad-app-credential-reset
$secret = az ad app credential reset --id $appId --years 10 --append --display-name "FromAzureCli" | ConvertFrom-Json
$secretValue = $secret.password
//set secret KEYVAULT - MICROSOFT_PROVIDER_AUTHENTICATION_SECRET & MICROSOFT_PROVIDER_AUTHENTICATION_SECRET_STAGING
//repeat for slot
# https://learn.microsoft.com/en-us/cli/azure/keyvault/secret?view=azure-cli-latest#az-keyvault-secret-set
az keyvault secret set --vault-name $keyVaultName --name MICROSOFT_PROVIDER_AUTHENTICATION_SECRET --value $secretValue
az keyvault secret set --vault-name $keyVaultName --name MICROSOFT_PROVIDER_AUTHENTICATION_SECRET_SLOT --value $secretValueSlot
# https://learn.microsoft.com/en-us/cli/azure/functionapp/config/appsettings?view=azure-cli-latest#az-functionapp-config-appsettings-set
az functionapp config appsettings set --resource-group $resourceGroupName --name $functionAppName --settings MICROSOFT_PROVIDER_AUTHENTICATION_SECRET=@Microsoft.KeyVault(SecretUri=https://$keyVaultName.vault.azure.net/secrets/MICROSOFT_PROVIDER_AUTHENTICATION_SECRET/)
# https://learn.microsoft.com/en-us/cli/azure/webapp/auth?view=azure-cli-latest#az-webapp-auth-update
az webapp auth update --resource-group $resourceGroupName --name $functionAppName --enabled true --unauthenticated-client-action Return401
# https://learn.microsoft.com/en-us/cli/azure/webapp/auth/microsoft?view=azure-cli-latest#az-webapp-auth-microsoft-update
az webapp auth microsoft update --resource-group $resourceGroupName --name $functionAppName --client-secret-setting-name MICROSOFT_PROVIDER_AUTHENTICATION_SECRET --client-id $appId --subscription $subscriptionId --issuer https://sts.windows.net/<tenant-id>/ --yes
These may not be 100% exact for what is needed; I had 3 weeks of iterations and multiple false positives.
Hopefully this can help you. I'm sure there's some better ways... it works. I'll improve it over time.