RBAC is all the rage (as it should be) these days, but it’s not really new. The concepts have been in use for many years, it’s just a case of bringing it into modern cloud-based scenarios. Azure continues to invest in this concept so that we can increasingly lock down the control and use of various cloud-based services they offer. There’s other “interesting” aspects to these scenarios though that can impact the availability and usefulness of it – things such as existing systems and identities, cross tenant applications, etc.
It’s these interesting aspects that bring me to this post today. At Office365Mon we are ALL about integration with Azure Active Directory and all of the goodness it provides. We also have had from day 1 the concept of allowing users from different tenants to be able to work on a set of resources using a common security model. That really means that when you create Office365Mon subscriptions, you can assign subscription administrators to anyone with an Azure AD account. It doesn’t require Azure B2C, or Azure B2B, or any kind of trust relationship between organizations. It all “just works”, which is exactly what you want. IMPORTANT DISCLAIMER: the RBAC space frequently changes. You should check in often to see what’s provided by the service and decide for yourself what works best for your scenario.
“Just works” in this case started out being really just a binary type operation – depending on who you are, you either had access or you didn’t. There was no limited access based on one or more roles that you had. As we got into more complex application scenarios it became increasingly important to develop some type of RBAC support capabilities, but there really wasn’t an out of the box solution for us. That led us to develop this fairly simple framework of YARBACS as I call it, or “Yet Another RBAC Solution”. It’s not a particularly complicated approach (I don’t think) so I thought I’d share the high level details here in case it may help those of you who are living with the same sort of constraints that we have. We have a large existing user base, everyone is effectively a cloud user to us, we don’t have any type of trust relationship with other Azure AD tenants, but we need to be able to support a single resource (Office365Mon subscription) to be managed by users from any number of tenants and with varying sets of permissions.
With that in mind, these are the basic building blocks that we use for our YARBACS:
- Enable support for Roles in our Azure AD secured ASP.NET application
- Create application roles in our AD tenant that will be used for access control
- Store user and role relationships so that it can be used in our application
- Add role attributes to views, etc.
- Use a custom MVC FilterAction to properly populate users from different tenants with application roles from our Azure AD tenant
Let’s walk through each of these in a little more detail.
Enable Support for Roles
Enabling support for roles in your Azure AD secured application is something that has been explained very nicely by Dushyant Gill, who works as a PM on the Azure team. You can find his blog explaining this process in more detail here: http://www.dushyantgill.com/blog/2014/12/10/roles-based-access-control-in-cloud-applications-using-azure-ad/. Rather than trying to plagiarize or restate his post here, just go read his. J The net effect is that you will end up with code in your Startup.Auth.cs class in your ASP.NET project that looks something like this:
ClientId = clientId,
Authority = Authority,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
//for role support
RoleClaimType = “roles”
… //other unrelated stuff here
Okay, step 1 accomplished – your application supports the standard use of ASP.NET roles now – permission demands, IsInRole, etc.
Create Application Roles in Azure AD
This next step is also covered in Dushyant’s blog post mentioned above. I’ll quickly recap here, but for complete steps and examples see Dushyant’s blog post. Briefly, you’ll go to the application for which you want to use YARBACS and create applications roles. Download the app manifest locally and add roles for the application – as many as you want. Upload the manifest back to Azure AD, and now you’re ready to start using them. For your internal users, you can navigate to the app in the Azure management portal and click on the Users tab. You’ll see all of the users that have used your app there. You can select whichever user(s) you want to assign to an app role and then click on the Assign button at the bottom. The trick of course is now adding users to these groups (i.e. roles) when the user is not part of your Azure AD tenant.
Store User and Role Relationships for the Application
This is another step that requires no rocket science. I’m not going to cover any implementation details here because it’s likely going to be different for every company in terms of how and what they implement it. The goal here is simple though – you need something – like a SQL database – to store the user identifier and the role(s) that user is associated with.
In terms of identifying your user in code so you can look up what roles are associated with it, I use the UPN. When you’ve authenticated to Azure AD you’ll find the UPN is in the ClaimsPrincipal.Current.Identity.Name property. To be fair, it’s possible for a user’s UPN to change, so if you find that to be a scenario that concerns you then you should use something else. As an alternative, I typically have code in my Startup.Auth.cs class that creates an AuthenticationResult as part of registering the application in the user’s Azure AD tenant after consent has been given. You can always go back and look at doing something with the AuthenticationResult’s UserInfo.UniqueId property, which is basically just a GUID that identifies the user in Azure AD and never changes, even if the UPN does.
Now that you have the information stored, when we build our MVC ActionFilter we’ll pull this data out to plug into application roles.
Add Role Attributes to Views, Etc.
This is where the power of the ASP.NET permissions model and roles really comes into play. In step 1 after you follow the steps in Dushyant’s blog, you are basically telling ASP.NET that anything in the claim type “roles” should be considered a role claim. As such, you can start using it the way you would any other kind of ASP.NET role checking. Here are a couple of examples:
- As a PrincipalPermission demand – you can add a permission demand to a view in your controller (like an ActionResult) with a simple attribute, like this: [PrincipalPermission(SecurityAction.Demand, Role = “MyAppAdmin”)]. So if someone tries to access a view and they have not been added to the MyAppAdmin role (i.e. if they don’t have a “roles” claim with that value), then they will get denied access to that view.
- Using IsInRole – you can use the standard ASP.NET method to determine if a user is in a particular role and then based on that make options available, hide UI, redirect the user, etc. Here’s an example of that: if (System.Security.Claims.ClaimsPrincipal.Current.IsInRole(“MyAppAdmin “))… In this case if the user has the “roles” claim with a value of MyAppAdmin then I can do whatever – enabling a feature, etc.
Those are just a couple of simple and the most common examples, but as I said, anything you can do with ASP.NET roles, you can now do with this YARBACS.
Use a Custom FilterAction to Populate Roles for Users
This is really where the magic happens – we look at an individual and determine what roles we want to assign to it. To start with, we create a new class and have it inherit from ActionFilterAttribute. Then we override the OnActionExecuting event and plug in our code to look for the roles for the user and assign them.
Here’s what the skeleton of the class looks like:
public class Office365MonSecurity : ActionFilterAttribute
public override void OnActionExecuting(ActionExecutingContext filterContext)
//code here to assign roles
Within the override, we plug in our code. To begin with, this code isn’t going to do any good unless the user is already authenticated. If they haven’t then I have no way to identify them and look up the roles (short of something like a cookie, which would be a really bad approach for many reasons). So first I check to make sure the request is authenticated:
//assign roles if user is authenticated
Inside this block then, I can do my look up and assign roles because I know the user has been authenticated and I can identify him. So here’s a little pseudo code to demonstrate:
SqlHelper sql = new SqlHelper();
List<string> roles = sql.GetRoles(ClaimsPrincipal.Current.Identity.Name);
foreach(string role in roles)
Claim roleClaim = new Claim(“roles”, role);
So that’s a pretty good and generic example of how you can implement it. Also, unlike application roles that you define through the UI in the Azure management portal, you can assign multiple roles to a user this way. It works because they just show up as role claims. Here’s an example of some simple code to demonstrate:
//assign everyone to the app reader role
Claim readerClaim = new Claim(“roles”, “MyAppReader”);
//add a role I just made up
Claim madeUpClaim = new Claim(“roles”, “BlazerFan”);
This code actually demonstrates a couple of interesting things. First, as I pointed out above, it adds multiple roles to the user. Second, it demonstrates using a completely “made up” role. What I mean by that is that the “BlazerFan” role does not exist in the list of application roles in Azure AD for my app. Instead I just created it on the fly, but again it all works because it’s added as a standard Claim of type “roles”, which is what we’ve configured our application to use as a role claim. Here’s a partial snippet of what my claims collection looks like after running through this demo code:
To actually use the FilterAction, I just need to add it as an attribute on the controller(s) where I’m going to use it. Here’s an example – Office365MonSecurity is the class name of my FilterAction:
public class SignupController : RoutingControllerBase
There you have it. All of the pieces to implement your own RBAC solution when the current service offering is not necessarily adequate for your scenario. It’s pretty simple to implement and maintain, and should support a wide range of scenarios.