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.

You may also like...

20 Responses

  1. Anthony says:

    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?

  2. 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.

  3. Vince says:

    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.

  4. 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.

  5. Gonzalo says:

    Thanks for this article! It’s really helpfull.
    Maybe you can help me with this: I’m using your code in a project, I will have users accesing from outside the domain and others from PCs within the domain.
    For the PCs in the domain, I want to skip the login process and get the current user to automatically log him in. How can I do this?

  6. If you want to have users auto login, you can use Windows authentication instead of Forms authentication. In the web config, use . This will set the authenticated user to the user logged into the client computer. This works well for sites such as a company intranet. If the user cannot be authenticated, then a login box will ask them for a user/password.

    If you want to use both authentication methods at the same time, I’m not sure that is possible.

  7. JeffKirby says:

    Hey Chris,

    I’ve translated this to C# and it all looks to be working, right up until I try and check the roles ( roles.getRolesForUser ) or look in the debugger on the User object in a page ( in this case the LogOnUserControl ). The Roles are empty. The Global.asax method is however populating the Identity correctly according to the debugger. I have Role Manager on ( should I not? ) but thats about all I’ve set ( using MVC2 default kit under VS2010 )

  8. @JeffKirby
    I’ve not used the roles.getRolesForUser method, but instead used the following attribute for checking the role within a controller action:

    < Authorize(Roles:="Group1,Group2,Group3") > _

    …and used the following for checking a role within a user control:

    < % If (Context.User.IsInRole("Group1")) Then %>

    This code was setup on MVC1.0 using VS2008. I haven’t tried running this on the upgraded versions, so I’m not sure if something will break by using them, but would assume that it would work.

    If you need to use the getRolesForUser function for some reason, you might download the MVC source code and see if you can find what exactly that function is doing.

  9. JeffKirby says:

    @Chris
    The reason I mentioned the getRolesForUser is that the Authorize(Roles: ) thing doesn’t work and I used it to see what was there. Maybe that was the wrong path to go down.
    I can make it work,by creating Roles in the role manager, and then in the global.asx method, add the user to the role like this

    string[] groups = authTicket.UserData.ToString().Split(‘|’);

    foreach (string group in groups)
    {
    if (Roles.RoleExists(group))
    {
    if (!User.IsInRole(group))
    {
    Roles.AddUserToRole(authTicket.Name, group);
    }
    }
    }

    Its quick and dirty and works fine :). Then everything works.

  10. JeffKirby says:

    I should also mention that negates the need to mess with the user context at all.

  11. @JeffKirby
    The Authorize attribute should work because the code in the global.asax is creating a new GenericPrincipal using the list of groups stored in the cookie and then passing it into the user context.

    The group names should match those you create in Active Directory. I have noticed that the built-in groups do not show in this list. I have had to use my own custom groups.

    If there are spaces in the group names, that may be the problem. Maybe the IsInRole trims the group passed into it and the Authorize atribute does not? Not sure.

    If you found another way that you prefer, that’s great! The nice thing about programming is there’s about a million ways to do something and you get to be creative in coming up with your answer.

  12. Lyle says:

    Hey Chris! Good article, nice site. If there is a way you could let JeffKirby know that I would love to see his C# code I’d really appreciate it!

  13. Arnold Smith says:

    Nice article. Thanks!

  14. Hugh says:

    Visual Basic, really?

  15. @Hugh
    Yep, sometimes you work in the languages dictated by your company and the others you work with. We have since moved on to use C# and it is much nicer, but I don’t look down on those who use VB because sometimes you don’t have a choice.

  16. Carlos says:

    How to add a view “login” with “razor” and what class would be:

    User, or FormsAut UserRepository ..

    Thanks
    GREETINGS FROM MÉXICO

  17. @Carlos
    This example was written with MVC 1, but it should work still in MVC 3 using the Razor views. The view shouldn’t matter as far as the example goes, your field names in HTML will need to match the parameter names being passed into the Logon POST method…

    userName, password, rememberMe, and returnUrl.

    …if your field names match the parameter names (if you manually type the HTML fields), MVC will bind them for you. You could also create the Razor view with a class like User and then pass a user object into your Logon POST method, but you would have to add the additional properties for returnUrl and rememberMe.

  18. Daniel Ma says:

    Sorry but i’m missing something,
    _userRepository = New UserRepository(ConfigurationManager.AppSettings(“LDAPServer”)
    How to set LDAPServer config in web.config?

  19. @Daniel Ma:
    In the AppSettings of the Web.Config, you need to add a key/value pair for that setting…something like:

    <appSettings>
    <add key=”LDAPServer” value=”yourdomain.com” />
    </appSettings>

  1. October 27, 2009

    […] to VoteASP.NET MVC Using Forms Authentication With LDAP (10/23/2009)Friday, October 23, 2009 from Chris JacksonIf you are using ASP.NET MVC and you want to authenticate […]

Leave a Reply

Your email address will not be published. Required fields are marked *