Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implement Active Directory login in an existing ASP.NET MVC 4.6 web project

I have to change the existing (Windows) login for my ASP.NET MVC + Knockout application with Active Directory authentication. It consists of mvc controllers and webapi controllers. Both have to be authenticated.

I thought I would do this by changing to forms authentication and create a login page and when the users clicks login, query Active Directory with the System.DirectoryServices.DirectoryEntry. Then the other processes like change password, register etc. would also get a custom html page and do their actions via the System.DirectoryServices.DirectoryEntry on our Active Directory.

(that is, I could not find any other way that people would do it, and I did find some who would do it like this, and it sounds the same like previous forms authentications I've seen. In this case the user/passwords would not be in a database table but in Active Directory. Same idea, swap database table by active directory).

To see how this would be on a brandnew project, I created a new ASP.NET MVC project and choose 'work- or school acounts' (which says 'for applications that authenticate users with active directory) and choose 'on premise'. However, then I have to provide these items:

  • on-premises authority
  • app Id url

I don't know what to do with that. The only thing I have is an active directory url like ldap://etc..

Is this another/newer/better way of doing active directory login? Or the only right one (is forms authentication wrong?) or the wrong one?

I'm confused.

like image 874
Michel Avatar asked Aug 21 '18 15:08

Michel


People also ask

How does Windows Authentication work in MVC?

Authorizing Windows Users and Groups After you enable Windows authentication, you can use the [Authorize] attribute to control access to controllers or controller actions. This attribute can be applied to an entire MVC controller or a particular controller action.


1 Answers

You can use the following approach in order to implement Active Directory Authentication in ASP.NET MVC.

Step 1: Modify the Login methods in the AccountController as shown below (also add the necessary references):

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    try
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        // Check if the User exists in LDAP
        if (Membership.GetUser(model.UserName) == null)
        {
            ModelState.AddModelError("", "Wrong username or password");
            return this.View(model);
        }

        ApplicationGroupManager groupManager = new ApplicationGroupManager();

        // Validate the user using LDAP 
        if (Membership.ValidateUser(model.UserName, model.Password))
        {
            FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
            // FormsAuthentication.SetAuthCookie(model.UserName, false);

            // Check if the User exists in the ASP.NET Identity table (AspNetUsers)
            string userName = model.UserName.ToString().ToLower(new CultureInfo("en-US", false)); // When UserName is entered in uppercase containing "I", the user cannot be found in LDAP
            //ApplicationUser user = UserManager.FindByName(userName);
            ApplicationUser user = await UserManager.FindByNameAsync(userName); //Asynchronous method

            if (user == null) // If the User DOES NOT exists in the ASP.NET Identity table (AspNetUsers)
            {
                // Create a new user using the User data retrieved from LDAP
                // Create an array of properties that we would like and add them to the search object  
                string[] requiredProperties = new string[] { "samaccountname", "givenname", "sn", "mail", "physicalDeliveryOfficeName", "title" };
                var userInfo = CreateDirectoryEntry(model.UserName, requiredProperties);

                user = new ApplicationUser();

                // For more information about "User Attributes - Inside Active Directory" : http://www.kouti.com/tables/userattributes.htm
                user.UserName = userInfo.GetDirectoryEntry().Properties["samaccountname"].Value.ToString();
                user.Name = userInfo.GetDirectoryEntry().Properties["givenname"].Value.ToString();
                user.Surname = userInfo.GetDirectoryEntry().Properties["sn"].Value.ToString();
                user.Email = userInfo.GetDirectoryEntry().Properties["mail"].Value.ToString();
                user.EmailConfirmed = true;
                //user.PasswordHash = null;
                //user.Department = GetDepartmentId(userInfo.GetDirectoryEntry().Properties["physicalDeliveryOfficeName"].Value.ToString());

                //await Register(user);
                var result = await UserManager.CreateAsync(user); //Asynchronous method

                //If the User has succesfully been created
                //if (result.Succeeded)
                //{
                //    //var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
                //    //var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
                //    //await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking this link: <a href=\"" + callbackUrl + "\">link</a>");
                //    //ViewBag.Link = callbackUrl;
                //    //return View("DisplayEmail");
                //}

                // Define user group (and roles)
                var defaultGroup = "751b30d7-80be-4b3e-bfdb-3ff8c13be05e"; // Id of the ApplicationGroup for the Default roles
                //groupManager.SetUserGroups(newUser.Id, new string[] { defaultGroup });
                await groupManager.SetUserGroupsAsync(user.Id, new string[] { defaultGroup }); //Asynchronous method
                //groupManager.SetGroupRoles(newGroup.Id, new string[] { role.Name });
            }
            // !!! THERE IS NO NEED TO ASSIGN ROLES AS IT IS ASSIGNED AUTOMATICALLY IN ASP.NET Identity 2.0
            //else // If the User exists in the ASP.NET Identity table (AspNetUsers)
            //{
            //    //##################### Some useful ASP.NET Identity 2.0 methods (for Info) #####################
            //    //ApplicationGroupManager gm = new ApplicationGroupManager();
            //    //string roleName = RoleManager.FindById("").Name; // Returns Role Name by using Role Id parameter
            //    //var userGroupRoles = gm.GetUserGroupRoles(""); // Returns Group Id and Role Id by using User Id parameter
            //    //var groupRoles = gm.GetGroupRoles(""); // Returns Group Roles by using Group Id parameter
            //    //string[] groupRoleNames = groupRoles.Select(p => p.Name).ToArray(); // Assing Group Role Names to a string array
            //    //###############################################################################################

            //    // Assign Default ApplicationGroupRoles to the User
            //    // As the default roles are already defined to the User after the first login to the system, there is no need to check if the role is NULL (otherwise it must be checked!!!)
            //    //var groupRoles = groupManager.GetGroupRoles("751b30d7-80be-4b3e-bfdb-3ff8c13be05e"); // Returns Group Roles by using Group Id parameter
            //    var groupRoles = await groupManager.GetGroupRolesAsync("751b30d7-80be-4b3e-bfdb-3ff8c13be05e"); // Returns Group Roles by using Group Id parameter (Asynchronous method)

            //    foreach (var role in groupRoles)
            //    {
            //        //Assign ApplicationGroupRoles to the User
            //        string roleName = RoleManager.FindById(role.Id).Name;
            //        UserManager.AddToRole(user.Id, roleName);
            //    }
            //}

            //Sign in the user
            await SignInAsync(user, model.RememberMe);

            if (this.Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
                        && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
            {
                return this.Redirect(returnUrl);
                //return RedirectToLocal(returnUrl);
            }
            return this.RedirectToAction("Index", "Home");
        }
        else
        {
            ModelState.AddModelError("", "Wrong username or password");
            return this.View(model);
        }
    }
    catch (Exception ex)
    {
        TempData["ErrorMessage"] = ex.Message.ToString();
        return View("Error", TempData["ErrorMessage"]);
    }
}

/* Since ASP.NET Identity and OWIN Cookie Authentication are claims-based system, the framework requires the app to generate a ClaimsIdentity for the user. 
ClaimsIdentity has information about all the claims for the user, such as what roles the user belongs to. You can also add more claims for the user at this stage.
The highlighted code below in the SignInAsync method signs in the user by using the AuthenticationManager from OWIN and calling SignIn and passing in the ClaimsIdentity. */
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
    var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
    AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}

static SearchResult CreateDirectoryEntry(string sAMAccountName, string[] requiredProperties)
{
    DirectoryEntry ldapConnection = null;

    try
    {
        // Create LDAP connection object  
        //ldapConnection = new DirectoryEntry("alpha.company.com");
        ldapConnection = new DirectoryEntry("LDAP://OU=Company_Infrastructure, DC=company, DC=mydomain", "******", "******");
        //ldapConnection.Path = connectionPath;
        ldapConnection.AuthenticationType = AuthenticationTypes.Secure;

        DirectorySearcher search = new DirectorySearcher(ldapConnection);
        search.Filter = String.Format("(sAMAccountName={0})", sAMAccountName);

        foreach (String property in requiredProperties)
            search.PropertiesToLoad.Add(property);

        SearchResult result = search.FindOne();
        //SearchResultCollection searchResultCollection = search.FindAll();

        if (result != null)
        {
            //foreach (String property in requiredProperties)
            //    foreach (Object myCollection in result.Properties[property])
            //        Console.WriteLine(String.Format("{0,-20} : {1}",
            //                      property, myCollection.ToString()));
            // return searchResultCollection;
            return result;
        }
        else
        {
            return null;
            //Console.WriteLine("User not found!");
        }
        //return ldapConnection;
    }
    catch (Exception e)
    {
        Console.WriteLine("Exception caught:\n\n" + e.ToString());
    }
    return null;
}

Note: In order to force signout in LDAP authentication, add FormsAuthentication.SignOut() line to the LogOff() method as shown below:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
    AuthenticationManager.SignOut();
    FormsAuthentication.SignOut(); //In order to force logout in LDAP authentication
    return RedirectToAction("Login", "Account");
}

Step 2: Update your LoginViewModel (or whatever your Account model class is named) to contain only this LoginModel class:

public class LoginViewModel
{
    [Required]
    public string UserName { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    public bool RememberMe { get; set; }
}

On the other hand, add the custom properties i.e. Name, Surname, UserName, Department, etc. to the necessary model i.e. ApplicationUser, RegisterViewModel.


Step 3: Finally, update your Web.config file to include these elements:

<connectionStrings>
  <!-- for LDAP -->
  <add name="ADConnectionString" connectionString="LDAP://**.**.***:000/DC=abc,DC=xyz" />
</connectionStrings>

<system.web>
  <!-- For LDAP -->
  <httpCookies httpOnlyCookies="true" />
  <authentication mode="Forms">
    <forms name=".ADAuthCookie" loginUrl="~/Account/Login" timeout="30" slidingExpiration="true" protection="All" />
  </authentication>
  <membership defaultProvider="ADMembershipProvider">
    <providers>
      <clear />
      <add name="ADMembershipProvider" type="System.Web.Security.ActiveDirectoryMembershipProvider" connectionStringName="ADConnectionString" attributeMapUsername="sAMAccountName" connectionUsername="******" connectionPassword="******" />
    </providers>
  </membership>

  ...
</system.web>

Update: Here is the ApplicationUser class used in the example:

// Must be expressed in terms of our custom Role and other types:
public class ApplicationUser : IdentityUser<int, ApplicationUserLogin, 
    ApplicationUserRole, ApplicationUserClaim>, IUser<int>
{
    public string Name { get; set; }

    public string Surname { get; set; }
    
    public async Task<ClaimsIdentity>
        GenerateUserIdentityAsync(UserManager<ApplicationUser, int> manager)
    {
        var userIdentity = await manager
            .CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
        return userIdentity;
    }
}

Hope this helps...

like image 158
Murat Yıldız Avatar answered Oct 06 '22 19:10

Murat Yıldız



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!