Introduction
Usually you just need to use:
- Integrated Windows authentication (uses Active Directory)
- OR Forms authentication
This is for the RARE case where you want to use the Active Directory provider in Forms Authentication.
Now implementing FormsAuthentication
with the AD Provider isn't too bad, but .NET does not end up populating the Roles for the current user, so you can't use User.IsInRole
.
The way around this is to create your own custom role provider.
*********************************************************************
This is for ASP.NET 2.0. Not sure of it's needed in 3+ and it certainly won't work in 1.0. *********************************************************************
Background
You'll need a solid understanding of Directory Services to get much out of this, since none of the code is commented.
Code
Web.Config
Ok, so the part that is custom here is the role manager. The type attribute should be the fully qualified name of your class. I just threw mine in the app_code directory, so it is just the class name. The admin user/password should be an active directory user that has access to do searches on active directory.
What you want to look at here is the roleManager
. If you haven't done Forms Auth with AD, you might also want to look at the Connection String and the Membership Provider areas.
="1.0"
<configuration>
<connectionStrings>
<add name="ADConnectionString"
connectionString="LDAP://yourhost.com/DC=host,DC=com" />
</connectionStrings>
<system.web>
<roleManager enabled="true" cacheRolesInCookie="true"
defaultProvider="MyADRoleProvider"
cookieName=".ASPXROLES" cookiePath="/" cookieTimeout="30"
cookieRequireSSL="false"
cookieSlidingExpiration="true" createPersistentCookie="false"
cookieProtection="All">
<providers>
<add name="MyADRoleProvider"
type="CustomActiveDirectoryRoleProvider"
connectionStringName="ADConnectionString"
applicationName="/"
connectionUsername="adminUser"
connectionPassword="adminPassword" />
</providers>
</roleManager>
<compilation debug="true">
<assemblies>
<add assembly="System.DirectoryServices,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A"/>
</assemblies>
</compilation>
<membership defaultProvider="MembershipADProvider">
<providers>
<add name="MembershipADProvider"
type="System.Web.Security.ActiveDirectoryMembershipProvider,
System.Web, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a"
connectionStringName="ADConnectionString"
connectionUsername="adminUsername"
connectionPassword="adminPassword"
attributeMapUsername="sAMAccountName" />
</providers>
</membership>
<authentication mode="Forms">
<forms name=".ASPNET"/>
</authentication>
<authorization>
<deny users="?"/>
</authorization>
</system.web>
</configuration>
Here's the "impressive" Login.aspx page:
<asp:Login ID="Login1" runat="server" DisplayRememberMe="false">
</asp:Login>
Alternatively, if you were trying to create a pass-through Login page, you can validate in this manner:
string username, password;
if (Membership.ValidateUser(username, password)) {
FormsAuthentication.RedirectFromLoginPage(username, false);
}
Next we'll see the provider. There are only a couple of methods you have to provide to get the User.IsInRole
function to work. If you want to implement more, though, that's fine. Here's the basic required information for CustomActiveDirectoryRoleProvider.cs:
Note, this is handling nested membership, and it has to keep track of the groups it's done so it doesn't duplicate any.
using System;
using System.Collections;
using System.Collections.Generic;
using System.DirectoryServices;
using System.Web.Configuration;
using System.Web.Security;
public class CustomActiveDirectoryRoleProvider : RoleProvider
{
string _connectionString = string.Empty;
string _applicationName = string.Empty;
string _userName = string.Empty;
string _userPassword = string.Empty;
public override void Initialize
(string name, System.Collections.Specialized.NameValueCollection config) {
_connectionString = config["connectionStringName"];
if (!string.IsNullOrEmpty(config["applicationName"]))
_applicationName = config["applicationName"];
if (!string.IsNullOrEmpty(config["connectionUsername"]))
_userName = config["connectionUsername"];
if (!string.IsNullOrEmpty(config["connectionPassword"]))
_userPassword = config["connectionPassword"];
base.Initialize(name, config);
}
public override string ApplicationName {
get {
return _applicationName;
}
set {
_applicationName = value;
}
}
public override string[] GetRolesForUser(string userName) {
userName = RemoveADGroup(userName);
return GetUserRoles(userName);
}
string RemoveADGroup(string name) {
string[] ary = name.Split(new char[] { '\\' });
return ary[ary.Length - 1];
}
string[] GetUserRoles(string userName) {
DirectoryEntry obEntry = new DirectoryEntry(
WebConfigurationManager.ConnectionStrings
[_connectionString].ConnectionString,
_userName, _userPassword);
DirectorySearcher srch = new DirectorySearcher
(obEntry, "(sAMAccountName=" + userName + ")");
SearchResult res = srch.FindOne();
Dictionary<string, string> dictionary = new Dictionary<string, string>();
if (null != res) {
DirectoryEntry obUser = new DirectoryEntry
(res.Path, _userName, _userPassword);
string rootPath = WebConfigurationManager.ConnectionStrings
[_connectionString].ConnectionString;
rootPath = rootPath.Substring(0,rootPath.LastIndexOf(@"/") + 1);
GetMemberships(obUser, dictionary, rootPath);
}
string[] ary = new string[dictionary.Count];
dictionary.Values.CopyTo(ary, 0);
return ary;
}
void GetMemberships(DirectoryEntry entry,
Dictionary<string, string> dictionary, string rootPath) {
List<DirectoryEntry> childrenToCheck = new List<DirectoryEntry>();
PropertyValueCollection children = entry.Properties["memberOf"];
foreach (string childDN in children) {
if (!dictionary.ContainsKey(childDN)) {
DirectoryEntry obGpEntry =
new DirectoryEntry(rootPath + childDN, _userName, _userPassword);
string groupName =
obGpEntry.Properties["sAMAccountName"].Value.ToString();
dictionary.Add(childDN, groupName);
childrenToCheck.Add(obGpEntry);
}
}
foreach (DirectoryEntry child in childrenToCheck) {
GetMemberships(child, dictionary, rootPath);
}
}
public override void AddUsersToRoles(string[] usernames, string[] roleNames) {
throw new Exception("The method or operation is not implemented.");
}
public override void CreateRole(string roleName) {
throw new Exception("The method or operation is not implemented.");
}
public override bool DeleteRole(string roleName, bool throwOnPopulatedRole) {
throw new Exception("The method or operation is not implemented.");
}
public override string[] FindUsersInRole(string roleName, string usernameToMatch) {
throw new Exception("The method or operation is not implemented.");
}
public override string[] GetAllRoles() {
throw new Exception("The method or operation is not implemented.");
}
public override string[] GetUsersInRole(string roleName) {
throw new Exception("The method or operation is not implemented.");
}
public override bool IsUserInRole(string username, string roleName) {
throw new Exception("The method or operation is not implemented.");
}
public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames) {
throw new Exception("The method or operation is not implemented.");
}
public override bool RoleExists(string roleName) {
throw new Exception("The method or operation is not implemented.");
}
}
Points of Interest
Here are a couple of the other methods that I implemented before I realized that I didn't need to use them unless I was going to be using the Roles
methods (i.e.: Roles.GetUsersInRole("blah\somerole")
). They do work... you just don't need them for the normal Page.User.IsInRole("blah\basdfasf");
public override string[] GetUsersInRole(string roleName) {
return GetGroupMembers(RemoveADGroup(roleName));
}
public override bool IsUserInRole(string username, string roleName) {
string[] ary = GetRolesForUser(username);
foreach (string s in ary) {
if (roleName.ToLower() == s.ToLower())
return true;
}
return false;
}
string[] GetGroupMembers(string groupName) {
DirectoryEntry obEntry = new DirectoryEntry(
WebConfigurationManager.ConnectionStrings[_connectionString].ConnectionString,
_userName, _userPassword);
DirectorySearcher srch = new DirectorySearcher
(obEntry, "(sAMAccountName=" + groupName + ")");
SearchResult res = srch.FindOne();
Dictionary<string, string> groups = new Dictionary<string, string>();
Dictionary<string, string> members = new Dictionary<string, string>();
if (null != res) {
DirectoryEntry entry = new DirectoryEntry(res.Path, _userName, _userPassword);
GetMembers(entry, groups, members);
}
string[] ary = new string[members.Count];
members.Values.CopyTo(ary, 0);
return ary;
}
void GetMembers(DirectoryEntry entry,
Dictionary<string, string> groups, Dictionary<string, string> members) {
List<DirectoryEntry> childrenToCheck = new List<DirectoryEntry>();
object children = entry.Invoke("Members", null);
foreach (object childObject in (IEnumerable)children) {
DirectoryEntry child = new DirectoryEntry(childObject);
string type = getEntryType(child);
if (type == "G" && !groups.ContainsKey(child.Path)) {
childrenToCheck.Add(child);
} else if (type == "P" && !members.ContainsKey(child.Path)) {
members.Add(child.Path, child.Properties
["sAMAccountName"].Value.ToString());
}
}
foreach (DirectoryEntry child in childrenToCheck) {
GetMembers(child, groups, members);
}
}
string getEntryType(DirectoryEntry inEntry) {
if (inEntry.Properties.Contains("objectCategory")) {
string fullValue = inEntry.Properties["objectCategory"].Value.ToString();
if (fullValue.StartsWith("CN=Group"))
return "G";
else if (fullValue.StartsWith("CN=Person"))
return "P";
}
return "";
}
References
- This site provided me insight on the custom provider, although I changed the access methods to handle nested groups.
- This link gives you the basics of Forms Authentication with Active Directory (MSDN).