Need Quality Code? Get Silver Backed

Custom Role Providers with Windows Authentication - Part 2 (RoleProvider and Integration)

16thJun

0

by Gary H

We spend our time searching for security and hate it when we get it.

John Steinbeck

In Part 1 of our series we built the infrastructure for our new Role Provider. This gives us the foundation that we will build upon to support integration with windows authentication.

The Role Provider

Creating a Custom Role Provider is relatively straightforward. We need to create a new class which inherits from System.Web.Security.RoleProvider and then implement at least the two methods IsUserInRole and GetRolesForUser. All other methods can be left throwing a NotImplementedException.

public class CustomRoleProvider : RoleProvider
{
	private IUserRepository Users { get; set; }
	private IRoleRepository Roles { get; set; }

	public PipelineRoleProvider()
	{
		Users = IoC.IoCStrategy.Get<IUserRepository>();
		Roles = IoC.IoCStrategy.Get<IRoleRepository>();
	}

	public override bool IsUserInRole(string username, string roleName)
	{
		var user = Users.Get(username);

		return user.Roles != null 
			&& user.Roles.Count > 0 
			&& user.Roles.Any(r => 
						   r.Name == Enums.Roles.Developer.ToString() 
						|| r.Name == Enums.Roles.Administrator.ToString() 
						|| r.Name == roleName);
	}

	public override string[] GetRolesForUser(string username)
	{
		var user = Users.Get(username);

		if (user == null || user.Roles == null)
		{
			
			return new string[0];
		}

		return user.Roles.Select(r => r.Name).ToArray();
	}
}

In our example provider above we have added a feature whereby the IsUserInRole method will return true if the user is a Developer or Administrator. This will give members of these roles full access to any area of the site. If you do not want to support this then remove these overrides. You will note we also refer to an Enum. This is simply to remove magic strings from the system. Our enum implementation looks like:

public enum Roles
{
	Developer,
	Administrator,
	Approver,
	Reviewer,
	User
}

Update Web.Config

Step two is to include your Role Provider in your Web.Config. This entails adding a RoleManager element to the system.web element. This should look like:

<roleManager cacheRolesInCookie="true" 
			defaultProvider="CustomRoleProvider" 
			enabled="true">
	<providers>
		<clear />
		<add name="CustomRoleProvider" 
			type="Namespace.For.CustomRoleProvider, Your.Assembly.Name" />
	</providers>
</roleManager>

That's it! Your role provider will now automatically be plumbed into your application and you can start locking down your MVC actions using an Authorize attribute.

[Authorize(Roles="Administrator, Development")]
public ActionResult Test()
{
	return View();
}

Making it More Useful

As was touched on above, we are not fans of Magic Strings. In fact, what we would really like to do is to be able to say "Hey, this action is usable by anyone in this list of roles, where each role is an enum". To do this we need to create our own attribute which will extend the existing Authorize attribute.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, 
	nherited = true, AllowMultiple = true)]
public class RoleAuthorizeAttribute : AuthorizeAttribute
{
	private static readonly string[] RolesWhoAlwaysHaveAccess = 
		new [] { Enums.Roles.Administrator.ToString(), 
				 Enums.Roles.Developer.ToString() };


	public RoleAuthorizeAttribute(params object[] roles)
	{
		if (roles.Any(r => r.GetType().BaseType != typeof (Enum)))
		{
			throw new ArgumentException(
				"The roles parameter may only contain enums", 
				"roles");
		}

		var temp = roles.Select(r => 
				AddSpacesToSentence(Enum.GetName(r.GetType(), r)))
			.ToList();

		temp.AddRange(RolesWhoAlwaysHaveAccess);

		Roles = string.Join(",", temp);
	}

	private string AddSpacesToSentence(string text)
	{
		if (string.IsNullOrWhiteSpace(text))
		{
			return string.Empty;
		}

		var newText = new StringBuilder(text.Length * 2);
		newText.Append(text[0]);
		for (int i = 1; i < text.Length; i++)
		{
			if (char.IsUpper(text[i]) && text[i - 1] != ' ')
			{
				newText.Append(' ');
			}

			newText.Append(text[i]);
		}
		return newText.ToString();
	}
}

As you can see, in our custom authorize attibute we override the passed in roles list so we always allow admin and development. We also split on capitals in our enum names and add spaces inbetween. This allows us to take an enum entry like FinanceController and have the role named Finance Controller in the database for readability. We can use this new attribute like:

[RoleAuthorize(Roles.Approver, Roles.Admin)]
public ActionResult AwaitingApproval()
{
	// ...
}

No more magic strings! It is also worth noting that this attribute can be applied on controllers as well as actions and that doing so is Additive. There is an excellent post on Stack Overflow that details exactly how the different styles and locations of Authorize interact.

AD Integration

So we have a working RoleProvider and we've eliminated the scourge of magic strings form our controllers. The final step is to add the AD integration. When a controller checks if a user has access to a controller it correctly utilises the RoleProvider to do so. If however we want to check roles ourselves (say to hide functionality for admins in a view) we would normally use:

	User.IsInRole("RoleName");

Unfortunately for us this does NOT use the RoleProvider, it will instead get the current IPrincipal assigned to the User and use it's IsInRole method. When we use Windows Authentication this IPrincipal is a WindowsIdentity and so the role check actually looks to see if the User is a member of an AD group with the same name! Fortunately, we can bypass this. To do so we need to create our own IPrincipal that extends a WindowsIdentity.

public class CustomPrincipal : WindowsPrincipal
{
	public CustomPrincipal(WindowsIdentity source, IUser baseUser) : 
							base(source)
	{
		PipelineUser = baseUser;
	}

	public override bool IsInRole(string role)
	{
		return (base.IsInRole(role) ||
				(
					User != null &&
					User.Roles != null &&
					User.Roles.Count > 0 &&
					User.Roles.Any(r =>
						r.Name == Enums.Roles.Administrator.ToString() 
						|| r.Name == Enums.Roles.Developer.ToString() 
						|| r.Name == role)
				));
	}

	public IUser User { get; protected set; }
}

Once again we have overrides for our blessed roles but the implementation is straightforward. We take an IUser in the constructor which we look up against in our IsInRole method. We also check the base IsInRole in case we need to support legacy permissions elsewhere in our site. To assign this principal we need to hook the Application_PostAuthenticateRequest method in our Global.asax.

protected void Application_PostAuthenticateRequest(object sender, 
														EventArgs e)
{
	if (User == null)
	{
		return;
	}

	var users = IoC.IoCStrategy.Get<IUserRepository>();
	var u = users.Get(User.Identity.Name);
	HttpContext.Current.User = 
			new CustomPrincipal((WindowsIdentity)User.Identity, u);
}

And we're done! Our roles are now correctly checked anywhere that we use the standard IsInRole calls built into the framework. The final step is to remove the manual task of creating users in your database.

Finishing Touches - Automated Mapping

Manual data entry is dull. Wouldn't it be far better if you could somehow map your existing site users into your new User/Role structure without having to do any real work yourself? Fortunately with AD integration, we can!

Important Note: Your Application Pool Identity needs access rights to read the AD information. You may need to create an actual Identity to do this rather than using AppPoolId

In the Session_Start method of our Global.asax we have already got a user, authenticated them and are ready for the one time session setup. This is the perfect time for us to map them into our new structure. To do so we need to create our user if they do not already exist, do a lookup against the AD Domain to get their groups and then map those groups onto our own Roles.

protected void Session_Start(object sender, EventArgs e)
{
	if (Context.User != null)
	{
		var users = IoC.IoCStrategy.Get<IUserRepository>();
		var usr = users.Get(Context.User.Identity.Name);
		if (usr == null)
		{
			usr = users.Create();
			usr.ADName = Context.User.Identity.Name;
			usr.Id = users.Save(usr);
			MapUserADDetails(usr, users);
		}			
	}
}

private static void MapUserADDetails(IUser user, IUserRepository users)
{
	using (HostingEnvironment.Impersonate())
	using (var domain = new PrincipalContext(ContextType.Domain, 
												"YOURDOMAIN"))
	using (var usr = UserPrincipal.FindByIdentity(domain, user.ADName))
	{
		var roles = IoC.IoCStrategy.Get<IRoleRepository>();

		if (usr == null)
		{
			return;
		}

		user.DisplayName = usr.DisplayName;
		user.EMailAddress = usr.EmailAddress;

		using (var groups = usr.GetAuthorizationGroups())
		{
			foreach (var n in groups
								.OfType<GroupPrincipal>()
								.Select(p => p.Name)
								.ToList())
			{
				switch (n)
				{
					case "Administrators":
						AddRoleToUser(user, "Administrator", roles);
						break;

					case "Development":
						AddRoleToUser(user, "Development", roles);
						break;

					case "Review Admin":
						AddRoleToUser(user, "Reviewer", roles);
						break;

					case "YourApp Browsers":
						AddRoleToUser(user, "User", roles);
						break;
				}
			}
		}
		users.Save(user);
	}
}

private static void AddRoleToUser(IUser user, string role, 
									IRoleRepository roles)
{
	if (user.Roles.All(r => r.Name != role))
	{
		user.Roles.Add(roles.GetByName(role));
	}
}

You can map the names of the AD groups onto anything that you want. We also make use of the EmailAddress field so that we can mail the user in the future and we also pull their preferred display name to make presenting their name on the site a little cleaner. The IUser lookup is an excellent candidate for caching to save some DB lookups on every page load. Do not however cache the whole principal. In IIS7 in integrated mode the WindowsIdentity token is closed after every request meaning your cached user token will not remain valid.

Wrapping it Up

Over the course of these articles we have looked at how we can apply the repository pattern in anger to allow us to abstract away our data layer and then leveraged this to create our own custom role provider. We have also looked at how we can integrate this with AD allowing custom roles in our database or AD groups to guide our authentication strategy.

If you have any comments on this series please feel free to leave a comment or use the contact form. We would love to hear from you.

Part 1 - Repositories

C# , MVC , Patterns

Comments are Locked for this Post