End-to-end walkthrough: How to Multi-tenant Application, register it with Azure AD, onboard tenants, go through the admin consent workflow, work with incremental [just-in-time] consent. All the code we write will be in C# and the application is an ASP.NET MVC application.
This is Step 3 of the series. To navigate to other articles in this series, look at the end of any article in the series for the list.
This article is basically theory. Laid out below is how the onboarding workflow will work, what state and other information we need to store, where and how we will store them, and so on.
What exactly is “onboarding” ?
We are used to apps of different types requiring us to “register” or “sign up” with them. Onboarding is no different. However, it applies at the organizational or account level.
A “tenant” is basically an entire directory of users, non-human accounts, devices, rooms and what not. Active Directory lets us create objects within its hierarchy to denote a variety of things. Azure’s Active Directory (written as Azure AD for short) is somewhat limited in what type of objects it will let you create within it.
From now, when we talk about Active Directory, we are talking only about Azure AD and not the AD that comes with an on-premises installation of Windows Server.
When we onboard a tenant into our application, we are creating a registration or association of that directory with our application.
This registration/association is two way. On our side, we need to create an entry for the incoming directory somewhere. We are essentially saying:
“Hey, we know this directory and in future we will allow user accounts and other objects from this directory to participate in our application“.
On the tenant-side, when you perform the association, Azure creates an entry for your application within that Azure AD (“tenant”). This tells the tenant that:
“Hey, we know this application and we will let our user accounts and other objects participate in this application.“
So, onboarding lets users from that tenant log on to your application and in return, your application may use the resources managed by that tenant.
How does this work?
In the previous part, you learnt how to register your own app in Azure AD as a multi-tenant app. That is only half the story. Once you someone has accepted your application [i.e., “onboarded”] into their tenant, you can go into the “Enterprise Applications” blade of that Azure AD tenant and see your application listed there. Of course, in normal circumstances, that tenant will belong to someone else and you will not be able to login there.
The entry in that Enterprise Applications blade is essentially a “pointer” to the app your registered in your Azure account. It will look a little different and that tenant’s administrator will not be able to modify most of the properties you set up at your end. If someone removes this entry, then the users of that tenant will need to go through the on-boarding workflow again to be able to use the application.
Detecting if your app is already on-boarded to a tenant
There is no “API” to do this. When someone logs on, if your app cannot access any resources in that user’s tenant, then your app probably has not been onboarded to that tenant yet. It might also be possible that it was previously onboarded, but someone deleted the entry from the tenant.
Unless you are writing an application that runs purely off its own database for everything, and only uses the Azure AD tenant for user-authentication, you will need permissions to access a whole bunch of stuff. And, a LOT of that stuff, especially the actually useful things, need an Administrator to consent to you using those APIs before your app can access that data.
Now, while Azure AD neatly segregates the duties of different types of admins, that also creates confusion and consternation. If the wrong type of administrator tries to provide access, you end up with one of two situations:
a) The consent process fails with strange results much after that event.
b) The consent process succeeds partially.
Both leave your app in an inconsistent state. While the problem will be visible to our human eyes, the app will have no recourse to identify the problem and prompt the user for next steps to resolve and continue.
The solution here is to require and limit the user performing the on-boarding workflow to the tenant’s Global Administrator. There is a programmatic way to identify the global admin for the target tenant. We will see exactly how to do that in the particular article.
Incremental consent / Just in Time Consent
The theory is that your app starts off with a minimal set of permissions consented to. As the user accesses larger parts of the application, the application will prompt for additional consents. Yes, that is the THEORY.
In practice, your user will be in Hawaii and the administrator that can provide consent on a hike in Antarctica. This is not going to work correctly or smoothly. Sure, Azure AD provides weird UX to request consent and manage it, etc. But, users want things to work. If you go with the so-called “recommended security practices”, unless your app has been mandated by the company’s IT department, people are going to stop using.
The best way forward is to request consent for EVERYTHING right at the start. At the on-boarding stage. That’s the only time during your application’s lifecycle you are going to have the undivided attention of the Global Admin. Live with it.
Incremental Access / Just in Time Access
This concept is similar to the Incremental Consent we saw above. But, here, you are only taking permission from the currently logged on user. This is how it is theorized to work:
App starts, you ask the user permission to get their basic details (name, email ID and maybe a couple more attributes). Then your app wants to read the user’s email, so you ask for permission — during the process of navigating to the page where the email is used — to read that. And so on. It is possible that the user needs to provide access only once per application or once per login, or every so many minutes.
Now, here, you will be limited by the OAuth API that is used to get the permission — and hence the underlying OAuth token — on what you can do. The APIs we will use mandate the need to use JIT-Access.
Unless you want to repeat the whole process every time you run the app [or access it], you better store the information somewhere. Typically, you would store it in a SQL or SQL-like database. Nowadays, you can also store it in a non-SQL database [like Document DB and so on]. Those solutions can turn out to be incredibly costly. When I am on Azure, I prefer using Azure Storage Service’s Table Storage system. That is why I also have my own SDK to interact efficiently with this system from my C# application code.
The code I provide will be leveraging Azure Table Storage via my SDK. You are free to modify this to use anything else [eg: SQL Server or MariaDB] as you please.
Am not talking about traditional REST-type services. Here, I am referring to “MVC services”. The ASP.NET Core MVC application system lets you create and use pluggable “services”. Doing this, lets us mix and match components based on the interfaces they implement while varying the implementation detail in the concrete class. Let me give you an example:
So, when you’re developing your auth code, you don’t want to clutter up a database with needless entries. Therefore, you would store the tokens you received in-memory. However once that code matures and you move to the next step, you want a better and more persistent storage (eg: a database). Using MVC services, all you need to do is modify one line of code in your Startup.cs and you are done. This also lets you retain all the implementations in your code, potentially choosing different ones for different purposes [eg: in-memory in Debug, database for Release].
Our application architecture
These are some salient points in our implementation:
- Authentication and token services are modular. We use MVC services to implement them.
- We store the OAuth tokens in Azure Table Storage via an MVC token storage service.
- All the key processes are wired up in Startup.cs.
- We implement full admin consent at on-boarding and JIT access during normal workflow.
- JIT access is implemented using Exception-pattern. That is, when a module has insufficient permissions for something, an custom exception is thrown and we handle that exception to acquire the required permission. This is again wired up in Startup.cs.
To handle the OAuth process itself, we use Microsoft’s “Microsoft.AspNetCore.Authentication.AzureAD.UI” library. Even then, we need to perform ~350 lines of initialization in our Startup and some more elsewhere (totally to almost ~1500 lines) to get things working. Not all of it is really plug-and-play!
Feel ready to dive in? Let’s move on to Chapter 4 !