Click here to Skip to main content
14,357,934 members

Active Directory Webservices for DevOps

Rate this:
5.00 (5 votes)
Please Sign up or sign in to vote.
5.00 (5 votes)
27 Oct 2015Apache
Active Directory Web services Which handles Authentication, SSO for other apps, aside from Native Routines

Introduction

I founded interesting to do a web services that handles most of day to day job when it comes to active directory, such as ADD/REMOVE Users/Groups/Computers Enable/Disable Users/Computers...etc. The idea behind was instead of implementing those micro routines every time you need them you just consume the web services. But the real reason why implemented this thing because of authentication and to be able to provide SSO across all applications that wishes to consume this web service. This implementation is not final, but it gives the idea how to extend to include all active directory related routines, which makes life easier for Sysadmins when they want to automate tasks such as create AD user along with Mailbox and enable Lync. those previously mentioned tasks could be done using PowerShell most of them if not all of them, but to be able to provide such functionalities to applications it is convenient enough to do it through web service

Background

What I’m doing here is Implementing WCF Web services for active directory Routines, Most of the routines is a wrappers around ComputerPrincipal, UserPrincipal, GroupPrincipal, and DirectoryEntry which is available under the following namespaces,

using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.DirectoryServices.ActiveDirectory; 

Aside from that implementing AES Encryption for the session which contains and XML Serialized object for Username, Password and SessionStart Date.

Using the code

The Code is quite long so i will try to keep it go through the main things.

The code is being split in two categories

  1.  Authentication Related (which include session handling and encryption)
  2.  Active Directory Routines

So we will start with Authentication related, and to begin with Session Handling, which contains a Serializable Struct contains the SessionData and then the handler handles Serialization and Deserialization of the SessionData Struct, I will not include the Encryption and decryption here as it could be replaced with what every Symetrical Encryption Algorithm you wish to implement. The Resulting Encrypted Session Will be sent with every single Request for every Active Directory routines.

    [Serializable]
    public struct SessionData
    {
        public string Username { get; set; }
        public string Password { get; set; }
        public DateTime SessionStart { get; set; }

    }

    public class SessionHandler
    {
       
       
        private static string SerializeObject<t>(SessionData sessionData)
        {
            string serialized = string.Empty;
            XmlSerializer serializer = new XmlSerializer( typeof( T ) );

            using ( StringWriter writer = new StringWriter() )
            {
                serializer.Serialize( writer , sessionData );

                byte[] data = Encoding.UTF8.GetBytes( writer.ToString() );

                return Convert.ToBase64String( data );
            }

            
        }

        private static T DesrializeObject<t>( string data )
        {
            return (T)DesrializeObject( data , typeof( T ) );
        }

        private static object DesrializeObject( string objectData , Type type )
        {
            var serializer = new XmlSerializer( type );
            object result;

            using ( TextReader reader = new StringReader( objectData ) )
            {
                result = serializer.Deserialize( reader );
            }

            return result;
        }

        private static string Encrypt(string session)
        {
           string sessionCypher = Cryptor.Encrypt( session );

           return sessionCypher;
        }

      

        private static string Decrypt( string session ) 
        {
            string decrptedSession = Cryptor.Decrypt( session );
            return decrptedSession;
        }

        public static string EncryptSession(SessionData session)
        {
            string serializedSession = SerializeObject<sessiondata>( session );
            string encryptedSession = Encrypt( serializedSession );

            return encryptedSession;
            
        }

        public static SessionData DecryptSession( string session ) 
        {
            string decryptedSession = Decrypt( session );
            SessionData desrialized = DesrializeObject<sessiondata>( decryptedSession );
            return desrialized; 
        }


    }
</sessiondata></sessiondata></t></t>

 

Everything Starts when user authenticate using his active directory credentials which returns an encrypted session as a JSON Object which Contains  IsAuthenticated, Message, SessionKey , the AuthData Class contains Username, Password 

public Session AuthenticateUserUsingCredentials( AuthDataRequest authData )
{
    UserInfoResponse userInfo = new UserInfoResponse();
    string emailAddress = authData.username;
    string password = authData.password;

    Session stat = new Session();

    string msg = string.Empty;

    if ( string.IsNullOrEmpty( emailAddress ) || string.IsNullOrEmpty( password ) )
    {
        stat.Message = "Email and/or password can't be empty!";
        stat.IsAuthenticated = false;

        return stat;
    }
    try
    {
        userInfo = GetUserAttributes( emailAddress );

        if ( userInfo == null )
        {
            stat.Message = "Error: Couldn't fetch user information!";
            stat.IsAuthenticated = false;

            return stat;
        }

        var directoryEntry = new DirectoryEntry( LocalGcUri , userInfo.Upn , password );

        directoryEntry.AuthenticationType = AuthenticationTypes.None;

        var localFilter = string.Format( AdSearchFilter , emailAddress );

        var localSearcher = new DirectorySearcher( directoryEntry );

        localSearcher.PropertiesToLoad.Add( "mail" );
        localSearcher.Filter = localFilter;

        var result = localSearcher.FindOne();

        if ( result != null )
        {
            stat.Message = "You have logged in successfully!";
            stat.IsAuthenticated = true;

            //Set the session Data
            SessionData session = new SessionData();

            session.Username = userInfo.EmailAddress;
            session.Password = password;
            session.SessionStart = DateTime.Now;

            //Encrypt Session Data
            stat.SessionKey = SessionHandler.EncryptSession( session );

            return stat;
        }

        stat.Message = "Login failed, please try again.";
        stat.IsAuthenticated = false;

        return stat;
    }
    catch ( Exception ex )
    {
        stat.Message = "Wrong Email and/or Password " + ex;
        stat.IsAuthenticated = false;

        return stat;
    }
}
And the Web service interface for Authentication defined to serialize the input and output as JSON
 
[OperationContract]
[WebInvoke( 
    UriTemplate = "auth/user" ,  
    RequestFormat= WebMessageFormat.Json,  
    ResponseFormat = WebMessageFormat.Json,
    BodyStyle = WebMessageBodyStyle.Bare,
    Method = "POST" )]
Session AuthenticateUserUsingCredentials( [MessageParameter( Name = "authdata" )] AuthDataRequest authData );

Kindly note after successfully authenticating yourself, you will be able to use the session key to authenticate to different applications which uses the same web services for authentication, and this could be done as follow

Web service OperationContract Definition

[OperationContract]
[WebInvoke(
    UriTemplate = "auth/session" , 
    ResponseFormat = WebMessageFormat.Json ,
    RequestFormat = WebMessageFormat.Json ,
    BodyStyle = WebMessageBodyStyle.Bare,
    Method="POST")]
Session AuthenticateUserUsingSession( [MessageParameter( Name = "sessionkey" )] string sessionKey );

As for the implementation it is described as below

public Session AuthenticateUserUsingSession( string sessionKey )
{
    return ValidateSession( sessionKey );
}

This Function is only a wrapper for SessionValidation, the session will be validated based on two factors . 

  1. Successful Decryption for the SessionKey
  2. The Validity of the session doesn't  exceed configurable TTL in my case is 2 hours
public Session ValidateSession( string sessionKey )
{
    Session stat = new Session();

    if ( string.IsNullOrWhiteSpace( sessionKey ) )
    {
        stat.Message = "No Session key has been provide";
        stat.IsAuthenticated = false;

        return stat;
    }
    else
    {
        try
        {
            SessionData sessionData = SessionHandler.DecryptSession( sessionKey );

            if ( sessionKey != null && ( ( DateTime.Now.Subtract( sessionData.SessionStart ) ).TotalHours < SessionTTL ) )
            {
                stat.Message = "You have logged in successfully!";
                stat.IsAuthenticated = true;
                stat.SessionKey = sessionKey;
                return stat;
            }
            else
            {
                AuthDataRequest authData = new AuthDataRequest();
                authData.username = sessionData.Username;
                authData.password = sessionData.Password;

                stat = AuthenticateUserUsingCredentials( authData );
                stat.Message = "You have logged in successfully!, and Session key has been renewed";

                return stat;
            }
        }
        catch ( Exception ex )
        {
            stat.Message = "Couldn't validate Session key, kinldy authenticate first " + ex;
            stat.IsAuthenticated = false;

            return stat;
        }
    }
}

As far as authentication I think we can stop here and move to Active Directory routines, kindly note am not going to list all the routines because they are quite a lot, so I will stick with couple of examples

AddUser

First we define a DataContract with DataMembers

[DataContract]
public class RequestUserCreate
{
    [DataMember]
    public string FirstName { get; set; }

    [DataMember]
    public string LastName { get; set; }

    [DataMember]
    public string UserLogonName { get; set; }

    [DataMember]
    public string EmployeeID { get; set; }

    [DataMember]
    public string EmailAddress { get; set; }

    [DataMember]
    public string Telephone { get; set; }

    [DataMember]
    public string Address { get; set; }

    [DataMember]
    public string PostalCode { get; set; }

    [DataMember]
    public string PostOfficeBox { get; set; }

    [DataMember]
    public string PhysicalDeliveryOffice { get; set; }

    [DataMember]
    public string Country { get; set; }

    [DataMember]
    public string City { get; set; }

    [DataMember]
    public string Title { get; set; }

    [DataMember]
    public string Department { get; set; }

    [DataMember]
    public string Company { get; set; }

    [DataMember]
    public string Description { get; set; }

    [DataMember]
    public string PhoneExtention { get; set; }

    [DataMember]
    public string PhoneIpAccessCode { get; set; }

    [DataMember]
    public string Password { get; set; }

    [DataMember]
    public DomainRequest DomainInfo { get; set; }

}

I think that you will note that there is an Object called DomainRequest, which include all the information needed to connect to a domain controller or to connect to Global Catalog aside from holding the session key after successful authentication

[DataContract]
public class DomainRequest
{
    [DataMember]
    public string ADHost { get; set; }

    [DataMember]
    public string DomainName { get; set; }

    [DataMember]
    public string ContainerPath { get; set; }

    [DataMember]
    public string BindingUserName { get; set; }

    [DataMember]
    public string BindingUserPassword { get; set; }

    [DataMember]
    public string SessionKey { get; set; }
}

but why would we add username and password as long as we have the session key and we are already authenticated, and the answer will be simple, because you may not have a write or read permission on that specific domain container, so you run this commands with a different username, but it is a must that you are already authenticated and the session key is valid, another thing to mention in the could it is always checking if you have a write access to that specific container.

Then we define the OperationContract

[OperationContract]
[WebInvoke(
    UriTemplate = "ad/account/add" ,
    RequestFormat = WebMessageFormat.Json ,
    ResponseFormat = WebMessageFormat.Json ,
    BodyStyle = WebMessageBodyStyle.Bare ,
    Method = "POST" )]
ResponseMessage AddADUser( [MessageParameter( Name = "userinfo" )] RequestUserCreate userinfo );

And the implementation will look like below  

public ResponseMessage AddADUser( RequestUserCreate userinfo )
{
    ResponseMessage status = new ResponseMessage();

    status.IsSuccessful = false;
    status.Message = string.Empty;

    Session stat = ValidateSession( userinfo.DomainInfo.SessionKey );

    if ( stat.IsAuthenticated == true )
    {

        PrincipalContext principalContext = null;

        string uri = FixADURI( userinfo.DomainInfo.ADHost , userinfo.DomainInfo.ContainerPath );

        if ( string.IsNullOrWhiteSpace( uri ) )
        {
            status.Message = status.Message = "AD Host is not allowed to be empty, kindly provide the AD Host";
            return status;
        }

        bool isAllowWite = CheckWriteOermission( uri , userinfo.DomainInfo.BindingUserName , userinfo.DomainInfo.BindingUserPassword );

        try
        {
            UserPrincipal usr = FindADUser( userinfo.UserLogonName , userinfo.DomainInfo );
            if ( usr != null )
            {
                status.Message = " user already exists. Please use a different User Logon Name";
                return status;
            }
            else
            {
                principalContext = new PrincipalContext( ContextType.Domain , userinfo.DomainInfo.DomainName , userinfo.DomainInfo.ContainerPath , userinfo.DomainInfo.BindingUserName , userinfo.DomainInfo.BindingUserPassword );
            }
        }
        catch ( Exception ex )
        {
            status.Message = @"Failed to create PrincipalContext: " + ex;
            return status;
        }

        // Create the new UserPrincipal object
        UserPrincipal userPrincipal = new UserPrincipal( principalContext );

        if ( !string.IsNullOrWhiteSpace( userinfo.LastName ) )
            userPrincipal.Surname = userinfo.LastName;

        if ( !string.IsNullOrWhiteSpace( userinfo.FirstName ) )
            userPrincipal.GivenName = userinfo.FirstName;

        if ( !string.IsNullOrWhiteSpace( userinfo.LastName ) && !string.IsNullOrWhiteSpace( userinfo.FirstName ) )
            userPrincipal.DisplayName = userinfo.FirstName + " " + userinfo.LastName;

        if ( !string.IsNullOrWhiteSpace( userinfo.Description ) )
            userPrincipal.Description = userinfo.Description;

        if ( !string.IsNullOrWhiteSpace( userinfo.EmployeeID ) )
            userPrincipal.EmployeeId = userinfo.EmployeeID;

        if ( !string.IsNullOrWhiteSpace( userinfo.EmailAddress ) )
            userPrincipal.EmailAddress = userinfo.EmailAddress;

        if ( !string.IsNullOrWhiteSpace( userinfo.Telephone ) )
            userPrincipal.VoiceTelephoneNumber = userinfo.Telephone;

        if ( !string.IsNullOrWhiteSpace( userinfo.UserLogonName ) )
            userPrincipal.SamAccountName = userinfo.UserLogonName;

        if ( !string.IsNullOrWhiteSpace( userinfo.Password ) )
            userPrincipal.SetPassword( userinfo.Password );

        userPrincipal.Enabled = true;
        userPrincipal.ExpirePasswordNow();

        try
        {
            userPrincipal.Save();

            DirectoryEntry de = (DirectoryEntry)userPrincipal.GetUnderlyingObject();

            FillUserExtraAttributes( ref de , userinfo );

            de.CommitChanges();
            status.Message = "Account has been created successfuly";
            status.IsSuccessful = true;
        }
        catch ( Exception ex )
        {
            status.Message = "Exception creating user object. " + ex;
            status.IsSuccessful = false;
            return status;
        }

        return status;
    }
    else
    {
        status.Message = "Kindly authenticate first";
        return status;
    }
}

Here I'm Using UserPrincipal to populate the user object with the basic information , save it and return the UnderlyingObject which is of type DirectoryEntry to populate the rest of the Information, the extra attributes function is listed below 

private void FillUserExtraAttributes( ref DirectoryEntry de , RequestUserCreate userinfo ) 
{
    try 
    {
        if ( !string.IsNullOrWhiteSpace( userinfo.Title ) )
            de.Properties[ "title" ].Value = userinfo.Title;

        if ( !string.IsNullOrWhiteSpace( userinfo.City ) )
            de.Properties[ "l" ].Value = userinfo.City;

        if ( !string.IsNullOrWhiteSpace( userinfo.Country ) )
            de.Properties[ "c" ].Value = userinfo.Country;

        if ( !string.IsNullOrWhiteSpace( userinfo.PostalCode ) )
            de.Properties[ "postalCode" ].Value = userinfo.PostalCode;

        if ( !string.IsNullOrWhiteSpace( userinfo.PostOfficeBox ) )
            de.Properties[ "postOfficeBox" ].Value = userinfo.PostOfficeBox;

        if ( !string.IsNullOrWhiteSpace( userinfo.Address ) )
            de.Properties[ "streetAddress" ].Value = userinfo.Address;

        if ( !string.IsNullOrWhiteSpace( userinfo.Department ) )
            de.Properties[ "department" ].Value = userinfo.Department;

        if ( !string.IsNullOrWhiteSpace( userinfo.PhysicalDeliveryOffice ) )
            de.Properties[ "physicalDeliveryOfficeName" ].Value = userinfo.PhysicalDeliveryOffice;

        if ( !string.IsNullOrWhiteSpace( userinfo.Company ) )
            de.Properties[ "company" ].Value = userinfo.Company;

        if ( !string.IsNullOrWhiteSpace( userinfo.PhoneExtention ) )
            de.Properties[ "extensionAttribute1" ].Value = userinfo.PhoneExtention;

        if ( !string.IsNullOrWhiteSpace( userinfo.PhoneIpAccessCode ) )
            de.Properties[ "extensionAttribute2" ].Value = userinfo.PhoneIpAccessCode;
    }
    catch ( Exception ex ) 
    {
        throw ex;
    }
}

 

Web services Definition

The WCF Web services has been exposed through REST and SOAP to provide the flexibility for the developers to choose their way of consumption, as for the web.conf the configuration is listed below 

<configuration>
  <appSettings>
    <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
    <add key="LocalDomainURI" value="GC://x.x.x.x" />
    <add key="LocalDomainUser" value="bind-user" />
    <add key="LocalDomainPassword" value="bind-password" />
    <add key="ADSearchFilter" value="(&amp;(objectClass=user)(objectCategory=person)(mail={0}))" />
    <add key="MailHost" value="X.X.X.X />
    <add key="ReplyTo" value="xxx@cexample.com" />
    <add key="NotificationsEmail" value="xxx@example.com" />
    <add key="AesKey" value="AES KEY"/>
    <add key="AesIV" value="AES IV"/>
    <add key="SessionTTL" value="2"/>
  </appSettings>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5"/>
  </system.web>
  <system.serviceModel>
    <services>
      <service name="ADWS.Adws">
        <endpoint address="rest" behaviorConfiguration="webBehaviour"

          binding="webHttpBinding" name="RESTEndPoint" contract="ADWS.IAdws" />
        <endpoint address="soap" binding="basicHttpBinding" name="SOAPEndPoint"

          contract="ADWS.IAdws" />
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="webBehaviour">
          <webHttp  helpEnabled="true"/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <protocolMapping>
      <add binding="webHttpBinding" scheme="http" />
    </protocolMapping>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
  <system.webServer>
    <security>
      <requestFiltering allowDoubleEscaping="true"/>
    </security>
    <modules runAllManagedModulesForAllRequests="true"/>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" />
        <add name="Access-Control-Allow-Headers" value="Content-Type, Accept" />
      </customHeaders>
    </httpProtocol>
    <directoryBrowse enabled="true"/>
  </system.webServer>
</configuration>

The AES KEY and IV should be provide here as for their security if somebody accessed the web.conf you can encrypt the web.conf kindly refer to the following article for more information about the topic

History

Version 1.0

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0

Share

About the Author

saddam abu ghaida
Team Leader CCC
Greece Greece
Nothing Much i love reading and developing new things and thats about it

Comments and Discussions

 
GeneralMy vote of 5 Pin
Santhakumar M27-Oct-15 5:38
professionalSanthakumar M27-Oct-15 5:38 
GeneralRe: My vote of 5 Pin
saddam abu ghaida27-Oct-15 5:42
professionalsaddam abu ghaida27-Oct-15 5:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 27 Oct 2015

Stats

12.3K views
320 downloads
20 bookmarked