using System;
using Novell.Directory.Ldap;
using Sitecore.Security.Domains;
using Sitecore.Diagnostics;
using Sitecore.Security.Authentication;
using Syscert = System.Security.Cryptography.X509Certificates;
using Sitecore.Pipelines.LoggingIn;

//  Add this to your web.config file in the <processors> section, <loggingin...> subsection:
//  <processor mode="on" type="Sitecore.Custom.Pipelines.AuthenticateUser, Sitecore.Custom" />
//  The namespace is arbitrary -- I just happened to use Sitecore.Custom.Pipelines.  The name
//  of the dll file is also arbitrary.

namespace Sitecore.Custom.Pipelines
{
    public class AuthenticateUser
    {
        private static int _ldapPort = 389;
        private static int _ldapSecurePort = 636;
        private static string _ldapHost = "yourhost.com";
        private static string _ldapSearchBase = "ou=people,dc=company,dc=com";

        private static string _userID = string.Empty;
        private static string _userPassword = string.Empty;
        private static string _userDN;

        private static string _errorMsg;
        private static bool _bindResult;

        public void Process(LoggingInArgs args)
        {
            // Why are these defined here and not above?  I discovered that if I don't do the assignments here, that I
            // can log in successfully, log out, then log in again using a bad password.  Not good.
            _userDN = string.Empty;
            _errorMsg = string.Empty;
            _bindResult = false;

            Domain domain = Context.Domain;

            // Use Sitecore's SQL authentication for the admin account
            if (args.Username == "sitecore\\admin")
            {
                return;
            }

            if (args.Username.Length > 0 && domain != null)
            {
                // Create the LDAP filter by removing the domain from the username and appending it to "uid="
                // Sitecore might actually have a method that does this...but I'm not sure.
                _userID = "uid=" + args.Username.Replace("sitecore\\", string.Empty).ToString();
                _userPassword = args.Password.ToString();

                // Get the user's distiguished name from LDAP using an anonymous bind
                GetUserDN();

                // If the user exists in LDAP, authenticate with the user's DN and password
                if (_userDN != string.Empty)
                {
                    Authenticate();

                    // If the user successfully authenticates, log them into Sitecore with the supplied username
                    // and shared password
                    if (_bindResult == true && AuthenticationManager.Login(args.Username, "password", false) == true)
                    {
                        Sitecore.Web.WebUtil.Redirect("~/sitecore");
                    }
                    else
                    {
                        Log.Info("Authentication failed for user " + _userID + ": " + _errorMsg, "Security - LDAP authentication");
                        args.AddMessage("Invalid password.");
                        args.Success = false;
                        // I noticed that if the next line is omitted, Sitecore goes on to check the user in its SQL database
                        args.AbortPipeline();
                    }
                }
                else
                {
                    Log.Info("Authentication failed for user " + _userID + ": " + _errorMsg, "Security - LDAP authentication");
                    args.AddMessage(_errorMsg);
                    args.Success = false;
                    args.AbortPipeline();
                }
            }

            return;
        }

        private static void GetUserDN()
        {
            try
            {
                LdapConnection ldapConn = new LdapConnection();

                // This is an anonymous bind.  Our directory has two entries for each user (one for security, one for HR), but
                // only the first DN is appropriate for the second bind, so our search scope is limited to a single level.
                ldapConn.Connect(_ldapHost, _ldapPort);
                ldapConn.Bind(null, null);

                LdapSearchResults result = ldapConn.Search(_ldapSearchBase, LdapConnection.SCOPE_ONE, _userID, null, false);

                if (result.hasMore())
                {
                    LdapEntry nextEntry = null;

                    try
                    {
                        nextEntry = result.next();
                        _userDN = nextEntry.DN;
                    }
                    catch (LdapException e)
                    {
                        _errorMsg = e.LdapErrorMessage;
                    }

                    ldapConn.Disconnect();
                }
                else
                {
                    _errorMsg = "User does not exist";
                }
            }
            catch (LdapException e)
            {
                _errorMsg = e.LdapErrorMessage;
            }
            catch (Exception e)
            {
                _errorMsg = e.Message;
            }
        }

        private static void Authenticate()
        {
            try
            {
                LdapConnection ldapConn = new LdapConnection();
                ldapConn.SecureSocketLayer = true;

                ldapConn.UserDefinedServerCertValidationDelegate += new CertificateValidationCallback(MySSLHandler);

                // This is a secure bind using the user's DN and password
                ldapConn.Connect(_ldapHost, _ldapSecurePort);
                ldapConn.Bind(_userDN, _userPassword);
                ldapConn.Disconnect();

                _bindResult = true;
            }
            catch (LdapException e)
            {
                _errorMsg = e.Message;
            }
            catch (Exception e)
            {
                _errorMsg = e.StackTrace;
            }
        }

        public static bool MySSLHandler(Syscert.X509Certificate certificate, int[] certificateErrors)
        {
            // Granted, we could do a whole bunch of certificate validation here, but we trust our certficate by default,
            // so we're simply returning true here.
            return true;
        }
    }
}