Web Design, Programming, Tutorials
ASP.NET MVC Using Forms Authentication With LDAP
If you are using ASP.NET MVC and you want to authenticate your users against Active Directory using LDAP, you need to do a little work to get everything set up. It is pretty easy to authenticate users with Active Directory using the <Authorize()> attribute, but I ran into some problems when I wanted to authorize a user based on a Windows Group.
Here are the steps I took to be able to authenticate active directory users and authorize their use of actions based on being members to user groups.
Web.Config
Find the authentication tag and change it to the following:
<authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" timeout="2880" /> </authentication>
The LoginUrl points to the Controller/Action where the login function is.
UserRepository
This is the class object that does the communication with Active Directory via LDAP. The project where this class resides should also add a reference to the System.DirectoryServices DLL.
Imports System.DirectoryServices
Public Class UserRepository
Private _server As String
Public Sub New(ByVal server As String)
_server = server
End Sub
Public Function GetUser(ByVal userName As String) As Data.User
Dim root As DirectoryEntry = New DirectoryEntry("LDAP://" + _server)
Dim search As DirectorySearcher = New DirectorySearcher(root)
search.SearchScope = SearchScope.Subtree
search.Filter = "(sAMAccountName=" + userName.Substring(userName.IndexOf("\") + 1) + ")"
Dim results As SearchResultCollection = search.FindAll()
Return _GetUser(results(0).Path)
End Function
Public Function GetUserByFullName(ByVal fullName As String) As Data.User
Dim root As DirectoryEntry = New DirectoryEntry("LDAP://" + _server)
Dim search As DirectorySearcher = New DirectorySearcher(root)
search.SearchScope = SearchScope.Subtree
search.Filter = "(displayName=" + fullName + ")"
Dim results As SearchResultCollection = search.FindAll()
Return _GetUser(results(0).Path)
End Function
Public Function GetMembers(ByVal groupPath As String) As IQueryable(Of Data.User)
Dim root As DirectoryEntry = New DirectoryEntry("LDAP://" + _server)
Dim search As DirectorySearcher = New DirectorySearcher(root)
Dim members As List(Of Data.User) = New List(Of Data.User)()
search.SearchScope = SearchScope.Subtree
search.Filter = "(memberOf=" + groupPath + ")"
Dim results As SearchResultCollection = search.FindAll()
For Each result As SearchResult In results
members.Add(_GetUser(result.Path))
Next
root.Close()
Return members.AsQueryable
End Function
Public Function Authenticate(ByVal userName As String, ByVal password As String) As Data.User
Dim root As DirectoryEntry = New DirectoryEntry("LDAP://" + _server, userName, password)
Dim search As DirectorySearcher = New DirectorySearcher(root)
Dim user As User = Nothing
search.SearchScope = SearchScope.Subtree
search.Filter = "(sAMAccountName=" + userName + ")"
Dim results As SearchResultCollection = search.FindAll()
If (Not (results Is Nothing)) Then
user = _GetUser(results(0).Path)
End If
Return user
End Function
Private Function _GetUser(ByVal userPath As String) As Data.User
Dim entry As DirectoryEntry = New DirectoryEntry(userPath)
Dim user As Data.User = New Data.User()
user.UserName = entry.Properties("sAMAccountName").Value
user.FirstName = entry.Properties("givenname").Value
user.LastName = entry.Properties("sn").Value
user.Email = entry.Properties("mail").Value
For Each group In entry.Properties("memberOf")
user.Groups.Add(_GetGroup(group))
Next
entry.Close()
Return user
End Function
Private Function _GetGroup(ByVal path As String) As String
Dim value As String = ""
Dim index1 As Integer = path.IndexOf("=", 1)
Dim index2 As Integer = path.IndexOf(",", 1)
If (Not (index1 = -1)) Then
value = path.Substring((index1 + 1), (index2 - index1) - 1)
End If
Return value
End Function
End Class
User
This class hold the user information that we want to work with from Active Directory. Here I am storing the username, first and last names, email address, and the list of groups the user is a member of.
This class is in my Data project and that is why you will see Data.User in the UserRepository. If you want everything to be in one project, you could create the Data namespace around the User class. I did run into problems with MVC getting confused between my User class and a built in one if I did not specify the namespace. You could also simply rename the User class to something else.
Public Class User
Private _userName As String
Private _firstName As String
Private _lastName As String
Private _email As String
Private _groups As List(Of String)
Public Property UserName() As String
Get
Return _userName
End Get
Set(ByVal value As String)
_userName = value
End Set
End Property
Public Property FirstName() As String
Get
Return _firstName
End Get
Set(ByVal value As String)
_firstName = value
End Set
End Property
Public Property LastName() As String
Get
Return _lastName
End Get
Set(ByVal value As String)
_lastName = value
End Set
End Property
Public Property Email() As String
Get
Return _email
End Get
Set(ByVal value As String)
_email = value
End Set
End Property
Public Property Groups() As List(Of String)
Get
Return _groups
End Get
Set(ByVal value As List(Of String))
_groups = value
End Set
End Property
Public Sub New()
Me.UserName = ""
Me.FirstName = ""
Me.LastName = ""
Me.Email = ""
Me.Groups = New List(Of String)()
End Sub
Public Function GetFullName() As String
Dim s As System.Text.StringBuilder = New System.Text.StringBuilder()
If (Not (Me.FirstName = "")) Then
s.Append(Me.FirstName)
s.Append(" ")
End If
s.Append(Me.LastName)
Return s.ToString()
End Function
Public Function SerializeGroups() As String
Dim text As String = ""
For Each item In Groups
If (text = "") Then
text = item
Else
text = text + "|" + item
End If
Next
Return text
End Function
End Class
AccountController
Next is the controller that handles signing in and out of the application. I’ve simplified this controller code for the sake of simplicity. Under normal circumstances, I would have the controller communicate to a service class that then interacts with the repository. The service class could do validation on the login form before attempting to authenticate empty user and password strings.
Imports System.Globalization
Imports System.Security.Principal
Imports S3.Data
<HandleError()> _
Public Class AccountController
Inherits System.Web.Mvc.Controller
'data members.
Private _userRepository as UserRepository
Private _formsAuthentication As IFormsAuthentication
Public Sub New()
_userRepository = New UserRepository(ConfigurationManager.AppSettings("LDAPServer")
_formsAuthentication = New FormsAuthentication()
End Sub
'GET: /Account/LogOn
Public Function LogOn() As ActionResult
Return View("LogOn")
End Function
'POST: /Account/LogOn
<AcceptVerbs(HttpVerbs.Post)> _
Public Function LogOn( _
ByVal userName As String, _
ByVal password As String, _
ByVal rememberMe As Boolean, _
ByVal returnUrl As String) As ActionResult
Dim result As ActionResult = View("LogOn")
Dim user As User = _userRepository.Authenticate(userName, password)
If (Not (user Is Nothing)) Then
_formsAuthentication.SignIn(user, rememberMe)
If (returnUrl Is Nothing) Then
result = RedirectToAction("Index", "Home")
Else
result = Redirect(returnUrl)
End If
End If
Return result
End Function
Public Function LogOff() As ActionResult
_formsAuthentication.SignOut()
Return RedirectToAction("Index", "Home")
End Function
End Class
FormsAuthentication
The next class is the FormsAuthentication class. The main reason that I made this code a class of its own is for the sake of unit testing my project. During unit testing, the commands that deal with writing and clearing the cookie would error out. To get around this, I created an empty FormsAuthenticationTest class that does nothing for the SignIn and SignOut methods. Note: The example code does not show the interfaces. Check out the repository pattern and factory pattern to see how to setup services and repositories for unit testing.
After a user has been authenticated by the UserRepository, the controller passes the returned user into the SignIn method of the FormsAuthentication. This method then stores the user’s full name and serialized groups in a cookie. The list of groups is serialized by creating a string with each group name separated by a “|” character. This serialization takes place in the User object.
Imports System.Web.Security
Public Class FormsAuthentication
Public Sub SignIn(ByVal user As Data.User, ByVal createPersistentCookie As Boolean)
Dim authTicket As System.Web.Security.FormsAuthenticationTicket = _
New System.Web.Security.FormsAuthenticationTicket( _
1, _
user.GetFullName(), _
Now, _
Now.AddMinutes(60), _
createPersistentCookie, _
user.SerializeGroups())
Dim encryptedTicket As String = System.Web.Security.FormsAuthentication.Encrypt(authTicket)
Dim authCookie As HttpCookie = New HttpCookie( _
System.Web.Security.FormsAuthentication.FormsCookieName, _
encryptedTicket)
If (createPersistentCookie) Then
authCookie.Expires = authTicket.Expiration
End If
HttpContext.Current.Response.Cookies.Add(authCookie)
End Sub
Public Sub SignOut()
System.Web.Security.FormsAuthentication.SignOut()
End Sub
End Class
Global.asax
With the previous code, you should be able to use Forms Authentication to log into your application and authenticate a user against the Active Directory via LDAP. The <Authorize()> attribute will allow you to secure your controller or actions. There’s just one problem. Now, if you want to use <Authorize(Roles:=”GroupName”), your user does not get authorization to the action or controller.
There’s just one change left that we need to make. ASP MVC makes the current logged in user available by accessing the Context.User. When you log into the application MVC is setting the user as authenticated, but no roles or groups have been added to that user. We need to modify the Global.asax file and use the AuthenticationRequest event.
The following code will fire each time an authentication request is triggered. The code will check if the authentication cookie exists and if it does, reads the user’s name and groups from it and stores them in the Context.User so MVC can access the groups.
Private Sub MvcApplication_AuthenticateRequest(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.AuthenticateRequest
Dim cookieName As String = System.Web.Security.FormsAuthentication.FormsCookieName
Dim authCookie As HttpCookie = Context.Request.Cookies(cookieName)
If (Not (authCookie Is Nothing)) Then
Dim authTicket As System.Web.Security.FormsAuthenticationTicket = Nothing
Try
authTicket = System.Web.Security.FormsAuthentication.Decrypt(authCookie.Value)
If (Not (authTicket Is Nothing)) Then
Dim groups As String() = authTicket.UserData.Split(New Char() {"|"})
Dim id As System.Security.Principal.GenericIdentity = _
New System.Security.Principal.GenericIdentity(authTicket.Name, "LdapAuthentication")
Dim principal As System.Security.Principal.GenericPrincipal = _
New System.Security.Principal.GenericPrincipal(id, groups)
Context.User = principal
End If
Catch ex As Exception
'Do nothing.
End Try
End If
End Sub
And that should be all you need to use FormsAuthentication with LDAP and group authorization.
about 3 months ago
How do you (can you?) store the Data.User class so that you can access the object in subsequent requests by the user? There is always the session object but is there a more elegant way to do it?
about 2 months ago
The FormsAuthentication class writes a cookie with the user’s name and list of groups. The code in the Global.asax file then loads the data from that cookie into the session’s current user for each request.
Then, if you want to use the Data.User object, you can do a lookup using the LDAP class on the user’s name read in from the cookie (which is now in the session’s current user).
There may be other ways to do this, but this was one way I found that worked with the attributes and Active Directory groups.
about 2 months ago
Is there a way to add useraccountcontrl flags? For example how would I be able to redirect users who have the password expired to another link where they can reset the password.
about 2 months ago
You could add the flag properties to your user object, then during the Authenticate method of the userRepository, check for the expired password and create a user object with the appropriate flags. In the account controller, you’d have to modify the if block after the user is authenticated… You would want to check if the user = nothing, redirect to signon screen with errors; user exists but flags are set, redirect to password change screen; otherwise user is valid and redirect to desired URL. I’m not sure on the specifics for checking LDAP for expired passwords. I’m sure you can find many resources about it on google.