5

I configure a Web App that use AD FS, for this I use OWIN.

For the login, all is ok. If i'm an user of a domain and go to the website, he is automatically connected.

But what I want to have is to handle users and roles by myself after login.

So I want to check that an user exists in my database with this AD account (this process will be make before the login in another application)

I want to use Identity from Microsoft to handle claims (roles and permissions). But I don't understand how to put my code to handle the successfull connection from AD FS (with Ws-Federation) and add verification and fill in the right roles.

My code in ConfigureAuth:

public partial class Startup
{
    private static string realm = ConfigurationManager.AppSettings["ida:Wtrealm"];
    private static string adfsMetadata = ConfigurationManager.AppSettings["ida:ADFSMetadata"];
    private NLogLoggingService _loggingService;

    public void ConfigureAuth(IAppBuilder app)
    {
        _loggingService = new NLogLoggingService("Startup");
        _loggingService.Debug("ConfigureAuth");

        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions());

        app.UseWsFederationAuthentication(
            new WsFederationAuthenticationOptions
            {
                Wtrealm = realm,
                MetadataAddress = adfsMetadata,

                //CallbackPath = PathString.FromUriComponent("/Account/TestCallback"),

                // https://msdn.microsoft.com/en-us/library/microsoft.owin.security.authenticationmode(v=vs.113).aspx
                AuthenticationMode = AuthenticationMode.Passive,

                //Notifications = new WsFederationAuthenticationNotifications
                //{

                //}
            });

    }

In Web.config, realm is the link to my Web App (https://ssoadfs.test) and adfsMetadata is the link to metadata.xml from AD FS.

What is the way to go to set my role and login logic after AD FS connection ?

Schema that what I was thinking:

enter image description here

EDIT: After some tries, I cannot handle any success callback. I don't want to have to handle roles in HomeController ...

My last Auth config:

            _loggingService = new NLogLoggingService("Startup");
        _loggingService.Debug("ConfigureAuth");

        // Configure the db context, user manager and signin manager to use a single instance per request
        app.CreatePerOwinContext(ApplicationUser.ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

        app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ApplicationCookie);

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            Provider = new CookieAuthenticationProvider
            {
                OnResponseSignIn = ctx =>
                {
                    _loggingService.Debug("OnResponseSignIn");
                    ctx.Identity = TransformClaims(ctx, app);
                },
                // Enables the application to validate the security stamp when the user logs in.
                // This is a security feature which is used when you change a password or add an external login to your account.  
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(30),
                    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
            }
        });

        app.UseWsFederationAuthentication(
            new WsFederationAuthenticationOptions
            {
                Wtrealm = realm,
                MetadataAddress = adfsMetadata,
                Caption = "Active Directory",

                CallbackPath = PathString.FromUriComponent("/Account/TestCallback"),

                Notifications = new WsFederationAuthenticationNotifications
                {
                    SecurityTokenValidated = n =>
                    {
                        new NLogLoggingService("Startup").Debug("SecurityTokenValidated");

                        var incomingClaimsFromAdfs = n.AuthenticationTicket.Identity.Claims.ToList();
                        var incomingClaimsHasNameIdentifier =
                            incomingClaimsFromAdfs.Any(
                                c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier);

                        _loggingService.Debug("SecurityTokenValidated - incomingClaimsHasNameIdentifier: " +
                                              incomingClaimsHasNameIdentifier);
                        if (!incomingClaimsHasNameIdentifier)
                        {
                            var emailClaim =
                                incomingClaimsFromAdfs.First(c => c.Type == System.Security.Claims.ClaimTypes.Name);
                            _loggingService.Debug(emailClaim.Value);
                        }

                        //if (!incomingClaimsHasNameIdentifier)
                        //{
                        //    var emailClaim = incomingClaimsFromAdfs.First(c => c.Type == System.Security.Claims.ClaimTypes.Name);
                        //    incomingClaimsFromAdfs.Add();

                        //    IUser user = await this.UserStore.FindByNameOrEmailAsync(userNameOrEmailAddress);
                        //    if ((Entity<long>)user == (Entity<long>)null)
                        //        LoginResult = new ApplicationUserManager.LoginResult(LoginResultType.InvalidUserNameOrEmailAddress, default(IUser));
                        //    //else if (!loggedInFromExternalSource && new PasswordHasher().VerifyHashedPassword(user.Password, plainPassword) != PasswordVerificationResult.Success)
                        //    //    LoginResult = new UserManager<TTenant, TRole, TUser>.LoginResult(LoginResultType.InvalidPassword, user);
                        //    else
                        //        LoginResult = await this.CreateLoginResultAsync(user, tenant);
                        //}
                        //else
                        //{
                        //    throw new ApplicationException("Get ADFS to provide the NameIdentifier claim!");
                        //}

                        //var normalizedClaims = incomingClaimsFromAdfs.Distinct(new ClaimComparer());
                        //var claimsIdentity = new ClaimsIdentity(normalizedClaims, n.AuthenticationTicket.Identity.AuthenticationType);
                        //n.AuthenticationTicket = new AuthenticationTicket(claimsIdentity, n.AuthenticationTicket.Properties);
                        return Task.FromResult(0);
                    }
                }
            });

In this code, I tried CallbackPath (nothing appeared in my log), WsFederationAuthenticationNotifications.SecurityTokenValidated (nothing appeared in my log), CookieAuthenticationProvider.OnResponseSignIn (same nothing happened)

In HomeController i'm able to have Identity.Name:

public ActionResult Index()
    {
        if (HttpContext.GetOwinContext().Authentication.User.Identity.IsAuthenticated)
        {
            new NLogLoggingService("Home").Debug("User is authenticated");
        }

        return View();
    }

Did I miss something to get Notifications working or Provider in CookieAuthenticationOptions ???

Jerome2606
  • 933
  • 20
  • 42
  • Using AD and Identity is not and out of the box option. You're stuck creating that functionality. You will need 2 UserManager instances configured distinctly... – Dave Alperovich Apr 11 '16 at 17:31
  • Can you delevop the "2 UserManager instances" ? For now, I think to create a Wep API that works with AD WS-Federation (still todo) and then call anoher method with the cookie to check if user exists in Database as User. – Jerome2606 Apr 12 '16 at 10:18
  • @user18620 Hello, not really a solution ... but I check this in AccountController.AuthenticationCallbackAD() by setting the default route in App_Start/RouteConfig. Inside this method I create the user or check if he is validated using ASP.NET Identity UserManager. And add custom claims. – Jerome2606 May 07 '16 at 12:54
  • Did you find a final solution to this problem? – PreguntonCojoneroCabrón Nov 08 '16 at 22:28
  • @PreguntonCojoneroCabrón No, I keep it working in HomeController for now, not a beautiful implementation but I didn't have the time to make some search for a better solution – Jerome2606 Nov 12 '16 at 20:43

1 Answers1

1

If you use ASP.NET Identity 2.0 or later version, you can use an approach similar to it shown below. Please note that this approach assign GroupRoles to the user instead of assigning each of roles one by one. You can change necessary parts according to your needs.

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

    ApplicationGroupManager groupManager = new ApplicationGroupManager();

    if (Membership.ValidateUser(model.UserName, model.Password))
    {
        FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);

        //Assign Roles to the current User
        ApplicationUser user = UserManager.FindByName(model.UserName);

        //If the user is registered in the system (ASP.NET Identity) add record to AspNetUsers table 
        if (user != null)
        {
            //Returns Group Id and Role Id by using User Id parameter
            var userGroupRoles = groupManager.GetUserGroupRoles("bfd9730e-2093-4fa0-89a2-226e301d831b"); 
            foreach (var role in userGroupRoles)
            {
                string roleName = RoleManager.FindById(role.ApplicationRoleId).Name;
                UserManager.AddToRole(user.Id, roleName);
            }
        }
        else
        {
            //crate new user
            //first retrieve user info 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);

            var newUser = new ApplicationUser();
            newUser.UserName = userInfo.GetDirectoryEntry().Properties["samaccountname"].Value.ToString();
            newUser.Name = userInfo.GetDirectoryEntry().Properties["givenname"].Value.ToString();
            newUser.Surname = userInfo.GetDirectoryEntry().Properties["sn"].Value.ToString();
            newUser.Email = userInfo.GetDirectoryEntry().Properties["mail"].Value.ToString();
            newUser.EmailConfirmed = true;
            newUser.PasswordHash = null;

            var result = await UserManager.CreateAsync(newUser);
            if (result.Succeeded)
            {
                //If the user is created ...
            }

            //Assign user group (and roles)
            var defaultGroup = "751b30d7-80be-4b3e-bfdb-3ff8c13be05e";
            groupManager.SetUserGroups(newUser.Id, new string[] { defaultGroup });
        }
        return this.RedirectToAction("Index", "Issue");
    }

    this.ModelState.AddModelError(string.Empty, "Wrong username or password!");
    return this.View(model);
}


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

    try
    {
        ldapConnection = new DirectoryEntry("LDAP://OU=******, DC=******, DC=******", "acb@xyz.com", "YourPassword");
        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(); //You can also retrieve all information

        if (result != null)
        {                
            return result;
        }
        else {
            return null;
            //Console.WriteLine("User not found!");
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("Exception caught:\n\n" + e.ToString());
    }

    return null;
}

Murat Yıldız
  • 11,299
  • 6
  • 63
  • 63
  • Hello Murat, thanks to trying to help me. But my problem is more that I can't handle a callback in the right way of OWIN. I will never pass in a Login page as it is an application SSO (so the user is authenticated by Owin before to get in my Application). For now, I add logic in HomeController to check if the user is authenticated then add user in UserManager ... :( – Jerome2606 Apr 18 '16 at 08:16
  • You are welcome Jerome. I could not understand why your used HomeController instead of AccountController? On the other hand, if you have enough time and not used ASP.NET Identity 2.0 or later, you might have a look at the articles like [ASP.NET MVC and Identity 2.0: Understanding the Basics](http://www.codeproject.com/Articles/762428/ASP-NET-MVC-and-Identity-Understanding-the-Basics). Good luck :) – Murat Yıldız Apr 18 '16 at 08:23
  • I use AspNet.Identity.Core, .EntityFramework, .Owin. But I would like to use AD FS and Owin, to simplify configuration (only needed the metadata.xml from AD). I use a code similar to your with UserManager from Identity and it's not a choice to add this logic in HomeController. I could put it In AccountController with defining the default Route. But I'm sure that Owin can help me to handle that in Startup Auth Config – Jerome2606 Apr 18 '16 at 08:36