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 6 of the series. To navigate to other articles in this series, look at the end of any article in the series for the list.
What is a Token Store and Why do you need it?
By default, when the user logs in to our app via Azure AD, the user is only authenticated to the AD. But he/she does not have any permissions to access anything there — and by consequence, our app will not have access to access anything either.
Think of it as the process of trying to board your flight at an airport.
- You will be stopped at the entry gate and security staff will check your identification. In our application’s workflow, this is the login prompt presented by Azure AD. “Who are you?“
- Just like the security at the airport gate cannot really determine if you are who you say you are — they do not validate the validity of the documents you present. They only test if all of them say the same thing. This is called a “claim“. You claim you are “John Doe”, but the system has no way to say for sure that you are “John Doe”.
- The security staff then sends you inward to your airline’s check in desk. While the human person does not really attach any tags to you, our code / call / request at this point has tags attached to it saying “This person calls himself ‘John Doe’, I am not sure if he is that, but things check out and you can go ahead and pretend he is in fact, John Doe.“. Yes, the set of authentication claims. At this point, you have been authenticated. But you are still not authorized to jump into the aircraft or do anything else!
- Now you reach your airline’s check in desk and you present your ticket. The staff validate your “claims” (Identification) and your “authorization request” or “scope” (“I need to board aircraft XYZ to go to destination ABC and my ticket number is 123456“) and determine if a person with this identification is authorized to use this ticket/scope. If there is an entry on their end clearing you, you are issued your boarding pass. The boarding pass for our application is our access token.
- The rest of your wandering around the airport and eventually boarding your flight is by virtue of your boarding card. That card authorizes you to be in certain areas — you can access the rest rooms, waiting areas, food courts and the aircraft itself. But, that card does not authorize you to go into crew-only or staff-only areas. It will not let you jump over the barriers or walk on the apron or runway outside. In the same way, your access token defines and limits what you can and cannot do.
Just like in the above airport example, you don’t run to the airline counter at the start to demand a new copy of the boarding pass every time someone stops you and demands to see it. You pull it out of your pocket / bag. In the same way, our app should not request new access tokens every time it needs one. The token must be cached. Every access token that you get from an OAuth endpoint will specify its own expiry time and the token may be cached and reused safely within that time.
And where do you cache the access token? In the Token Store, of course!
So what can be a token store? Any storage mechanism that will let us query for a particular token (and retrieve it), remove stored tokens and store new tokens. You do not need to provide the ability to update a token, since when a token is updated [that is, its expiry time is extended using a concept known as “refresh tokens”], you essentially get a new token.
In our application, as I said earlier, I am going to be using Azure Table Storage. But you can use anything:
- In-memory – tokens will be lost every time your app restarts. Not scalable between instances in a server-farm or on the cloud. This is great to check out the workflow during development.
- In a SQL database – overkill. You do not need a lot of the features such storage offers. However, if you already have SQL databases for your application(s), then you can use it without adding additional cost to your infrastructure.
- In a document database – These are systems akin to Azure Table Storage, Cosmos DB, Document DB, Cassandra DB and so on that let you store data in Json format. This is great for us since the OAuth token is essentially Json.
- Files – You may additionally store the tokens as files on your web server’s disk. However, this is hard to scale unless you use network-hosted file systems [like a file share].
The token class itself is simple. We need minimal controls to identify the record when we retrieve it. The token itself is persisted as a single column containing the raw Json data.
Add a new folder to our project named “Data”. All our database persisted content will have its counterpart [data]-classes here. Add a class named AzureAdToken.
You need only a single reference in this class:
Annotate the class with the attribute: [Table(“AzureAdToken”)]. This attribute tells our Azure Table SDK [my SDK, that is] that the data from this class is to be stored into an Azure Storage Table table named AzureAdToken. You may change this string to something else.
Add the following to this class. No other code is necessary at this time:
- A blank default constructor
- Three properties of type string:
- TenantId – Annotate with attribute [PartitionKey]. This will be the partition key for our Azure table
- UserName – Annotate with attribute [RowKey]. This will be the row key for the data item in the Azure table.
- Token – Annotate with attribute [TableColumn(“Token”)]. This will become a table column named “Token” in the table.
We need to implement a mechanism that plugs into the MVC pipeline and also provide the facility to add/remove tokens at run time. To do this, we need to implement an interface and then a concrete class. The interface is required solely because we want to use MVC’s pluggable services architecture to achieve this.
In the AzureAd folder of the project, add a new Interface file. Call our interface “IAzureAdTokenProvider”. Define just two methods in it with the following signatures:
- Task InitializeAsync(ITokenCache tokenCache);
- void RemoveAccount(IAccount account);
You will need two references for this to work:
using Microsoft.Identity.Client; using System.Threading.Tasks;
In the same AzureAd folder, add a Class file. Call it AzureAdTokenProvider. Implement our interface in it. The code is quite long, so take a look at what you need to do in the file on GitHub. But, this is what we are doing there:
The ITokenCache object [in InitializeAsync] requires two event handlers to be set. These are BeforeAccess and AfterAccess.
BeforeAccess is an event raised when the token-system is checking to see if a token already exists. So, here, you need to query the backend system and retrieve the token if it exists. If you do not find a token, no problem, do not make any changes.
Similarly, in AfterAccess, you need to store the provided token. ASP.NET Core will set the “HasStateChanged” property of the notification to TRUE if the token was modified. If the token was modified, you need to save the new token. Otherwise, it is the same as what you already had and you can bypass your code.
The RemoveAccount() method simply clears the token stored for the given account from the storage.
We have added a method here [private] GetCacheKey(). Here, if the cache is an “application cache”, that is, we are trying to store an App token and not a User token, we return an empty tenant Guid and an empty User Guid. If the token is for the user, we return the TID [Tenant Id Guid] and UID [User object Id Guid] values from the user’s claim-set.
Finally, we need to plug it into our ASP.NET MVC service pipeline. To do that, open up your Startup.cs, go to the line at the bottom of ConfigureServices() where we have the services.AddControllersWithViews() call. Just above that, add this line:
Note: Your code will not compile at this point. Because, as you saw when you were doing the AzureAdTokenProvider class, we are trying to use an interface [and its implementation] IAzureTableStorageService that we have not written yet.
We are going to write a bunch of common “services” that we will need in the next chapter. At present, your [error-full 🙁 ] should be the same as my checkin set.