Authenticated access to Azure Functions using Azure AD

I’ve been working on something cool since yesterday afternoon. It’s taken up pretty much all my time since yesterday afternoon (all the way to night) and then today up to now. It’s not anything cool that I’ve created, but it’s something I wanted to try out for a while but only yesterday got into the “mood” for and then spent a lot of time trying to fit the pieces together to get it working.

So what do I want to do? I need an Azure Function for work that I’d like users to authenticate against using Azure AD. By default I can have Azure Functions authenticated against using various access keys. But I don’t want to use that as this is quite a sensitive Function (or it will be once I actually start working on it) so I’d not like to depend on static keys. What happens if someone leaves the firm – I’ll have to change keys and communicate to everyone. Or I’ll have to create  separate keys per person for instance – too much trouble.  I don’t know if there’s some limit to the number of keys too and that could be another stumbling block. Plus I’d have to maintain some store of keys and users if I want to work with them within the Function… I am too lazy for all that. 😵

In fact, I don’t actually need an Azure Function… but when I started thinking about authentication for this task I realized that Azure Functions is the only one that lets me have authentication via Azure AD. The other alternatives – Logic Apps and Automation Accounts – can be invoked by an HTTP request but have a fixed key. Azure Functions is more flexible in letting me have multiple keys, and it also supports Azure AD. Nice!

(Correction: My bad, turns out Logic Apps too can do Azure AD authentication.  Haven’t tried this out yet but it looks like you need to define the claims and only those requests that match are authorized).

In addition I want the Azure AD authentication to use the Device Code flow. I don’t want the users of this Function to authenticate against Azure AD with their username/ password (the ROPC flow) and since this Function is meant to be an API rather than a website I want something they can just do entirely via command line (or a PowerShell script for instance).

Anyways, here’s how one can enable Azure AD authentication against an Azure Function. My Function uses PowerShell so I have to do things slightly different as most instructions just assume you are using C# and I don’t know C# nor wanted to figure that out just for this piece.

Enable Authentication in the Azure Function

Under the Settings > Authentication you can “Add identity provider”.

Select “Microsoft” and follow along to create the App Registration. Here’s what I went with (mostly the default except changing to “HTTP 401”).

Initially I disabled the Token store. I got the impression it was just some place the tokens were stored and I could retrieve or refresh them, and I didn’t see a reason why I might need to do that.

Later I enabled the Token store. I’ll elaborate the reason for that later, but for now leave it enabled. Let’s go with the default permissions (on the next screen) for now. Will add to it later.

That’s all!

The App Registration

I’ll include screenshots of the App Registration that gets created so we know what is setup by default. This is for anyone who creates the App Registration manually (better to Google and I am sure there’s official Microsoft instructions on the same) but also coz I’ll be making some changes to the defaults.

First up, the “Authentication” setting of the App Registration.

Things to note:

The Redirect Url. This is the Function Url with “/.auth/login/aad/callback” tacked on to it. The “aad” bit would be different if I hadn’t used Azure AD and went with Google for instance. What this tells Azure AD is that if it is replying with the authentication tokens to an Url then only accept the above Url as a destination. I don’t really use that Url but it’s good to know what it is there for.

The flow of authentication when using Azure AD or similar is that when a user visits a Function app and is unauthenticated they are redirected to the “/.auth/login/<provider>” Url to login (where “<provider>” would have been “aad” in my case). Once the user authenticates there (i.e. with Azure AD in my case) they are redirected to “/.auth/login/aad/callback” and that’s where the tokens they get from the provider are consumed. This Url then returns its own token and that is what one sets in a header (X-ZUMO-AUTH) to actually authenticate against the Function.

Note that extra step: the authentication tokens from Azure AD cannot be used to directly authenticate with a Function. It has to be sent to a different Url and exchanged for a token the Function expects. All this and more are in the official docs if you are interested in more details.

Now, typically you’d be using an SDK and that’ll take care of all this for you. :) Or you’d have the HTTP 302 redirect turned on and that’ll take you to the “/.auth/login/<provider>” – but remember I turned that off above (as I am going to be using this as an API and so redirecting doesn’t make sense). Thus I have to do all these manual steps myself.

Anyways, so that’s the reason for the Reply Url in the App Registration and why it doesn’t matter much to me.

The “Implicit grant and hybrid flows” section doesn’t matter as we are not using that. (In an Implicit flow you get tokens from an /authorize endpoint and these can be Access Tokens or ID Tokens or both; that’s what this setting decides but we don’t care as we are not using this Flow).

Further down on the same page we have “Public flows“:

Toggle this to “Yes” as we will be using the Device Code flow. (Don’t forget to click “Save” after doing this!)

Next up is the API permissions, which has the sole permission we granted earlier:

Go ahead and add the “profile” permission here. This is a delegated permission. Graph. OpenId permissions.

And do an “Admin Consent” so both permissions are consented to. If not users will get an “Approval Required” message when trying to authenticate.

The “profile” permission is required because without it the claims from Azure AD don’t contain any user info. Here’s what I get in my ID token without “profile”:

While this is fine if all I care about is only allowing authenticated access to the Function, sometimes one would also like to know who it is that’s accessing the Function. That is to say, maybe I’d like to know from within the Function that it is user “xyz” who is accessing it so maybe I can treat them differently or make a log entry of it somewhere…

To get that info I have to enabled the “profile” permission. Then I get claims like these:

Better, right? I get the username (UPN) and also the id (oid) which I can use to lookup the user in Graph or Azure AD. I also get the Name (name)… wunderbar!

There’s also some stuff in the “Expose an API” section… there’s nothing to change there.

Great, that’s that!

Azure Function

Let’s create an Azure Function with the HTTP trigger. Be sure to set its Authorization to be “Anonymous”. I don’t want the Function to require authorization as that will be handled by the overall Function App.

Can I access it now? Nope.

Cool, that’s as expected.

Authenticating via Device Code flow

Ok so now let’s try and get authenticated.

As I said earlier I want to use the Device Code flow. This requires sending a POST request to a specific Url, getting a code, signing in to a browser using that code (and authenticating yourself), then putting the return code in another Url to get the ID Token. Standard stuff basically.

I’ll be putting some code below so let me define a few variables I’ll be using as we go along:

Do go through the Device Code flow document as everything will make much more sense from there. That’s a great document!

First step is to kick off the process via the /devicecode endpoint. This involves sending a POST request to that endpoint with the Tenant ID, the App Registration ID, and the Scopes we are interested in. I am interested in User.Read, Profile, and openid. I mentioned the first two earlier – User.Read is to sign the user in and read their profile; Profile is required so the ID Tokens contain the user details like oid etc. And openid is needed so we get an ID token in the first place!

Initially I had missed requesting for openid and profile with the result that I was only getting an access_token in my response. It was only after spending some time that I realized of course ID Tokens need the openid scope and while I didn’t have to grant that in the App Registration I do have to include that in the scope to get an ID Token! (I had spent some time a few months ago reading up on all this… and meant to write a blog post explaining it all to myself, but never got around to it).

Anyways, to recap: send a POST to the above endpoint. Ask for the 3 scopes.

The $response variable contains the following:

So as part of my script I output the message (looks like To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code DH5XSAKDS to authenticate.) and just to make it easy I copy the code to the clipboard. :)

After this one has to POST to the /token endpoint and if the user has authenticated we’ll get the ID Token (or not if you forgot the scopes as above! 😃), while if the user has not authenticated yet we’ll get a reponse to wait.

I thus have the following code:

I am pretty pleased I spent time writing this. It will sensibly wait while I login to the Url in my browser, and once I do that it will get the ID token from the endpoint. I like how I used the try catch block to do a switch of the Error Message. It’s the little things… 😊

When this exits $response2 has the ID Token.

Just to emphasize: the $response2 variable has both ID Token and Access Tokens but it is the former we want. If that is missing check your scopes. Both look similar when you decode the contents but Access Tokens won’t help here. In fact the documentation is not very clear on that point either and this is yet another area where I spent many hours figuring out why things weren’t working. Initially I had forgotten to add the openid scope and was getting only Access Tokens and I was trying to proceed with that and failing.

(Update: Access Tokens won’t help because they have an audience (aud claim) for Graph. Only ID Tokens have the audience sent to the App Registration ID).

Authenticating against the Function middleware

As mentioned earlier the authentication flow of Functions with Azure AD is to send the tokens to the “/.auth/login/aad” endpoint. This is part of the Function middleware and is independent of the acutal Function code.

But it doesn’t clarify that it is looking for the ID Token. In fact, a follow on link from that document has the following:

The required property in the body is called “access_token” for goodness sakes!! But it actually needs the ID Token. If you try to pass the Access Token instead it gives an error:

Invoke-RestMethod: You do not have permission to view this directory or page.

On the other hand this works:

Another thing which always bites me in the a$$ with Invoke-RestMethod is that it correctly converts a hash table sent as header to JSON but doesn’t do the same for the body. So if I had the following code instead it would fail:

Invoke-RestMethod: Bad Request

I’ve to thank Postman for making me realize this. When running out of ideas and Google results I turned to Postman to make the same query and realized that I’d get the same error if I didn’t send it as JSON… which got me thinking that even though my content type was “application/json” it’s possible the $authBody3 hash table wasn’t being converted to JSON by Invoke-RestMethod. Adding a ConvertTo-JSON fixed that.

Anyways, if you send the ID Token over correctly, the “/.auth/login/aad” endpoint returns a correct response. So all one needs to do is put that in the appropriate header when calling the Function. Which is what my code snippet above does.

The Script

Since the gave the script in bits and pieces above here’s the full version:

I feel so Web 1.0 putting a script in a blog post. The cool kids put it as a Gists in GitHub nowdays… I know. 🙂

Getting User Details

We are not done yet. With the above I am now able to restrict my Function to only Azure AD authorized users. I can limit this to specific groups or users by going to the “Enterprise Application” of the App Registration and locking it down there (this is standard Azure AD stuff so I am not going into details).

Within the Function though how do I get the details of the authenticated user? According to the official docs I am supposed to see these as headers. Or if I was using C# there’s a ClaimsPrincipal class I can use. For PowerShell (and others) sadly headers are my only option.

What headers do I get? I can see these in the $Request.Headers variable within a Function as these are bound to the Request object. Here’s what I get in my Function:

Hmm, not much. I am supposed to get these two headers according to the docs:

  • X-MS-CLIENT-PRINCIPAL-NAME
  • X-MS-CLIENT-PRINCIPAL-ID

I see the ID but that has no relation to my actual User Object GUID or anything in Azure AD. It seems to be a transient one as it keeps changing. I was hoping the NAME one would at least give a UserName or something but it is nowhere to be found.

Two headers are worth noting: X-MS-CLIENT-PRINCIPAL and X-MS-TOKEN-AAD-ACCESS-TOKEN. The former has the token I got from the “/.auth/login/aad” endpoint. It is a JWT and doesn’t have anything useful (to me at least haha):

Note that the “sub” claim matches X-MS-CLIENT-PRINCIPAL-ID. That’s the only interesting thing I could see.

The X-MS-TOKEN-AAD-ACCESS-TOKEN header, on the other hand, has my ID Token. But the important point to note about this is that it only appears if the Token Store is enabled (hence I enabled it earlier when setting up Authentication). As per the docs once Token Store is enabled we should see these headers:

I don’t see the other three but at least I have the important one. Why important? Because the ID Token is the one with the oid and other claims as we noticed above. Technically one is not supposed to examine an ID Token coz even though it is currently JWT it could change anytime… but what the heck, it is something to go by. (Correction: ID Tokens are always JWT; I was confusing with Access Tokens which needn’t be JWT).

The JWT has three parts – a header, body, and signature – all separated by dots. Each of these is in Base 64. So if I split the JWT along dots, take the second bit, convert the Base 64 to get JSON, then convert this JSON to a Hash Table I should be able to extract the claims. So let’s do that in the function to output my name as I authenticate:

I added this to the Function. The default PowerShell script in an Azure Function has the following block so this will pick up the $name variable and ideally print it to the requestor:

And sure enough it does!

That’s cool, huh! 😎

This is why I am pretty pleased with myself. All these pieces are available but nothing worked out of the box and I had to stick with it to get it working. And the end product is cool coz now I can have Azure Functions authenticated against Azure AD and I can also identify the authenticated user’s name, email/ UPN, and Object ID. Now I can build on top of this as needed.