Create an Azure AD protected API that calls into Cosmos DB with Azure Functions and .NET Core 3.1

In today's post we will see how we can create an Azure AD protected API using Azure Functions. The API will use Cosmos DB as a backend and authorized users will be able to interact with the Cosmos DB data based on their permissions. We will be using .NET Core 3.1 and C# to put all this together. There are 3 main components here

  1. Azure AD to for token validation and authorization
  2. The Function App
  3. The Cosmos DB database

First, we will need to create and populate our Cosmos DB with some data. If you already have sample data then that's awesome, you can skip this step. Then we need to register two apps in Azure AD and configure application roles for our users. Finally, we will implement the appropriate code in our Functions to validate user tokens and then perform the appropriate CRUD operation in CosmosDB

Create and populate the Cosmos DB database

Head over to the Azure portal and create a new Cosmos DB database. Provide the subscription, resource group and a name for your Cosmos Account. If you want to simply test Cosmos DB and save some money, you can choose the Free Tier Discount. Please note that there are limitations to the free tier.

Press Review and Create if you wish to omit Networking or Encryption settings. Once our Cosmos DB instance is ready, we can create a new empty collection by navigating to the Data Explorer tab and selecting New Collection:

Since we will be working with the Volcano sample dataset, we will be using the id field as the Partition Key. To upload the sample data, we need to expand the Volcano Collection, click on the Items node and select the Upload Item option from the menu:

The import should be instantaneous, so refreshing the query results on the page should result to this:

One last thing we need to do is make a note of the connection string as we will need to configure our Azure Functions with it. Head to the Keys tab and grab the connection string (either the primary or secondary will work):

At this stage we have a Cosmos DB instance we can use with our Functions. Next, Azure AD.

Configure Azure AD for authentication and authorization

We will need to create 2 separate application registrations in Azure AD. One for our (Functions) API and one for the API client. You could have multiple API clients (mobile app, web, desktop) all with different scopes as well but for now we will keep it simple.

Create the API App Registration

First, the Function API app registration. In the Azure Portal, navigate to Azure AD select the App Registrations tab and create a new registration. Give it a meaningful name, select Single Tenant and leave the Redirect URI empty:

Next, navigate to the API Permissions tab and delete any existing Permissions (should be only one for Graph -> User.Read). Since this is an API that we want to expose to the world, we need to navigate to the Expose an API tab to configure our settings. First we need to add a scope so, press the Add a scope button:

Add a scope name, in this instance access_as_user, and the appropriate messages for admins and users. Make sure to press Add scope in the end:

If you want to change the Application ID URI, you can do so:

Finally, we need to add the Roles to our application. Unfortunately there is no UI to do this yet so we have to edit the json in the application Manifest directly.

We may not have an easy way to add Roles to an app via the UI for now but we do have a way to do all this using the Graph API. At /build we announce the GA availability for Applications which means that you can now programmatically create and configure app registrations via the Graph SDK

Open the Manifest tab and edit the appRoles section. Interesting side-note: Emojis are fully supported, though you may want to approach this with some caution :) We will be adding two roles: a) Data.Read and b) Data.ReadWrite like below:

"appRoles": [
{
	"allowedMemberTypes": [
		"User"
	],
	"description": "Read-Write access to CosmosDB data",
	"displayName": "πŸ˜‚πŸ˜‚πŸ‘βœŒData.ReadWrite",
	"id": "0e13539f-f5d6-4b94-a349-4b178f677759",
	"isEnabled": true,
	"origin": "Application",
	"value": "Data.ReadWrite"
},
{
	"allowedMemberTypes": [
    	"User"
    ],
    "description": "Read access to CosmosDB data",
    "displayName": "Data.Read",
    "id": "caf1baec-8df5-4358-9703-6a5adc9a6a82",
    "isEnabled": true,
    "origin": "Application",
    "value": "Data.Read"
}],

You'll also need to update the AccessTokenAcceptedVersion to 2 (i.e. AAD v2) or otherwise the token validation process will fail later in our Functions code.

Make sure to press the Save button to persist the changes. At this point, we're done configuring the API app

Create the client API app registration

There could be many different clients that consume our API. In this instance, we will use Postman to emulate the front end but the important bit here is that each client app registration could come with different scopes and hence different access levels or restrictions. Our Functions code will be responsible for checking and validating both the access tokens and the user claims.

We have to create a new single-tenant app registration, but this time we need to provide a Redirect URI so that Postman can receive the JWT token (like PIN number - I know). The right Redirect URI for Postman is https://www.postman.com/oauth2/callback. We don't have to select Access or ID tokens in the configuration. Since we're using Postman, we also need a client secret, however depending on your front-end stack, you may not need one (see web app etc). We can create a new secret in the Certificates & secrets tab. Make sure to make a note of the secret as you won't be able to get it back once you leave this tab - you can, however, recreate it as they are cheap and easy to generate :)

Next, and final step, we need to configure the API Permissions for our app. Press the Add a permission button and select My APIs. We should be able to see the FunctionAPI app registration we created in the previous step.

Select the API Permission (scope) we configured for our FunctionAPI app and press Add Permissions

As a final step here, we need to Grant admin consent

Almost done. The very last step is to assign users to the application roles. This is done in the Enterprise Applications tab in Azure AD. Find the FunctionAPI app we registered in step one and select Assign users and groups.

Select Users and then assign them the appropriate roles:

You can assign multiple users and multiple roles simultaneously to speed things up as well :) Now, I know I shouldn't but I couldn't resist adding some emojis to our Role Permissions, which, surprisingly, showed up in the role selection window! Who said Identity and Azure AD can't be fun?

Our Azure AD configuration is now complete. 2 out of 3 tasks done. Now on to write some fun code!!!

Create a protected API using Azure Functions and Azure AD

We can use the Azure Functions Core Tools (CLI), the Azure Functions extension in VS Code or Visual Studio to create a new Function App and the following 3 HttpTrigger Functions:

  • GetVolcano (GET,POST)
  • GetAllVolcanoes (GET, POST)
  • UpdateVolcane (POST)

For this example I will be using the Core Tools so the commands to create all these are:

func init
func function create -> HttpTrigger -> GetAllVolcanoes
func function create -> HttpTrigger -> GetVolcano
func function create -> HttpTrigger -> UpdateVolcano

We can now open this in VS Code start adding the appropriate code. First of all, we need to add the appropriate NuGet packages. Open the *.csproj file in your Function App folder and add the following package references:

<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.6.0" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="6.6.0" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.CosmosDB" Version="3.0.7" />

Pressing save will trigger VS Code to prompt you if you want to restore the newly added NuGet packages - go ahead and do this. Next, we need to add some extra configuration settings, namely the Azure AD details and the Cosmos DB Connection string. Open your local.settings.json file and add the following information

{
    "IsEncrypted": false,
    "Values": {
        "AzureWebJobsStorage": "UseDevelopmentStorage=true",
        "FUNCTIONS_WORKER_RUNTIME": "dotnet",
        "CosmosDBConnection": "<Your Azure CosmosDB connection string in full>"
    },
    "AzureAd": {
        "Instance": "<yourAADInstanceName>.onmicrosoft.com",
        "TenantId": "b55f0c51-61a7-0000-84df-33569b247796",
        "ClientId": "69516c95-a7cc-0000-a62f-66aa336ef62a"
    }
}

Our Functions need to perform the token validation and also check for the appropriate claims (in the form of scopes and user roles). To do this, we will add class named Β TokenValidator that contains the following code:

There are 3 methods here:

  • GetJwtFromHeader()
  • ValidateTokenAsync()
  • HasRightRolesAndScope()

The first one grabs the JWT token from the header. The second one uses Open ID Connect to validate the token against Azure AD and the final one checks that the ClaimsPrincipal has the right roles and scope. Different user will likely have different roles, as in our case we have a read-only and a read-write role. We also check for scopes because different client apps may have different scopes which would also dictate a different behavior in the API. In our case we only have one scope but there is nothing stopping you adding more to your API and doing the appropriate checks in the code.

GetVolcano - Function

For this Function, we will use the Cosmos DB Function bindings to connect seamlessly with our database. There is also a little bit of code to grab the config settings from the local.settings.json file. The full function code is provided below:

From an authentication and authorization perspective, the Function allows anyone with the Data.Read and Data.ReadWrite roles to be able to execute the code. It also expects that the user has provided the expected scope, i.e access_as_user.

GetAllVolcanoes - Function

For this function we will again use the Cosmos DB Function bindings and the code is more or less the same apart from the fact that we return all the records instead of selecting just one. The roles and scopes allowed are also the same as before. The full code is shown below:

UpdateVolcano - Function

This function is slightly different. Firstly, we use an output Cosmos DB binding to execute the Update/Insert into Cosmos DB. Secondly, this API endpoint is restricted to users that have the Data.ReadWrite role. Read-only users will receive an HTTP 401 error if they attempt to execute this code. The code is again below:

Volcano - DTO

Finally, we need a class that describes our volcano object and can be used to serialize and deserialize data. The volcano.cs class is attached below:

Putting it all together

As I mentioned earlier, I will be using Postman to test the API so off with it :) In Postman, I created a new request and I went straight into the Authorization bit as this is the most important step

When we press the Get New Access Token button, we are presented with a new window where we need to enter the details from our Client app registration (not the FunctionAPI app - that's use by our Functions, remember?).

Of particular importance are the Callback URL which needs to match what we put in the client app Redirect URI in AAD and the Scope which should be API Permission we configure in the client app API Permissions tab. If you're wonder where to find the auth and token endpoints, head back to your Azure AD -> App Registrations tab and look at the Overview:

If all is configured correctly, upon pressing the Request Token button you should be prompted by AAD to login in with your tenant's account details and in return you should receive a token. We can inspect the token on jwt.ms where we should see something similar to this:

You should notice how the roles, scope(scp) and audience(aud) all contain the information we need for our token to be validated by our API. I can now execute my HTTP request and receive some data:

And although this succeeds (as expected), check what happens when I use the same token to call the UpdateVolcano() method - 401 Unauthorized:

As you can see, following the steps in this blog we were able configure AAD, create a Cosmos DB database and implement an Azure AD secured API using .NET Core 3.1 in Azure Functions that performs CRU(D) operations against our data. You can find the full source code in this Github repo. All you need to do is slap a local.settings.json file with your settings (as shown earlier) and you're ready to go!

Watch me and JP aka @AzureAndChill build this solution live during our first stream on Mixer/Twitch