At my company, we have many internal apps that are hosted in the cloud, one of these is a quality metric app that gives pass rates of latest testing runs against the current build. In an open and transparent sense, I want anyone in the company to use this app and see what’s going on with our build quality and recent code changes. Authenticating our users against Azure Active Directory (AAD) for this ASP.NET app is pretty straight forward and well documented. But there are some pages on this web app that allow testing to start/stop, or disable certain test agents etc. It makes sense to restrict those functions to only those people in the company who would need to do that. Hence I need to Authorise certain staff to do some functions and not others. This Authorization concept is well understood in ASP.NET (and can be achieved with Roles), but less so in terms of how you would integrate roles or equivalent Authorization with AAD. Most Role based implementations for ASP.NET use a local matrix of user to roles mappings. This is exactly the piece I wanted to avoid and is discussed in this article.
Identity Management – Setting the Scene
Our company I imagine is pretty typical of a lot of smaller companies. We have a group of people in an office, people working from home, 1-2 staff in India and the UK. Some of the apps we use are hosted in our data centre, some are hosted on Azure. Staff also want to access key apps and data from mobile devices (who doesn’t?). Compared to the corporate eco-systems of a decade ago, this fluid environment represents a real challenge. Instead of a controlled and defined eco-system with a clear corporate perimeter and a centralised Identity Management system, we instead have apps located anywhere, staff located anywhere, and an essential requirement to keep everything secure and available.
I am placing big money on Azure Active Directory (AAD) – essentially Identity Management in the Cloud, offered by Microsoft and based partially on their enterprise success product ‘Active Directory’ (which has nearly 15 years of maturity and enterprise adoption).
With AAD (via the Azure management portal) I can create users in our company, manage their password resets, lock them out, insist they use Multi-factor authentication and other goodies such as reporting. This holds great promise if I can leverage AAD features across our inventory of distributed cloud based apps. The ‘holy grail’ for me is when someone leaves the company, I just disable their AAD account and bingo they are locked out of every app we have, regardless of where that app is hosted. I love the idea of that.
AAD isn’t just a silo offering either. Supposedly 2000+ vendors offer AAD integration into their own products. DropBox, Zoho, Zendesk, Trello, and the list grows. We’ve already added a few and the process was pretty painless. This is just gold when an employee leaves. With one click I can also deny them access to DropBox, Trello, and many other 3rd party apps as well. This stuff used to take a day.
But setting up a user in AAD using the azure portal is kind of underwhelming. You create a new user, get a temporary password, they change it on the next login, then nothing… Nothing will actually happen until you start associating that user or your active directory tenant with Applications, either Apps your company is developing or 3rd party apps offered by other vendors. This article will focus on linking up an internal company ASP.NET app to AAD.
Completely Outsource Identity Management from your ASP.NET app
A few years ago, ASP.NET apps showed great promise by suddenly allowing external authentication providers to authenticate your users against, with just a few additional lines of code – Google Accounts, Facebook, Microsoft Accounts were all supported. This was pretty exciting as it allowed people to use your app, but authenticate against a different provider. It meant one less password for the user to remember and also pushed the responsibility of managing and securing passwords away from the developers and owners of the app itself. Apps also had the ability to handle both local users (where the hashed password was held by the app) and external users that were authenticated via one of the external providers.
So at this point, the user registers with your app but authenticates against (for example) Google. After this registration process the user would still exist in the app’s database (assuming you are using the Identity Management system that comes out of the box with VS2013 templates). The main difference is that the user would be stored in the database with no password hash since that stuff would be managed by Google and not our app.
In a way it has to work like that for a wide-ranging app on the internet. You’re still going to need a database with some kind of users table that holds all your users. Typically you may want to store extra info about them, but also you may want to control what they can do. Are they a Verified user? Are they an Admin? Are they on the Premium Plan? Do you want to lock them out of the app?
On the surface of it, surely only your app can know the answers to those questions. It’s impossible to get Google to authenticate the user and also say “By the way, Peter is on the Premium Plan, give him access to your special features”. That just doesn’t make sense. And so we find ourselves at the junction between Authentication and Authorization. We outsourced the Authentication to some trusted providers (Google, Facebook, Microsoft), but Authorization could not be outsourced, so we had to do that internally (holding an authorisation matrix of who can do what internally within our app’s user database)… Or could it? Maybe there is a way to outsource Authorization as well (read on).
There are many ways to do this kind of Authorisation aspect and you can certainly roll your own, but the ASP.NET Roles feature is quite handy and well understood. By annotating Controllers, Controller Actions, or even the web.config you can make your entire app or parts of it locked down to certain users in certain roles. Out of the box you get the [Authorize] attribute in ASP.NET MVC and Web API (although be careful the Web API one is in a different namespace). And you can specify certain Roles in the Authorize attribute [Authorize(Roles=”Admin”)].
But Roles are a bit old school these days. Microsoft has been pushing Claims as an alternative authentication process through their Windows Identity Framework, and claims seem like a good fit. AAD supports claims too, so this feels like the right direction.
The question I had was this: For an internal app where I fully control the user base (limited to users defined in AAD) could I control both Authentication and Authorisation of a user solely from AAD? Could I abandon the need for a local user database in each of my apps, where the pretty much sole purpose of it would be to hold the roles each user belongs to?
The answer is yes! And this is pretty exciting. You end up with the following advantages:
- You end up with barely any code in your ASP.NET app relating to Authentication and Authorization.
- You don’t need a database local to your app to hold user/role matrix stuff.
- As an Admin, you don’t need to login to each app to changes user roles etc when someone moves around or needs access. You therefore don’t need all the UI management in each app around this.
- You end up with a global place to manage your users (Azure Active Directory) and you can see which Groups a user is in, and these define access levels.
- Your ASP.NET app becomes simpler and therefore has a smaller attack footprint.
Roles or Claims or Groups?
Out of the box in VS2013 with an ASP.NET app using Identity v2, you will get a local DB schema created which supports users, roles, users in roles and external providers such as Google etc. The schema looks like this:
Even without UI support, you can create a role, then get the user ID of a user and hack the RoleId and UserId into the AspNetUserRoles table above and bang you will have support for Role Checking in your app. A UI would be nice, but I don’t think the current VS / ASP.NET templates support User and Role Management (they used to with the Simple Membership system prior to Identity).
But this approach goes down the path of putting the Authorisation stuff (who can do what) into the ASP.NET app. My goal was to outsource that fully to Azure Active Directory so the app knows the bare minimum about Authorization; ie: it can still say “Uh-uh, you aren’t allowed to see that page”, but it is not responsible for the management of which users can perform various actions in the app.
If I could achieve that I wouldn’t even need the tables above in my app.
I follow pretty closely these OpenID Connect and OWIN code samples located here. The samples are great, you can basically download the VS solution, change about two items in your web.config to point to your own AAD tenant and you are up and running. There is a Role based sample and a Claims based sample, but neither of these matched what I wanted to achieve, so the article you’re reading now is like a hybrid.
This tutorial by Geoff Webber-Cross is also very good and uses the AAD Graph API to interrogate the AAD tenant about Authorization. In many cases this is a good idea. Geoff’s tutorial pre-dated the ability for AAD to emit Group claims in the authentication response, so at the time he wrote that tutorial his was really the only way to do it. But there is now a slightly simpler way discussed below.
Let’s also approach this problem by re-using as much stuff as possible that works out of the box with Visual Studio, ASP.NET and Identity Framework, and attempt to write as little code as possible on our own. Especially around code relating to security, I always see a “roll your own” approach as risky.
I’m now going to jump in a bit to the actual problem we are trying to solve. There is tons of background on the net, I suggest you at least read this tutorial and code sample to get an idea of what I’ve setup already. It’s pretty good and explains things well. Below I’ve used this approach, created a temporary AAD tenant called paulscompany.onmicrososft.com and created two users within AAD (TestUser1 and TestUser2). I added an application in the AAD portal for my ASP.NET app and got a clientID, modified the web.config to point to my AAD tenant, and also stored the clientId. It’s all explained pretty well in the above link, so there is point in explaining further here. However, that’s also now the departure point from the tutorial above, after this we are going to deviate slightly from that tutorial to solve our actual problem.
By following the tutorial above, and with barely any custom code, our simple ASP.NET application is now authenticating against our AAD tenant (paulscompany.onmicrosoft.com) and any authenticated user can sign in use the ASP.NET app, and even visit pages that are restricted with the [Authorize] attribute. This is great and didn’t take a lot of effort. But how about restricting access between TestUser1 and TestUser2 so that they can both login, but only one of them can see a special page?
Let’s use ASP.NET’s Authorize attribute to decorate various Controllers or Controller Actions we want to secure. And let’s use the fact that the Authorize attribute supports the Roles property (which can be a list of strings). By using these features we can take advantage of in-built ASP.NET built-in authorization without writing anything tricky.
Once a user is logged on using AAD and using the more modern OWIN middleware, its possible to take the currently logged on user (principal) and convert them to a ClaimsPrincipal (see first line of code below). This will tell you more stuff about the currently logged on user including the list of claims issued by the identity provider (in this case AAD).
You can see below that the About action has an [Authorize] attribute on it. So in the pipeline an ASP.NET User has been constructed using information in the incoming Authentication cookie and the claims are built up as per what is in the cookie. Interesting to note that there are 17 claims by default. The two claims of interest to us at the moment are: tenanatId is the ID of the AAD tenant (in this case paulscompany.onmicrosoft.com). the ObjectId is the Id of the user I logged in with TestUser1.
It’s convenient to use the Roles Authorization feature in ASP.NET as its baked in, but its not convenient to actually use Roles and Users in Roles as this requires a matrix and persistence mechanism to store this stuff somewhere. My objective was not to store this stuff locally in a DB attached to this app which is what the ASP.NET templates will do for you out of the box.
Further to this, AAD for example has no native concept of Roles and Users in Roles. That said, there is a pretty interesting tutorial here that shows how to edit the manifest of the application as defined in AAD and define by metadata (JSON) various roles you want to support. The AAD UI in the portal goes as far as reading these JSON meta-data bits and then providing a basic UI to assign a user to a role. This is pretty cool, but when security reviewing your AAD, its impossible to see which of your applications (that use AAD) have these roles defined and which roles users are in. So it’s good but its not great. I think Microsoft provided the capability for apps that were heavily invested in Roles and Users, and wanted to port to AAD. This would allow them to get the app up and running more easily without having to re-write lots of it.
As mentioned above Roles is basically an old school ASP.NET concept that started around the time of the Simple Membership system. What is more modern and more native to both on-premise Active Directory and AAD is Groups!. A user can belong to multiple Groups in AAD. Is a Group the same as a role? Well it can be, it comes down to semantics and how you want to define your Groups.
Within about 60 seconds in the AAD portal I can create any groups I want and assign users to those Groups. At any time, I can add more users, remove users and manage the entire matrix of users to Groups easily. I can do this without going near my ASP.NET app. The screenshot below shows the experience. I’ve created a group called “QADashboard_Admin” and added one of the AD users to it (TestUser1).
I think if you go down this path, you need to name the Groups intelligently. A group called “Admin” might make sense in a single ASP.NET app (if it was a role), but doesn’t make sense in an overall AAD tenant. I mean Admin for what? In my case my original requirements were as follows:
- Allow any user in the Active Directory to authenticate against the QA Dashboard app and see testing results.
- Only allow some of the users from Active Directory to see restricted pages.
From the above requirements a Group named QADashboard_Admin is fairly descriptive. Bear in mind as time goes by and you have multiple apps and multiple users, you will always come back to AAD in the portal to assign users to groups. If the Group names don’t make sense or don’t stand on their own, then you’re going to have a hard time accurately controlling this.
Although we are getting closer, by creating our group and adding a user, at this stage we will have achieved nothing. Part of the reason is that of the 17 claims shown earlier that were issued by AAD after the user successfully authenticated, none of these relate to anything about the Groups a user belongs to. To ask AAD to emit the group information as well requires a small tweak to the manifest.
While we are in the portal though lets take a look at the properties of the Group we created (see below). It has a GUID starting with “941b….”. We’ll need this later.
when you download the manifest (see this article for details) for your Application from the portal in AAD, you will see in the JSON file a section called ‘groupMembershipClaims’. The current value will be null (see below). You need to change this to “SecurityGroup”. Save the file locally and re-upload the modified manifest back into the portal.
With just that one change, let’s now see what happens when we login and authenticate and inspect the claims. I’ve logged in again as TestUser1 (same as earlier). Studying the claims below, the good news is that we have a new one (compared to before). It’s a claim of type ‘groups’ and the GroupId, definitely matches the Group we created in AAD for QADashboard_Admin (a GUID starting with 941b). So AAD is now telling us that TestUser1 belongs to that Group in AAD.
This is great progress, as we now have AAD emitting the Groups a user belongs to as a list of claims. Although I haven’t shown it here, if I logged in now as TestUser2, they definitely would not have Claim shown above as they are not a member of that group.
We are getting close now. We have two users, one is an Admin kind of group, the other is not. Both users can authenticate and use the ASP.NET app. AAD is now emitting the GroupId of the Admin group for TestUser1. We just need to tell our ASP.NET app minimal info about this Group and we are done. As is stands now, our ASP.NET app will receive the extra claim above but it does not know what it means or what to do with it.
We have a couple of broad issues remaining. Firstly, there seems to be no way to get AAD to emit the name of the Group (QADashboard_Admin), it only emits the Id. Secondly since we still want to hook into the Roles aspect of the Authorize attribute we need a sneaky bit of code to transform Group claims to Role claims. By doing this, everything downstream that is baked in to ASP.NET and the Identity Framework will work for free.
If we really want to work with the group name, there is no choice but to use the AAD Graph API which can basically tell you anything. To achieve this, you would need to generate a secret key for your app (from the AAD portal). You then provide the client secret and the key, make an HTTP request and you can interrogate the AAD tenant (you can choose read or read/write when issuing the key). Geoff goes down this path in his tutorial. This is fine but a little overkill. Since this is an internal app, and slowly changing, and the IDs from AAD are immutable, I took the approach to say if the claims contain a ‘groups’ claim with Id = ‘941be607…….’ then the person is definitely a member of the QADashboard_Admin group. Yes, that’s hardcoding, yes it’s a bit obscure – maybe I’ll regret it, maybe not. If you go down the key approach to access Graph API to get the name of the Group – the key you generate only lasts for up to 2 years. So that approach comes with its own maintenance headaches. The approach I am taking does not require any keys.
For simplicity when I detect this Guid in the ‘group’ claims I add a new claim of type ‘roles’ and a value of ‘Admin’. This is kind of a claims transformation process and is quite common with Claims based Authentication. I could have added a new claim of type ‘roles’ with value ‘QADashboard_Admin’. This also would work, but then I would need to map this to my [Authorize(Roles=”QADashboard_Admin”)]
This part is a juggling act. In a way I didn’t want the implementation of my ASP.NET app to be so heavily tied to the AAD Groups. So at AAD level the Group is “QADashboard_Admin”, but at the app level the role will be checked simply against “Admin”. You can implement how you see fit. Below is my modified ConfigureAuth method as part of the OWIN pipeline.
The first highlighted line tells the OpenId Connect modules to look for any claims of type “roles” when validating ASP.NET Roles used on the Authorize attribute. So this part completely blows away any need for local matrix/tables of users to roles. Basically if they are in the claims they will be used. The issue is how to get the Roles in the Claims. That’s handled by the second highlighted section. Upon receiving back a list of claims from AAD after the user is authenticated, this list is conditionally enriched with more claims. Specifically we will add new claims of type ‘role’ if there are current claims of type ‘group’ that match the AAD Group we are looking for. As mentioned above unless we use the Graph API to obtain more info, we only have the GroupId (Guid) to work with, not the group name.
After adjusting this code and logging back on as TestUser1 and inspecting the claims, we now see that as well as the GroupId emitted by AAD, we have further enriched the claims ourselves by adding a claim of type ‘roles’ with value = ‘Admin’ (see below). This happened early in the pipeline, so that by the time we hit our Authorize attribute and demand a certain role, the correct role claim is already present.
So TestUser1 gets a role of Admin added to their claims list, TestUser2 does not. We can now secure certain parts of our app so that TestUser1 can do things and see things that TestUser2 cannot.
This approach is pretty good. What did we achieve?
- We barely wrote any code in our ASP.NET, yet we achieved access to the ASP.NET for any authenticated user in our AAD tenant.
- We also controlled the Authorisation to parts of our app based on the AAD Groups a user belongs to. This has the enormous advantage that I can add and remove users to the appropriate groups all from the centralised AAD portal. I don’t need to go anywhere near the ASP.NET to manage which users can do what.
- I also don’t need to store any roles, or user-to-role relationships in the ASP.NET app. In fact for this app, there is no database with user information at all.
- All user information, credentials, passwords etc are totally outsourced to AAD, thereby simplifying the ASP.NET app enormously. There is no need for this app to support password reset password recovery, sending emails, changing passwords, etc. All of that stuff is irrelevant for an app like this where Authentication has been outsourced.
- At the AAD level I can also turn on features like multi-factor authentication, again without touching the app.
There are still a few rough edges, but all-in-all this approach demonstrates a solid way to outsource both Authentication and Authorisation out of an ASP.NET to AAD.