A quick recap - we’ve got three identity providers integrated into our app, set up an Azure Functions App in our backend using ARM, and we’ve set up authentication on that function app. We’ve also swapped our identity provider authentication token for an Azure App Service authentication token so we can use it on our backend. Now it’s time to consider the actual Azure Function for registration.
You want to store profile information in your backend. In my case, it allows me to communicate with the user (since I store their email address) and it allows other people to find my users (since they will know the email address). If I don’t have a profile service, then users won’t be able to see which photos are theirs if they log in with multiple providers.
The actual Azure Function is made up of two files. The easiest way to make one is using the Azure Functions Visual Studio Code plugin. Azure Functions have triggers (things that cause them to run) and bindings (where they get their inputs and where they send their outputs). For the registration API, we’re going to trigger on a HTTP POST and the HTTP request will be the input binding and the HTTP response will be the output binding. If you follow the tutorial linked above, you will get the right thing.
Let’s take a quick look at the first file - the
function.json file. This defines the triggers and bindings. It also determines the authentication level. There are three auth levels - anonymous (no API key required), function (a function level API key required), or system (a function app level API key required). I’m using anonymous auth level here:
Basically, my function is going to operate as a HTTP API - the input binding will be called
req and the output binding will be called
res. It will be triggered by a HTTP POST. Since it’s in a directory called
register, it will be called by doing a
Let’s turn our attention to the code. The basic code is simple:
First thing to note - functions are async! There is a synchronous version, but you won’t need it for most purposes. The
req is the variable that is passed in from the input binding. It must be named the same thing as the name within the
function.json file. The context contains a bunch of things, including the
res variable that is the output binding. It’s on the context because there might be multiple of them. Aside from that, the other thing to be aware of on the context is
context.log(), which sends data to the logs (App Insights or the streaming logs).
Step 1: Validate input
The request has the headers, query parameters, and body on it. The body is decoded if it is JSON. My API is going to take the email address and name in the body. Most of the time, this will be provided by the IdP. However, some IdPs (most notably, Sign in by Apple) are more privacy focused, and the IdP (or user) may not have given you permission to one of them. I want the API to handle those cases as well. Let’s start exploring the code for this:
You should put any validation you need here. I’ve got a couple of “helper” methods in use here. Once of them determines if the email address is valid, and the other converts a string to title case (i.e. the first character is upper-case). In Functions work, I try to code the function rather than use a module. Modules take time to load and that results in additional time during a cold-boot of the Function, so I try to avoid it.
The next step is to validate the authentication token. This is where you need some intimate knowledge of Azure App Service Authentication. There are several headers that App Service Authentication adds to your request:
x-ms-client-principalis a base-64 encoded JSON blob with the contents of the id_token.
x-zumo-authis a JWT that is sent from the client and contains information to identify the user. It’s basically the same information as
x-ms-client-principal, but the latter is easier to decode.
Neither of these things contains the email address or full name of the user. For that, you want the results of doing a HTTP
GET /.auth/me. You can use the request or node-fetch modules to do this, but I eschew modules for straight code. Here is my code (which is placed in the initialization section, like other functions):
This turns a standard HTTPS request into a promise. My code for looking at this is as follows:
I now have two objects -
input.principal, which looks like this:
input.idp looks like this:
You can see from this that (a) the two sets of information are completely separate, and (b) you want the information in
/.auth/me for this job.
Working with Cosmos
The next part of the process involves working with Cosmos. I’ve created a Cosmos resource in the ARM deployment, but not done anything with it thus far. First, let’s bring in the Cosmos client:
I’m using the latest production release of the Cosmos client library - v3.2.0 right now. In the initialization section of the function, I’m going to bring that in:
Cosmos has databases. Each database has containers, and each container has documents. The code above creates the client to the resource, but we still need to assume that the database and container do not exist. This is done within the Azure Function:
I should do something more than this if things really break, but I’m ok with returning a 500 status. If you want to do more (like send alerts to an on-call person), wrap this in a try-catch to do it. Now that the Cosmos service is set up properly, we can use it. There are different things to do depending on whether the user exists or not, whether they are pending registration, and whether it’s a new IdP. Let’s take a look at each one:
- If the IdP name matches a record in the database:
- If the IdP record is PENDING, return a “code required” error.
- If the IdP record is not PENDING, return success with the profile.
- If the IdP name does not match a record in the database, but the IdP email matches a record in the database:
- If the IdP exists with a different name field, return a “conflict” error.
- If the IdP does not exist, then add it to the record, return success with the profile.
- If the IdP name does not match any records in the database (including via email search):
- If the IdP email matches the requested email, register a new account as REGISTERED, then return success with the profile.
- If the IdP email does not match the requested email, register a new account with PENDING, send a code, and return a “code required” error.
There are probably some corner cases here. What happens when I want to log in as an existing user with a new IdP? What happens when I want to log in as an existing user with a new IdP that has a different email? What happens when the IdP doesn’t give me an email? These are all additional cases that I’m going to cover.
For now, I’m going to take a look at three cases just to see the code. These cases will represent the majority of registration calls.
- The IdP name matches a record in the container. This is the normal “subsequent run” version. I’m ignoring email validation for right now.
- The IdP name doesn’t match, and the email doesn’t match a record in the container. This is the normal “first run” version.
First of all, we need to see if the IdP record matches a record in our database. The records in the database will look like this:
The IdP information will not be returned as part of the profile. However, we can check it during a search. First, let’s create a query and do a search:
If we return one record from this search, then we’ve matched our first case - everything matches. We can short-circuit a lot of checking by making this the first check.
All the other cases need to understand if the IdP email exists, so let’s do that search in a similar way.
We can now handle the case for when the email record does not exist, and the email in the claim matches the requested email:
The Cosmos container and database operations are great because they look just like CRUD operations. If you know the ID of the record that you want, you can just
read it. If you want to delete it, just do it. In this case, I’m giving the
create call a full record. It returns the newly inserted record with all the metadata that is stored alongside the record. Once I’ve got that, I just have to return it.
Now that I have both the returning user path and the new user path sorted, I can test with Postman. Generate the X-ZUMO-AUTH token with the Android app, paste it into the header, and do the post with the appropriate body. The first time through will store the record and then return it. The second time through will read the record and return it. It will be the same record both times.
The Android parts
I can now switch over to the Android part of this and update the app to deal with accessing the profile. This is just a case of doing another HTTP request - this time to
/api/register and dealing with the response. Since I (currently) have all the information I need, this is easily accomplished by just looking for the 200 OK response.
I’ve filled in some of the other code paths in the registration backend, and I’ve got some other plans on the profile page to link accounts that I will also need to deal with. However, this is good enough for me to progress now. You can find the code here:
I did find a bug while researching this blog. Azure App Service Authentication requires that the Azure Active Directory package use a JWT for the access token. When using the MSAL library with non-organizational accounts (such as my personal account), the access token is “something else” that isn’t a JWT, thus you get an unauthenticated request from the
/.auth/me/aad login. The solution to this (at least temporarily) is to use the Microsoft Authentication instead of Azure Active Directory.
In the next step, I’m going to start work on the main functionality of the app. Until I’ve got something to share, that’s it for now!