User authentication against AD and Roles based authorization in ASP.NET MVC – Part I
It is a common scenario in an enterprise application when as per the requirement we have to authenticate the users against their AD account and authorize them against application specific roles. Especially for the applications that are intranet facing, the customer generally opts for utilizing the existing infrastructure to support Single Sign On kind of model.
For authorization, based on where/how the roles will be stored we have following options to choose:
1. The roles can be maintained using Groups in AD. (If a role has a purpose of only authorization and it is not dependent on any other application specific data)
OR
2. The roles can be maintained in the DB (For example – A Supervisor can only work on requests for HR LOB or A Development Manager can only approve requests for IT LOB etc…)
In either cases, we have to write specific business logic to wire the relationship between user and roles.
Let’s start implementing option 1:
Step 1: In the web.config file, we have to enable Windows authentication:
<authentication mode="Windows">
</authentication>
Step 2:
Create a customized version of Authorize Attribute which will check if the logged in user belongs to an AD group. For simplicity I am maintaining the roles in web.config via a appsettings key. A sample code example is as below:
namespace MyApplication
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Configuration;
using System.DirectoryServices;
/// <summary>
/// Pass app settings key value in 'Roles' parameter. this will retrieve the actual users value from configuration file.(web.config)
/// </summary>
public sealed class ADGroupAuthorizeAttribute : AuthorizeAttribute
{
public ADGroupAuthorizeAttribute()
: base()
{
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
#if DEBUG
return true;
#endif
////Get the allowed groups from web.config - This can be changed to suit specific needs.
string groupPath = ConfigurationManager.AppSettings[this.Roles];
////This is the currently logged in user
string userId = httpContext.User.Identity.Name;
////This code assumes that you have a Utilities class with ADDomain, LdapConnectionString, LdapAccessUserName and LdapAccessUserPassword
var addomain = string.IsNullOrEmpty(Utilities.ADDomain) ? "mydomain\\" : Utilities.ADDomain.ToLower();
if (string.IsNullOrEmpty(groupPath) || string.IsNullOrEmpty(userId))
{
return false;
}
if (userId.ToLower().Contains(addomain))
{
userId = userId.ToLower().Replace(addomain, string.Empty);
}
DirectoryEntry root = new DirectoryEntry(Utilities.LdapConnectionString, Utilities.LdapAccessUserName, Utilities.LdapAccessUserPassword);
DirectorySearcher search = new DirectorySearcher(root);
List<object> members = new List<object>();
search.SearchScope = SearchScope.Subtree;
search.Filter = "(&(objectClass=group)(cn=" + groupPath + "))";
var result = search.FindOne();
if (result != null)
{
var userName = this.Locateuser(userId);
if (!string.IsNullOrEmpty(userName))
{
var admembers = result.Properties["member"];
if (admembers != null && admembers.Count > 0)
{
for (int i = 0; i < admembers.Count; i++)
{
var admember = admembers[i];
var splitted = admember.ToString().Split(new[] { ',' });
var aduser = splitted[0] + splitted[1];
aduser = aduser.Split(new[] { '=' })[1].Replace("\\", ",");
if (aduser.ToLower() == userName.ToLower())
{
return true;
}
else
{
continue;
}
}
}
else
{
return false;
}
}
else
{
return false;
}
}
return false;
}
private string Locateuser(string userId)
{
string account = userId.Replace(Utilities.ADDomain, string.Empty);
try
{
DirectorySearcher search = new DirectorySearcher(new DirectoryEntry(Utilities.LdapConnectionString, Utilities.LdapAccessUserName, Utilities.LdapAccessUserPassword));
search.Filter = "(SAMAccountName=" + account + ")";
search.PropertiesToLoad.Add("displayName");
SearchResult result = search.FindOne();
if (result != null)
{
return result.Properties["displayname"][0].ToString().ToUpper();
}
else
{
return string.Empty;
}
}
catch (Exception ex)
{
string debug = ex.Message;
return string.Empty;
}
}
}
}
Step 3: [ADGroupAuthorizeAttribute ()] for each of the action where you want to authorize based on AD Groups.
In the next post, I will focus on creating a custom Role Provider and a custom authorize attribute which can be used to redirect to an error page when the user is not authenticated / authorized.
Comments
Anonymous
December 13, 2010
Very useful post. This was my next step to implement on my intranet web application. Is there any chance to post the Utilities class too. Or if there is the reference to UtililiesAnonymous
December 13, 2010
Utilities is nothing but a class with four static string properties. Either you can read them from appsettings or directly assign to the properties. I would prefer to read them from config for portability.Anonymous
December 14, 2010
your code never reach to this point after I add [[ADGroupAuthorizeAttribute ()] to one of my Controller Action. Can you please verify that your code is not broken. string groupPath = ConfigurationManager.AppSettings[this.Roles];Anonymous
December 14, 2010
Your code never reach to this point after I add [[ADGroupAuthorizeAttribute ()] to one of my Controller Action. Can you please verify that your code is not broken. string groupPath = ConfigurationManager.AppSettings[this.Roles];Anonymous
December 14, 2010
The comment has been removedAnonymous
December 14, 2010
I intentionally put the if# DEBUG because once the code is working, you would not like to wait for the AD auth to be done for each app run in DEV. The COMException please check : stackoverflow.com/.../why-does-authenticating-against-ldap-with-directoryentry-intermittently-throw-comAnonymous
December 15, 2010
Is it possible to have source code for your sample. Since I'm not sure about some settings you are using from web.config. ThanksAnonymous
December 15, 2010
Finally I am able to get to this point. Now that if the Action is not authorized it prompt the Windows SecurityLogin window for credentials. I was not expecting this to happen rather go Error page. I was thinking your next post will be focused to "redirect to error page in case of unauthorized/autheticated". Can you please help me by provinding this post.Anonymous
December 15, 2010
Finally I am able to get to this point. Now that if the Action is not unauthorized it prompt the Windows SecurityLogin window for credentials. I was not expecting this to happen rather go Error page. I was thinking your next post will be focused to "redirect to error page in case of unauthorized/autheticated". Can you please help me by provinding this post.Anonymous
December 16, 2010
What I found that If i need to filter the content result and RouteData to different page on UnAuthorize then we can override the OnAuthorization method. This helped me to redirect in case of unauthorization. public override void OnAuthorization(AuthorizationContext filterContext) { base.OnAuthorization(filterContext); if (filterContext.Result is HttpUnauthorizedResult) { filterContext.Result = new RedirectToRouteResult( new System.Web.Routing.RouteValueDictionary { { "langCode", filterContext.RouteData.Values[ "langCode" ] }, { "controller", "Error" }, { "action", "Index" }, { "ReturnUrl", filterContext.HttpContext.Request.RawUrl } }); }Anonymous
December 16, 2010
Thanks Manash. I was busy with other post on ACT. Good that you found a solution.Anonymous
October 23, 2012
If Windows auth is used why not simply use Windows authorization: In ASP.NET/MVC: (User.IsInRole("Domain\AD GroupName")) or [Authorize(Roles = "Domain\AD GroupName")] Mthod Name() Why the need to searching AD groups for the AD groups teh user belongs to?