By Mats Halfvares, the Content Studio development team

Introduction

Content Studio has no built-in repository for users and security groups and by default it uses the native Windows security database. This includes both the local NTML security system and the Active Directory.
The absence of such a repository opens up the possibility to use other user catalogs than the default. In order to do so you must provide the system with mechanism for the caller to supply her credentials and verify them against the catalog and, in case of a successful login, provide user information and security group (role) membership for that caller to Content Studio.
When this has been done the authenticated user and her group memberships will turn up in Content Studio as any Windows user or group. It is then possible to set any of the permissions on any object in Content Studio as usual, thus the powerful security system of Content Studio will be fully available.
This article demonstrates how a .NET developer can create and install the custom code needed when implementing this functionality.

In Content Studio 5.0 it is possible to write a custom authentication provider that acts as the "glue" between the Content Studio native authentication system and an external authentication system such as your own system or commercial catalogs such as Novell NDS or other LDAP catalogs. The external system must provide a mechanism to logon the user and return a token of this logon to the caller when the caller is successfully logged in. This token is the stored at the client in a non-persistent way and the external system can compose the token in such a way that its logon session will expire after a certain time. However, since the built-in authentication model in Windows has logon sessions that does not time out, the current implementation on the Content Studio web application does not support this fully. Thus, for the time being your systems logon tokens should not time out. The logon token is then passed in to the provider that communicates with the external system in order to get a SID (SecurityIdentifier) that uniquely represents the caller, the caller's login name in the DOMAIN\LOGINNAME format and some additional data as well. The author is well aware that both the login name in this format and the SecurityIdentifier is Microsoft's own format but this is where the logic of the provider excels.
When the caller successfully has been identified against the external catalog, the user must be registered in Content Studio. The last step is to map all the user's group memberships against the registered groups in Content Studio. When using the native Windows authentication model all these group memberships are contained within the user's Token but for an external system the provider have to ask the catalog for this list. This list contains of a number of SIDs that will be mapped against registered groups in Content Studio. If the list contains a group that is not registered it will be ignored by Content Studio. For this reason the provider author must also create a utility that can register security groups or roles contained in the external catalog. This application need to provide Content Studio with the group's SID, its Domain name, its name and its description.


The Security Identifier


A SID value includes components that provide information about the SID structure and components that uniquely identify a trustee. A SID consists of the following components:


  • The revision level of the SID structure
  • A 48-bit identifier authority value that identifies the authority that issued the SID
  • A variable number of subauthority or relative identifier (RID) values that uniquely identify the trustee relative to the authority that issued the SID

The combination of the identifier authority value and the subauthority values ensures that no two SIDs will be the same, even if two different SID-issuing authorities issue the same combination of RID values. Each SID-issuing authority issues a given RID only once. SIDs are stored in binary format in a SID structure. 
When you work with SID:s you will most often use the following standardized string notation for SIDs, which makes it simpler to visualize their components:

S-R-I-S-S...

In this notation, the literal character S identifies the series of digits as a SID, R is the revision level, I is the identifier-authority value, and S... is one or more subauthority values.

The following example uses this notation to display the well-known domain-relative SID of the local Administrators group:

S-15-32-544

(This section is an excerpt from the Microsoft Platform SDK documentation)

Creating your own Security Identifier

When building your own SID you should ensure that each catalog should generate its own series of SID values that can be uniquely separated from other catalogues. This ensures that several different authentication providers can be used within the same Content Studio site. The first factor to look at, after the revision number which must be 1, is the identifier authority value. This is a 48-bit number and Microsoft is using only the values 0 - 5. The rest are free for external programs to use. I recommend you to avoid these well-known identifier authorities since they define that the issuer of the identifier is the the Windows platform, which in this case, is not true. Assuming that my unique authority value is 150 the first part of our SID will be S-1-150-.

The remainder is the up to seven, normally five, sub-authorities to create. The first one should be 21 to indicate that this is a regular user or group account. The rest of them but the last one, is the DOMAIN part if the SID. For each computer or active directory domain a new unique value of normally four sub-authorities are created that will a part of the SID for every object in the domain. Thus if we for this particular catalog use the value 493728-38943583-34927, our SID will now have the value:
S-1-150-21-493728-38943583-34927-
The last sub-authority is the RID relative identifier in the domain of the specific object (user or group). The catalog must be able to return a unique value for each object in the catalog, in such a way that these values never will be reused if the object is removed and later recreated with the same name. If our relative identifier is 1007 our SID now is complete and has the value:
S-1-150-21-493728-38943583-34927-1007.

The exact algorithm to use when creating the relative identifier will largely depend on the catalog used and its capabilities. For example, the ASP.NET forms login database uses a GUID structure as the identifier of groups and users - these GUIDs are guaranteed to be unique so you can use them as the uniquely identifier for any user and group in the catalog. By implementing a hashing algorithm and extract a hashed value of these GUIDs you can easily obtain the integer value you need to create the RID for your SID. This can be done in a stored procedure in the database, as a SQL-statement or directly in your provide code.

Writing a provider

In order to be able to debug your component you must develop on a computer that has Content Studio installed.
A custom authentication provider must inherit the AuthenticationBase class in the ContentStudio.Security namespace. This class has one method you must implement, OpenSession. OpenSession takes two arguments, the site ConnectionID and a Token which is some sort of digital proof provided by the external catalog as a token of successful login in the external catalog.

Now, start Visual Studio and create a new class library with one class CustomAuthProvider in the namespace AuthProviders. Next add a reference to Content Studio Server - on a normal installation you will find this assembly in the Global Assembly Cache together with the standard framework classes. Let the class inherit the ContentStudio.Security.AuthenticationBase class and implement the OpenSession abstract method as a stub. At this point your code looks something like this:


using System;
using System.Security.Principal;
namespace AuthProviders
{
    public class CustomAuthProvider : ContentStudio.Security.AuthenticationBase
    {
        public override int OpenSession(int ConnectionID, object Token)
        {
           return 0;
        } 
    } 
}
    

As you can see a custom provider inherits the ContentStudio.Security.AuthenticationBase class. This class is marked abstract (MustInherit in Visual Basic) the acts as base class of the Content Studio authentication subsystem and any authentication system provides Content Studio with the identity (SID) of the caller and a list of all security groups (SID:s) that the caller is a member of. During development there are three important protected methods you must call when you implement the provider.

GetCallerSessionID
Returns the caller's session identifier, and if found, updates the session expires value. Inheriting classes call this method after that the caller has been identified to test if a user's session already is valid. This will radically improve performance since no calls to RegisterUser and SaveUserGroupMapping needs to be done. If the session is valid, this method returns a value greater that 999 and should be returned to the caller. Otherwise, the session is invalid and must be re-opened.
The method accepts the security identifier of the caller and, if found, returns the caller's session identifier. If this method returns a valid session identifier you do not need to continue with the rest of the process, thus saving time and resources.
 
RegisterUser
This method registers a user in Content Studio. If the user already exists and has an invalid session the user information will be invalidated, basic information will be updated and all group mappings will be lost. However, if the user's session is valid RegisterUser only updates the basic information without clearing the group mappings. External inheriting classes call this method as a part of the authentication process. After this call the process call the SaveUserGroupMapping method to assign group memberships to the user.
RegisterUser accepts the SID of the caller and a number of user details such as UserName, FullName, Description and Email address and returns, as an output parameter, the internal UserId
 
SaveUserGroupMapping
Builds the internal mappings between a Content Studio user and the registered groups where the user belongs. External classes inheriting from this class call this method as a part of the authentication process.
This method accepts a UserId, retrieved from a call to RegisterUser , and up to ten SecurityIdentifier objects each one representing the SID of one of the group in the catalog where the caller is a member. You can call this method as many times as you need but Content Studio will only care about groups already registered in Content Studio. This mapping will be used in every access check call in the system later. The return value or this method is the caller's session identifier.

The following sample implements a very simple provider that authenticates the caller against a fictive custom catalog. In the sample there is no code provided for communication with the external catalog - the user, password and her group memberships are just hard-coded. In a real-world scenario, the real work will be to implement the communication with the custom catalog and the logic to identify the caller from her logon token and the task to create the sids of the user and her groups.

Exceptions

There are two special exceptions that the custom login provider should throw on logon problems

CSCustomAuthenticationTokenException
The provider throws this exception when it finds that the Token passed in has format that is not valid for the underlying authentication system. When using multiple authentication system this can indicate that the wrong custom provider has been used.
CSCustomAuthenticationFailedException
The provider throws this exception when it finds that the Token passed in is valid for this system but has expired or cannot be mapped to a user in the external system. When the calling code (ex. Content Studio web site) catches this exception it knows that it should redirect the caller to the login page again.
using System;
using System.Security.Principal;

namespace AuthProviders
{
    public class CustomAuthProvider : ContentStudio.Security.AuthenticationBase
    {
        /// <summary>
        /// Opens a new session, or retrieves the session identifier for a 
        /// user that exists in an external catalog
        /// </summary>
        /// <param name="connectionID">The site identifier</param>
        /// <param name="token">A Token that identifies the user. 
        /// This data can be the result of a logon procedure.</param>
        /// <returns>The session identifier for the user.</returns>
        public override int OpenSession(int connectionID, object token)
        {
            try
            {
                //check the Token passed in with the custom catalog.
                //Test the format of the token, if bad fromat throw an error
                if(!IsValidTokenformat(token))
                {
                    throw new CSCustomAuthenticationTokenException("Invalid logon token format");
                }
                
                //The custom catalog will identify the user based on her Token and return her
                //personal data.
                SecurityIdentifier sid = identifyUser(token);
                if (sid == null)
                {
                    //Login failed, no such token found. The caller must logon again
                    throw new CSCustomAuthenticationFailedException("Invalid logon, caller must logon again!");
                }
                //perhaps the caller has a valid session already?
                int SessionID = GetCallerSessionID(connectionID, sid);
                if (SessionID > 999)
                    return SessionID;
                //Must do a full open session, since that caller has no valid session at this moment.

                //Get user personal information
                string Domain, UserName, Fullname, Description, Email;
                getUserData(sid, 
                            out Domain, 
                            out UserName, 
                            out Fullname, 
                            out Description, 
                            out Email);
                int UserID;
                //Register the user with Content Studio
                RegisterUser(connectionID, 
                             sid, 
                             Domain, 
                             UserName, 
                             Fullname, 
                             Description, 
                             Email, DateTime.MaxValue,
                             out UserID);
                //Get the sids of the security groups the user should be a member of
                bool more = true;
                int PageNo = 1;
                while (more)
                {
                    SecurityIdentifier[] sids = getUserGroups(sid, PageNo++, out more);
                    //map the groups to the user
                    SaveUserGroupMapping(connectionID,
                                         UserID,
                                         out SessionID,
                                         sids[0],
                                         sids[1],
                                         sids[2],
                                         sids[3],
                                         sids[4],
                                         sids[5],
                                         sids[6],
                                         sids[7],
                                         sids[8],
                                         sids[9]);
                }
                return SessionID;
            }
            catch (Exception ex)
            {
                throw new Exception("Cannot open session\r\n" + 
                                    ex.GetType().ToString() + ": " + ex.Message);
            }
        }
        /// <summary>
        /// Identifies the user in the external catalog based on the Token passed in.
        /// </summary>
        /// <param name="token">A Token that identifies the user. 
        /// This data can be the result of a logon procedure.</param>
        /// <returns>A security identifier created by the external catalog.
        /// This value must be deterministic and always return the same value for 
        /// each unique user.</returns>
        private SecurityIdentifier identifyUser(object token)
        {
            if(token.ToString() == "cr568fad89")
                return new SecurityIdentifier("S-1-150-21-493728-38943583-34927-1007");
            //no valid token for the user
            return null;
        }
        ///<summary>
        /// Tests if a passed in token is valid for this system.
        /// </summary>
        /// <returns>
        /// true if the token is valid, false otherwise.
        /// </returns>
        
        private bool IsValidTokenformat(object token)
        {
            if(token == null)
                return false;
            if(token.ToString().Length == 10)
                return true;
            return false;
        }


        /// <summary>
        /// Returns personal information about a user
        /// </summary>
        /// <param name="sid">The users security identifier in the external catalog</param>
        /// <param name="Domain">When the method return contains the Domain name of the caller's account. 
        /// User name in combination with domain must be unique. This parameter is passed uninitialized.</param>
        /// <param name="UserName">When the method return contains the user name of the user. 
        /// User name in combination with domain must be unique. This parameter is passed uninitialized.</param>
        /// <param name="Fullname">When the method return contains the full name of the user.
        ///  This parameter is passed uninitialized.</param>
        /// <param name="Description">When the method return contains the description of the user. 
        /// This parameter is passed uninitialized.</param>
        /// <param name="Email">When the method return contains the  
        /// This parameter is passed uninitialized.</param>
        private void getUserData(SecurityIdentifier sid, 
                                 out string domain, 
                                 out string userName, 
                                 out string fullname, 
                                 out string description, 
                                 out string email)
        {
            //Values below is retrieved from the extenal user catalog based on 
            //the passed in security identifier
            domain = userName = fullname = description = email = null;
            if (sid.Value == "S-1-150-21-493728-38943583-34927-1007")
            {
                domain = "CONTENTSTUDIO";
                userName = "JACCREE";
                fullname = "Jacob Creek";
                description = "A nice person in our organization";
                email = "jacob.creek@contentstudio.com";
            }
        }
        /// <summary>
        /// Gets the groups of the caller
        /// </summary>
        /// <param name="sid">The security identifier of the user whose external 
        /// provider groups should be inserted</param>
        /// <param name="PageNo">The data page number to get data for</param>
        /// <param name="moreGroups">After the call this parameter indicates 
        /// whether there are more groups to get. This parameter is passed uninitialized.</param>
        /// <returns>An array of 10 member containg the security identifiers of the group 
        /// where the caller belongs</returns>
        private SecurityIdentifier[] getUserGroups(SecurityIdentifier sid, 
                                                   int pageNo, 
                                                   out bool moreGroups)
        {
            //Add some standard groups that all of our users should be members of
            SecurityIdentifier[] sids = new SecurityIdentifier[10];
            sids[0] = new SecurityIdentifier(WellKnownSidType.WorldSid, null); //Everyone
            sids[1] = new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null); //Authenticated users
            sids[2] = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); //Users
            //Values below is retrieved from the extenal user catalog.
            //unless these groups are registered they will be ignored.
            sids[3] = new SecurityIdentifier("S-1-150-21-493728-38943583-34927-9"); //External group
            sids[4] = new SecurityIdentifier("S-1-150-21-493728-38943583-34927-10"); //External group
            //no more groups for this user found in our catalog
            moreGroups = false;
            return sids;
        }
    }
}

Importing groups from the external catalog


If you would like Content Studio to be able to use security groups from the external catalog you must import these groups into Content Studio. As with users, the program used to import groups must communicate with the external catalog and obtain a list of groups or at least provide some search capabilities. As with users, the external catalog or your import utility must be able to convert any group to a SID in such a way that this value always remains as long as the group exists and that the SID never will be reused if the group is deleted and later recreated with the same name. A developer that wants to implement a custom registration utility can do this using the regular public Content Studio API. The Group class contains an overloaded implementation of its Verify method that allows you to pass a group name in the DOMAIN\GROUPNAME format, a description and a SID without making any verification against the computer security groups or the network security groups.

The following small sample shows a class that lists and registers external groups in Content Studio. This class can be imported to the App_Code library making it available to every page on a web site.

Note 
The group register module does not need to register in Content Studio server and for this reason, unlike the provider, it can be called directly!


using System;
using System;
using System.Security.Principal;

namespace AuthProviders
{
    public class GroupRegister
    {
        //A small class that acts as container for found groups
        public class ExternalGroupItem
        {
            SecurityIdentifier _SID;
            String _GroupName;
            String _DomainName;
            String _Description;
            public ExternalGroupItem(SecurityIdentifier SID,
                                     String DomainName,
                                     String GroupName,
                                     String Description)
            {
                _SID = SID;
                _DomainName = DomainName;
                _GroupName = GroupName;
                _Description = Description;
            }
            public SecurityIdentifier SID
            {
                get { return _SID; }
            }
            public String GroupName
            {
                get { return _GroupName; }
            }
            public String DomainName
            {
                get { return _DomainName; }
            }
            public String Description
            {
                get { return _Description; }
            }
        }

        private int _SessionID;
        private int _ConnectionID;

        public GroupRegister(int ConnectionID, int SessionID)
        {
            _SessionID = SessionID;
            _ConnectionID = ConnectionID;
        }
        
        //returns a list of groups from the fictive external catalog
        public ExternalGroupItem[] GetExternalGroups()
        {
            ExternalGroupItem[] egis = new ExternalGroupItem[2];
            egis[0] = new ExternalGroupItem(new SecurityIdentifier("S-1-150-21-493728-38943583-34927-9"),
                                            "CONTENTSTUDIO",
                                            "SiteEditors",
                                            "This group from the CS catalog has members that acts as site editors");
            egis[1] = new ExternalGroupItem(new SecurityIdentifier("S-1-150-21-493728-38943583-34927-10"),
                                            "CONTENTSTUDIO",
                                            "SiteUsers",
                                            "This group from the CS catalog has members that can use the site");
            return egis;
        }

        //Registers or updates the Content Studio registration of an external group
        public int ImportOrVerify(ExternalGroupItem item)
        {
            ContentStudio.Security.Group group = new ContentStudio.Security.Group();
            return group.Verify(_ConnectionID, 
                                _SessionID, 
                                item.SID.ToString(), 
                                item.DomainName, 
                                item.GroupName, 
                                item.Description, 
                                false);
        }
    }
}    

Calling this class is straight forward and can be done on a custom aspx page, the sample below contains a submit button whose click event is mapped to the onRegisterGroups event handler show below.

protected void onRegisterGroups(Object Sender, EventArgs e)
{
  AuthProviders.GroupRegister gprg = new AuthProviders.GroupRegister(CS_ConnectionId, CS_UserSessionId);
  AuthProviders.GroupRegister.ExternalGroupItem[] exgroups = gprg.GetExternalGroups();
  foreach(AuthProviders.GroupRegister.ExternalGroupItem exgi in exgroups )
  {
    gprg.ImportOrVerify(exgi); 
  }
}

Installing the provider

Your provider is called by the Content Studio Web application when it finds that a custom authentication mechanism is in use with the current client. Content Studio calls an overloaded version of the OpenSession method that in turn calls the external providers implementation of the OpenSession method.
In order to be able to install the provider you must be logged in with administrative privileges on the server where Content Studio is installed.

  1. Copy the assembly containing your provider to the same folder as the Content Studio binaries (e.g. CS5ServiceHost.exe) are installed (ex. C:\Program Files\Teknikhuset\Content Studio5\CSServer).
  2. Register your provider with Content Studio by adding a new registry value under the key HKEY_LOCAL_MACHINE\SOFTWARE\Teknikhuset\Content Studio\5.0\AuthProviders. The value should be a regular string value and should have the same name as the name of your provider ex. (MyProvider). The value should be the moniker string that is used to reference your provider. Normally it has the following format:

    NS-CLASS, ASSEMBLY, Version=VERSION, Culture=CULTURE, PublicKeyToken=TOKEN

    Legend
    NS-CLASS
    The namespace of the provider and the name of the class that implements the provider (ex. MyProviders.TheCustomProvider)
    ASSEMBLY
    The name of the assembly hosting the code. This is normally the name of your dll minus the file extension.
    VERSION
    The version of your assembly ex. 1.0.0.1
    CULTURE
    The culture of your assembly if localized. Normally this is Neutral
    TOKEN
    The public key token of your assembly. Only used with signed assemblies. Use null if unsigned code.

    Using this pattern the reference fullname of the SessionManager class in the Content Studio server assembly can be written:

    ContentStudio.Security.SessionManager, CSServer5, Version=5.0.0.1, Culture=neutral, PublicKeyToken=A01ADABEA8411678

    In its simplest form an unsigned the assembly fullname can be written:
    AuthProviders.CustomAuthProvider, AuthProv

    This information is used by Content Studio in the OpenSession method when it registers the external login with the custom provider. 
     
    NOTE

    Ensure that the syntax, spelling and data are correct, any mistake made in this value and Content Studio will not be able to find your provider. 

If you would like to call your provider through Content Studio, you call the OpenSession method passing in the site's ConnectionID, your provider's name (as registered above) and the Token you received in the custom login process. This method will return the Content Studio session identifier that you use as an argument to all other API-calls in the Content Studio API.

Integrating with Content Studio

When using custom authentication with Content Studio you must use the Forms login method in ASP.NET. Forms authentication provides you with a way to authenticate users using your own code and then maintain an authentication token in a named cookie. When Content Studio discovers the forms login token cookie as a result of a successful login, Content Studio authenticates the user by calling the overloaded version of the OpenSession method.

To use forms authentication, you create a logon page that collects credentials from the user and that includes code to authenticate the credentials. If the credentials are valid, you can call methods of the FormsAuthentication class to redirect the request to the originally requested resource with an appropriate authentication ticket (cookie). If you do not want the redirection, you can simply get the forms authentication cookie or set it. In its very simplest for you can validate the user directly in the login page and then set the cookie as a token of the successful login, but normally you must create a custom membership provider.

Creating and using a Custom Membership Provider

A Membership Provider is a standard that ASP.NET uses when it needs to identify a user based on her passed in credentials and these providers must inherit from the System.Web.Security.MembershipProvider class.

This is a very simple membership provider that can be used to logon a user against our fictive system. In this case, like the examples above, the username and passwords are hard coded just and must, of course, be implemented in conjunction with the custom user catalog.

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.Security;

namespace MyMembershipProviders
{
    public class MyProvider : System.Web.Security.MembershipProvider
    {
        private string _AppName = null;
        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
        {
            if (config == null)
                throw new ArgumentNullException("config");

            if (String.IsNullOrEmpty(name))
                name = "MyProvider";

            if (String.IsNullOrEmpty(config["description"]))
            {
                config.Remove("description");
                config.Add("description", "My own membership provider");
            }

            // Initialize the abstract base class.
            base.Initialize(name, config);

            //Set the application name property
            _AppName = config["applicationName"];
            if (String.IsNullOrEmpty(_AppName))
                _AppName = System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath;

            //Other properties from the config object can be set here.
            //See the msdn documentation for more information.
        }
                       
        public override bool ValidateUser(string username, string password)
        {
            //Code to validate the user's name and password and return the token
            string token;
            if (doValidation(username, password, out token))
            {

                //Get the timeout value from Web.Config
                //get the current application path first
                string path = HttpContext.Current.Request.ApplicationPath;
                System.Configuration.Configuration configuration =
                    System.Web.Configuration.WebConfigurationManager.OpenWebConfiguration(path);
                //Get the system.web/authentication section.
                System.Web.Configuration.AuthenticationSection authenticationSection =
                    (System.Web.Configuration.AuthenticationSection)configuration.GetSection("system.web/authentication");
                int timeout = (int)authenticationSection.Forms.Timeout.TotalMinutes;

                //Create a ticket to set to the FormsCookie
                FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(2,
                  username,
                  DateTime.Now,
                  DateTime.Now.AddMinutes(timeout),
                  false, //cookie should not be persistent
                  token, //token goes into the UserData property
                  FormsAuthentication.FormsCookiePath);
                // Encrypt the ticket.
                string encTicket = FormsAuthentication.Encrypt(ticket);
                //set it
                HttpContext.Current.Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));

                //Optionally; set the cookie that Content Studio uses if it needs to 
                //get what provider is in use.
                //This is Content Studio specific code.
                //This cookie is used by Content Studio when it determines the name of the
                //provider that should be used during the authentication process. If you choose to
                //omit this cookie, Content Studio uses the name of the default provider. If existiing, this cookie
                //must have the same value as the name of the registry key that stores the type-information of your
                //custom authentication provider. 
                HttpContext.Current.Response.Cookies.Add(new HttpCookie("CS_CurrentProviderName", Name));
                return true;
            }
            return false;
        }

        /// <summary>Implements the actual login prodedure against the underlying catalog</summary>
        /// <param name="username">The login name of the calling user</param>
        /// <param name="password">The password provide by the calling user</param>
        /// <param name="token">After the call contains the proof of a successful login on the underlying catalog.
        /// This parameter is passed uninitalized.</param>
        /// <returns><b>true</b> as a result of a successful login, <b>false</b> otherwise.</returns>
        private bool doValidation(string username, string password, out string token)
        {
            if (username == "JACCREE" && password == "papp")
            {
                //the token of login will created as a result of a successful login.
                //this will of course be done against the external catalog.
                token = "cr568fad89";
                return true;
            }
            token = null;
            return false;
        }

        public override string ApplicationName
        {
            get
            {
                return _AppName;
            }
            set
            {
                _AppName = value;
            }
        }

        #region Not implemented members
        
        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override bool EnablePasswordReset
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override bool EnablePasswordRetrieval
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override int GetNumberOfUsersOnline()
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override string GetPassword(string username, string answer)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override string GetUserNameByEmail(string email)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override int MinRequiredPasswordLength
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override int PasswordAttemptWindow
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override bool RequiresUniqueEmail
        {
            get { throw new Exception("The method or operation is not implemented."); }
        }

        public override string ResetPassword(string username, string answer)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override bool UnlockUser(string userName)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void UpdateUser(MembershipUser user)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        #endregion
    }
}
        

Setting up custom authentication on the Web site

To set up Forms login you must create a login page and make some settings in the web.config file to indicate the provider to use and some additional settings as well. Additionally you might need to configure the IIS as well.

Creating the login page

The login page is essential when working with ASP.NET Forms login since the ASP.NET redirects the caller to this page whenever it finds that the caller no longer has a valid login.

Note
Instead of writing your own login page you can use the one shipped with the product. For more details see the Using the built in login page

A working sample login dialog ships with Content Studio and can be found in the System/Documents/Protected category. This can be used when testing the providers and as a template for your own dialog.

The easiest way of creating a login page is by using the Login ASP.NET Server control.

C#
// This will run on page load
protected void Page_Load(object sender, EventArgs e )
{
    // Enter code here...
    //Test if we already have a Windows login session, in that case
    //we must override the Forms login!
    string LogOnUser = Request.ServerVariables["LOGON_USER"];
    if (LogOnUser != String.Empty)
    {
        //Yes, leave this page
        FormsAuthentication.RedirectFromLoginPage(LogOnUser, false);
    }
    if(Request.UserAgent == "ContentStudioAdminAjax")
    {
        //Failed forms authentication found, write an Xml response
        //to the caller.
        Response.Clear();
        Response.ContentType = "text/xml";
        XmlWriterSettings sett = new XmlWriterSettings();
        sett.OmitXmlDeclaration = true;
        StringBuilder sbu = new StringBuilder();
        using(XmlWriter Xwri = XmlWriter.Create(sbu, sett))
        {
            Xwri.WriteStartDocument();
            Xwri.WriteStartElement("root");
            Xwri.WriteElementString("status", "-6");
            Xwri.WriteElementString("statustext", "Custom log in failed.");
            Xwri.WriteStartElement("forms");
            Xwri.WriteAttributeString("LoginUrl", FormsAuthentication.LoginUrl);
            Xwri.WriteAttributeString("FormsCookieName", FormsAuthentication.FormsCookieName);
            Xwri.WriteEndElement(); //forms
            Xwri.WriteEndElement(); //root
            Xwri.WriteEndDocument();
            Xwri.Flush();
        }
        Response.Write(sbu.ToString());
        Response.End();
    }
}
C#
protected void Login1_Authenticate(object sender, System.EventArgs e)
{
    if(Membership.ValidateUser(Login1.UserName, Login1.Password))
        Response.Redirect(FormsAuthentication.GetRedirectUrl(Login1.UserName, false), true);		
}

Configuring the Web site.

Important
The following section contains instruction to edit the Web.config file. Making changes to this file will affect how the Web site behaves and mistakes here can make the Web site or Content Studio stop working. Personnel that lack the necessary knowledge about this file should not make any changes to this file. In any case, make sure that you have a recent and valid backup of this file before making any changes!

There are some configurations to be done before the custom authentication works on the site. You must inform ASP.NET what membership provider to use and configure the Forms login properties to use.

Now, you should have entered something like the following configuration elements in Web.config.
NOTE
There are other elements as well in the configuration file as well! Do not remove and alter any of the other element other that the ones documented here!

  <configuration>
    <ContentStudio>
        <FormsAuthenticationModule>
           <PoviderNames>
              <add key="MyProvider" value="" />
           </PoviderNames>
        </FormsAuthenticationModule>
        <!-- more elements can follow -->
    </ContentStudio>
   
    <system.web>
        <membership defaultProvider="MyProvider">
           <providers>
              <add name="MyProvider"
                   type="MyMembershipProviders.MyProvider, MyProvider, version=1.0.0.1, culture=neutral, publicKeyToken=null"/>
           </providers>
        </membership>

        <authentication mode="Forms">		  
            <forms enableCrossAppRedirects="true" 
                   name="MyProvider" 
                   loginUrl="/MySite/MyUnit/LoginPages/80BABE79-11EF-46C2-99D7-2C372C6B2041.aspx" 
                   timeout="60" />
        </authentication>

        <machineKey
            validationKey="C50B3C89CB21F4F1422FF158A5B42D0E8DB8CB5CDA1742572A487D9401E3400267682B202B746511891C1BAF47F8D25C07F6C39A104696DB51F17C529AD3CABE"
            decryptionKey="8A9BE8FD67AF6979E7D20198CFEA50DD3D3799C77AF2B72F"
            validation="SHA1" />
                
        <authorization>
            <deny users="?" />
        </authorization>
    </system.web>
    
<configuration>

Before finished, you must check that the Web site runs in anonymous mode in Internet Information Services (IIS) since Windows login methods cannot be mixed with Forms authentication.

Using the built in login page

Content Studio ships with a ready to use login page located in the System/Documents/Protected category and to use it you just need to change the loginURL attribute in the authentication section.

<authentication mode="Forms">		  
  <forms enableCrossAppRedirects="true" 
         name="MyProvider" 
         loginUrl="/MySite/System/Documents/Protected/Login.aspx" 
         timeout="60" />
</authentication>

As before, you must substitute the word MySite with the name of your site.
You can use this page in your own solution or use it as a template for your own login page. The built in login page is read-only since it is a part of the Content Studio installation and can be updated when upgrading to later versions of the product.

Setting up custom authentication on the administrative Web site

In order for the administrative site to use custom authentication you must install the membership provider so that the administrative site can find it. Unless you have installed the provider in the GAC you must either copy the resulting assembly (.DLL) file into the /Bin directory of the administrative Web site or place a code file in the App_Code folder under the administrative Web site. The administrative site will inherit the settings from the site's Web.config file and will use the same login dialog and provider.

Also, you must ensure that also the administrative Web site inherits the configuration settings from the Web site, thus if any of the elements document above exists - just remove them. The configuration/system.web/authentication might exist as predefined in a standard installation and should thus be removed.

Finally, you must ensure that the administrative Web site runs in anonymous mode in Internet Information Services (IIS). Normally, the Content Studio admin Web site cannot be run in this mode but when using a custom authentication system this is required since under the hood, all users will run as the same Windows user account and Content Studio cannot distinguish different users from each other. The custom authentication system, however, provides Content Studio with a way to do so even if the users are anonymous.

Running Windows and Custom authentication simultaneously

When custom authentication is in use, you should also configure Content Studio to use the built in Windows authentication mechanism together with the custom authentication system. Since these two methods cannot exist simultaneously on the same Web site you will have to set up another Web site and its administrative virtual directory that points to the same set of files as the ordinary site. You must supply another url to these sites and make sure that this url:s is registered in the host header value of the site to create in IIS. In Content Studio you must also specify the alternate url in the two Multihoming settings in Content Studio.
On the Windows authentication enabled site, you must ensure that at least the admin Web site is set up not to support anonymous authentication and if custom authentication is enabled on the Web site itself you must turn off anonymous authentication for the site as well.

A Windows enabled site is necessary to perform some administrative tasks and provides the possibility to use Content Studio in case of a problem related to the custom authentication system or a communication problem with the custom catalog.

Note
Multihoming can only be implemented on a server operating system, thus Windows and Custom authentication cannot be set up on computers running on client operating system such as Windows XP Professional and Windows Vista Business.

Debugging your provider

To be able to debug you Authentication provider you must have Content Studio installed and running on your development computer.

Setting up the Membership provider for debugging

Setting up the Authentication provider for debugging

When you have set up the two debugging session you should be able to login on to Content Studio through the login page and step through the code both in the Membership provider and in the Authentication provider.