Securing an Azure Functions App with EasyAuth and AD B2C
Azure Functions comes with three levels of authorization. Anonymous
means anyone can call your function, Function
means only someone with the function key can call it, and Admin
means only someone with the admin key can call it.
What this means is that to secure our Azure functions we must pre-share the secret key with the client. Once they have that key they can call the function, passing it as a HTTP header or query parameter. But this approach is not great for serverless single page apps. We don’t want entrust the secret to a client app where it might get compromised, and we don’t have an easy way of getting it to the client app anyway.
Instead, it would be better if the users of our single page application could log in to an authorization server, and for that to issue us with access tokens (preferably JSON web tokens) that we can pass to our functions. They can then check that the access token is valid, and proceed to accept or deny the function request. And this is of course the way OAuth 2.0 works.
However, at the moment there isn’t an easy way to enable verification of access tokens in Azure Functions. You could make your function anonymous and then write the verification yourself, but it’s generally not a good idea to implement your own security.
I wanted to see if there was an easier way.
And it turns out that there is a feature in App Service called “Easy Auth”. Azure Function apps support this by virtue of being built on top of App Service.
Easy Auth is an on-off switch. If you turn it on for your App Service, then every incoming HTTP request must be authorized. If you’re not authorized you’ll get redirected to log in at the authorization server.
Easy Auth supports several identity providers, including Facebook, Google, Twitter, Microsoft and Azure Active Directory. You can only pick one though (however if the one you pick is Azure AD B2C, then that can support additional social identity providers).
The downside of using EasyAuth is that your whole site requires login. You can’t have any pages that can be viewed without needing to provide credentials.
But the benefit of this approach is that it provides a relatively simple way to get things secured. And if we combine it with Azure AD B2C, we can allow users to self sign-up for our application and support things like password resets, two factor auth, email verification and so on. This is a great “serverless” approach to authentication, delegating to a third party service and keeping our own applic
So I set myself the challenge of integrating a simple SPA that calls through to an Azure Functions back-end with AD B2C. I can’t promise this is the only or best way to do this, but here’s the steps I took to get it working.
Step 1 – Create an Azure AD B2C Tenant
First of all you’ll need to create an Azure AD B2C tenant. This can be done through the portal, and detailed instructions are available here so I won’t repeat them here. You’ll need to make sure you associate it with a subscription.
Step 2 – Create a Sign Up Or Sign In Policy
Next we need a sign-up or sign-in policy. This allows people to sign in, but also to self register for your application. If you want to control users yourself then you’d just need a sign-in policy. You can create one of these policies in the portal in the settings for your AD B2C tenant.
The policy lets us select one or more identity provider. Generally you’ll want to enable basic email and password logins, but you can also add Facebook, Twitter, Google etc, so this is a great option if you want to support multiple social logins. (learn how to configure them here).
You can specify “sign-up” attributes which is what information you require from a new user signing up. That might just be their full name and country, but could also include some custom attributes that you define.
You can choose which claims will be included within the access tokens (JWTs), which can make life easier for your application getting hold of useful user info such as their display name without needing to look it up separately.
You can turn on multi-factor authentication, which gives an excellent second level of security for users who have a verified phone number.
And you can also customize the UI of the login page. This is important, because your users will log in at a login.microsoftonline.com page that doesn’t look like it has anything to do with your app by default:
Step 3 – Create an AD B2C Application
Finally you need to create a new application in AD to represent the application you will be protecting with Azure AD. Give it a name, select that you want to include a web app, and then you need to provide a Reply URL.
The reply URL is a special URL that includes the name of your function app. So if your app is called myfuncapp, the reply URL will be https://myfuncapp.azurewebsites.net/.auth/login/aad/callback
Once you save this application, it will be given an application id (which is a GUID). That’s important as you’ll need it to set up Easy Auth, which is the next step.
Step 4 – Set Up Azure Functions Proxies
The way we’re going to make our single page app magically work with our back end functions is for both the static content and the functions to be served up by our function app. And we can do that by using function proxies. I actually wrote an article about how to use proxies to serve static web content, but there’s a gotcha that’s worth calling out here. Since we’re proxying web requests to static content and serving up functions from the same function app, we need to make sure that the proxies don’t intercept any calls to /api
which is where the functions will be going.
So here’s how I do it. I set up three proxies.
The first has a route template of /
, and points at my index.html
in blob storage. e.g. https://mystorageacct.blob.core.windows.net/web/index.html
The second has a route template of /css/{*restOfPath}
and points at https://mystorageacct.blob.core.windows.net/web/css/{restOfPath}
And the third has a route template of /scripts/{*restOfPath}
and points at https://mystorageacct.blob.core.windows.net/web/scripts/{restOfPath}
This way my site can have static content in css and scripts folders and a single index.html
file, while the /api
route will still go to any other functions I have.
Step 5 – Configure CORS
Our static website will be calling through to the functions, so lets make sure that CORS is set up. In the Azure Functions CORS settings, add an entry for https://myfuncapp.azurewebsites.net
(obviously use the actual URI of your function app, or any custom domain you have pointing at it)
Step 6 – Enable Easy Auth
We enable Easy Auth by going to our Azure Function app settings screen and selecting Authentication/Authorization, and turning App Service Authentication on.
And we’ll also say that when a request is not authenticated, it should log in with Azure Active Directory.
Next we need to set up the Azure Active Directory authentication provider, for which we need to selcet “advanced” mode. There are two pieces of information that we need to provide. First is the client ID, which is the application ID of our application we created earlier in AD B2C. The second (issuer URL) is the URL of our sign up or sign in policy from AD B2C. This can be found by looking at the properties of the sign up or sign in policy in AD B2C.
Once we’ve set that up, any request to either the static web content (through our proxy) or to call a function will require us to be logged in. If we try to access the site without being logged in, we’ll end up getting redirected to the login page at microsofonline.com.
Step 7 – Calling the functions from JavaScript
So how can we call the function from JavaScript? Well, it’s pretty straightforward. I’m using the fetch API and the only special thing I needed was to make sure I set the credentials
property to include
, presumably to make sure that the auth cookies set by AD B2C were included in the request.
fetch(functionUrl, { method: 'post',
credentials: 'include',
body: JSON.stringify(body),
headers: new Headers({'Content-Type': 'text/json'})
})
.then(function(resp) { if (resp.status === 200) {
// ...
Step 8 – Accessing the JWT in the Azure Function
The next question is how can the Azure Function can find out who is calling? Which user is logged in? And can we see what’s inside their JWT?
To answer these questions I put the following simple code in my C# function to examine the Headers
of the incoming HttpRequestMessage
binding.
foreach(var h in req.Headers)
{
log.Info($"{h.Key}:{String.Join(",", h.Value)}");
}
This reveals that some special headers are added to the request by Easy Auth. Most notably X-MS-CLIENT-PRINCIPAL-ID
contains the guid of the logged in user, and helpfully their user name is also provided. But more importantly X-MS-TOKEN-AAD-ID-TOKEN
contains the (base 64 encoded) JWT itself. To decode this manually you can visit the excellent jwt.io, and you’ll see that the information in it includes all the attributes that you asked for in your sign up and sign in policy, which might include things like the user’s email address or a custom claim.
X-MS-CLIENT-PRINCIPAL-NAME:Test User 1
X-MS-CLIENT-PRINCIPAL-ID:7e9be1af-6943-21d6-9ae1-5c78c11ff756
X-MS-CLIENT-PRINCIPAL-IDP:aad
X-MS-TOKEN-AAD-ID-TOKEN:eyJ0eXAiOiJKV1QiLCJhbGciOiJSUz...
Unfortunately, Azure Functions won’t do anything to decode the JWT for you, but I’m sure there are some NuGet packages that can do this, so no need to write that yourself.
Should I Use This?
I must admit it didn’t feel too “easy” setting all this up. I took a lot of wrong turns before finally getting something working. And I’m not sure how great the end product is. It is a fairly coarse grained security approach (all web pages and functions require authentication), and having the functions returning redirect requests rather than unauthorized response codes feels hacky.
What would be really great is if if Azure Functions offered bearer token validation as a first class authentication option at the function level. I’d like to say that my function is protected by bearer tokens and give it the well known configuration of my authorization server. Hopefully something along these lines will be added to Azure Functions in the near future.
Of course another option would just be to set your functions to anonymous and examine the Authorization header yourself. You would need to be very careful that you properly validated the token though, making sure the signature is valid and it hasn’t expired. That’s probably reason enough to avoid this option because you can be sure that if you try to implement security yourself, you’ll end up with a vulnerable system.
Maybe there’s a better way to integrate Azure Functions and AD B2C. Let me know in the comments if there is. Chris Gillum who’s the expert in this area has a great two part article on integrating a SPA with AD B2C (part 1, part 2 although it isn’t explicitly using Azure Functions, so I’m not sure whether all the configuration options shown can be used with Function Apps (that’s an experiment for another day).
Comments
I had the same question - using SPA and Azure Functions - and found this article. It's pretty recent so perhaps you don't have anything new to add, but I thought I'd ask anyway since things are changing quickly with Azure. Perhaps the update to .Net Standard 2.0 that is in the works will improve things.
cchdiscno this is the best I can offer at the moment. I've done some work on manually verifying a JWT from AD B2C, but that's not ready to blog about yet, and hopefully won't be necessary in the future anyway.
Mark HeathRight. Because perhaps .net standard 2.0 will have native support for JWT?
cchdiscI don't think that net standard is the issue. There are nugget packages that can support jwt just fine already. It's simply whether that get round to building out into the functions runtime as a first class citizen you can select on a per function basis for authorization
Mark HeathI think we're saying the same thing. It has to be a first-class citizen of the framework before it can be made a first-class citizen of Functions
cchdiscI have a site (SPA) connecting to Azure Functions and I can see the requests using Fiddler. I can copy the POST/GET requests in Postman including the URL, headers and body. The problem now is that anyone can send the request from Postman with incorrect data. I can't apply authentication in the part of the site that has a feature as I want it easy for my users to use it.
Elvin DauzAny thoughts?
well you have to have some kind of authorization scheme, as its always going to be possible for users to view requests with tools like fiddler and send different payloads. One possible way is to use Auth0, which has some good tutorials for integrating with Azure Functions - http://fabriccontroller.net.... I'd like to see Azure Functions make it much easier to validate OAuth tokens on an individual function basis, rather than the technique I show here which applies to all functions.
Mark HeathWhat AuthorizationLevel do you set your function to? When I use User, it still requires a code in the query string.
Scott Wettsteinthe function can be set to anonymous, as the authorization has already been done at a higher level, so no secret api code is required
Mark Heathi think it must be Anonymous for now. User is an option in the enum but i can't find anywhere that says it has been implemented. it's risky for internal APIs that you might accidentally expose them to the world if you forget to turn EasyAuth on.
Scott WettsteinDo you validate the jwt token to make sure the jwt is infact valid? i.e. Make sure the issuer and audience match expected ones.
dasithsThat's the point of Easy Auth - it validates the token for you. You can of course perform additional steps like checking it has a scope you expect
Mark HeathThere are a few things the documentation isn't clear about. For example what if someone makes a call to the api with the x-whatever headers present. Does EasyAuth clear them and only put validates ones in? The other major issue is development time mocking of this. I ended up mocking it to put a test user when running locally. My experience with EasyAuth has been that it's very simple but doesn't cater for more complex scenarios like requesting extra claims from AAD in the OAuth flow(For example, guest users in an AD don't return the first name and last name claims. To check if they are a guest user there is an extra claim which is required and that not present in the set returned in EasyAuth.)
dasithsyes, easy auth is quite a basic approach that doesn't work in all scenarios. Perhaps a better option for your scenario would be to use API management which now has a serverless pricing tier
Mark Heath