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:
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-1–5-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
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
-
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 toRegisterUser , 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
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 theIn order to be able to install the provider you must be logged in with administrative privileges on the server where Content Studio is installed.
- 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).
- 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
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
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.
- Create a new class library project in Visual Studio and set a reference to the System.Configuration and System.Web assemblies. In the example below the project and its resulting assembly is named CustMemProv.
- Add a new class (in the example it is named MyProvider and let it inherit the System.Web.Security.MembershipProvider class.
- There are a lot of members that must be implemented but whether or not they actually do anything depends whether you need the specific feature the member actually provides. For example there is a method called GetMemberByEmail that identifies a user in you catalog by the user's email address, but it might not be needed. For those members not supported it is a good idea to throw a NotImplemented exception.
- However, there are a few members that must do something:
- The Initialize method
This method gets is called when the provider is initialized and receives the name of the provider and a collection of settings that can be used to initialize the provider. You should also set the ApplicationName property and optionally the Description property inherited from the ultimate base class Provider. Before leaving this method you must initialize the provider by calling the Initialize method of the Provider base class. - The ValidateUser method:
This method receives the user's credentials and return true if the logon process was successful. It also sets the authentication token cookie, which Content Studio will use to authenticate the user. The name of this cookie is stored in Web.config and retrieved by the FormsAuthentication.FormsCookieName property. This cookie must be a serialized and encrypted instance of the FormsAuthenticationTicket class. You must also obtain the timeout value for the forms authentication ticket from the Web.Config file. The FormsAuthenticationTicket object has a UserData property that should be used to store the token obtained from the login method of your system. The example below shows a complete implementation of this method that can be used in your own provider.
Optionally, you can inform Content Studio about the name of your provider by setting a cookie named CS_CurrentProviderName to the name of registry key that stores the type information of your authentication provider. This cookie can be used if you want to use a provider other than the default one or if the registry value that contains the type string of you custom authentication provider has a name different from the name of the membership provider. - For stability it is recommended that you implement the ApplicationName property as well as the example below demonstrates.
- The Initialize method
- When you build your own provider you can start with the code sample below and just implement the private doValidation method. If you need additional functionality or properties you implement these accordingly. Compile and build your provider and copy the resulting .DLL file to the /Bin directory of the Administrative Content Studio site. For the site you must upload the .DLL file to the System/Assemblies category or create a new code file in the System/App_Code/CSCode category and paste in the whole content of the code file.
- For more information see the article Sample Membership Provider Implementation in the MSDN-documentation.
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.
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.
- Create a new document in Content Studio in category that is of the type HTML with Meta data
- Add a new form to the page and set it to run at the server by using the form element's property dialog.
- From the Toolbox, select the Login control, drop it on the new document and accept the default name Login1.
- You now need to write code in the Login control's Authenticate event and in the Load event of the page itself.
- Switch to the Code behind view and paste in the following code on the Page_Load event handler
// 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();
}
}
- Switch back to the Normal view.
- Generate an event handler in code behind by right click on the Login control and select the Generate standard event handler option. This will create an event handler code block in the code behind view.
- In the generated event handler (Login1_Authenticate), paste in the following code block.
protected void Login1_Authenticate(object sender, System.EventArgs e)
{
if(Membership.ValidateUser(Login1.UserName, Login1.Password))
Response.Redirect(FormsAuthentication.GetRedirectUrl(Login1.UserName, false), true);
}
- When ready, approve your login page and record the name and path of the resulting ASPX-file. You will need this information later when you configure the login page setting in the site's Web.config file.
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.
- Make a backup of the Web.config file on the site!
- Under the
configuration/system.web
section in the Web.config file on the site add the following section.<membership defaultProvider="MyProvider"> <providers> <add name="MyProvider" type="MyMembershipProviders.MyProvider, MyProvider, version=1.0.0.1, culture=neutral, publicKeyToken=null"/> </providers> </membership>
The following attributes must be specified.
- name
- This attribute specifies the name of your membership provider.
- type
-
The type name value is exemplified below and the only part needed is the first part
specifying the NAMESPACE.CLASS_NAME unless your membership provider is
compiled, then at least the assembly name is required.
ASP.NET must be able to locate your assembly, thus you must include it in a code file that you create in the System/App_Code/CSCode Content Studio folder (or VBCode if you use Visual Basic). Alternatively you can compile the code as a code library (.DLL) and upload it to the System/Assemblies Content Studio folder or install the library in the GAC.
When the code exists in the App_Code directory only the NAMESPACE.CLASS_NAME can be provided.
- Locate the configuration/system.web/authentication element in the Web.config
file and replace it with the following configuration.
<authentication mode="Forms"> <forms enableCrossAppRedirects="true" loginUrl="/MySite/MyUnit/LoginPages/80BABE79-11EF-46C2-99D7-2C372C6B2041.aspx" timeout="60" /> </authentication>
- enableCrossAppRedirects
- This attribute must be true if you want to use the same login page both on the site and on the administrative Web site.
- If you set this attribute to false or omit it, a logged in user in the administrative site will experience a separate login dialog in the administrative Web site's Web view.
- loginUrl
- This attribute defines the path to the login page in the file system. To make testing easier a simple, write protected, login dialog is shipped with Content Studio that can be used as a template for your own login dialogs as well. The path to this page is /MySite/System/Documents/Protected/LogIn.aspx (replace MySite with the actual site name).
- timeout
- This attribute defines how long (in minutes) a login session is valid after the last activity from the user. The default value (defined in the Machine.config file) is 30 minutes.
-
Breaking changes in version 5.1
Starting with version 5.1, forms login no longer integrates with the Content Studio security system by default. In order to integrate with Content Studio a new configuration is needed. The installation program automatically upgrades Web.config and installs a new configSection and a number of sectionGroups elements. These configuration elements defines the configuration element that is used to supply Content Studio specific settings.
The sectionGroup named
FormsAuthenticationModule
defines the setting where all forms authentication providers that integrates with the Content Studio security system are defined.<configuration> <configSections> <sectionGroup name="ContentStudio"> <sectionGroup name="FormsAuthenticationModule"> <section name="ProviderNames" type="System.Configuration.NameValueSectionHandler"/> </sectionGroup> <!-- more Content Studio sectionGroups can exist --> </sectionGroup> <!-- other sectionGroups might exist --> </configSections> <!-- other elements --> <configuration>
In Web.config directly under the
<configuration>
element there is a section named<ContentStudio>
where the<FormsAuthenticationModule>
element should exist. The installation program upgrades Web.config an inserts this section as well, but in a default installation the add element is commented out.
In order for your provider to integrate with Content Studio, remove the comment and in thekey
attribute replace theMyProviderName
value with the name of your provider. Thevalue
attribute is ignored and should be an empty string as shown in the example below. Content Studio now knows that your provider should integrate with is underlying security system.<configuration> <ContentStudio> <FormsAuthenticationModule> <ProviderNames> <add key="MyProviderName" value=""/> </ProviderNames> </FormsAuthenticationModule> <ContentStudio> <!-- more elements exist --> <configuration>
- Now, you should specify a machine encrypting key into the configuration/system.web/
section as well. The machine key is used to encrypt and decrypt authentication ticket
and normally this key is auto-generated by ASP.NET at runtime. Unfortunately this
automate generated key is valid per application only and since the Web site and
its underlying administrative web site are separate application they cannot share
the same encrypted key. This can be fixed by adding a common encryption key that
both the applications can use. Since the administrative site will inherit configurations
made in the Web site they can share this key easily.
Add the following element to the configuration/system.web/ section in the Web.config file:<machineKey validationKey="7AD4CA6B62E7773ADBC2CAA31ECF18E8E5830D2997F5D69D360E36DB66CCD3E4559E8B18B1AF9986EAE1D5E51FF496322A1879BCDBF87E839CB6C7ADE83C5893" decryptionKey="44DB73F6B39B601235B80E6321B3E510EA774DC740FABE6B" validation="SHA1" />
The keys above are hexadecimal values and shown here are just examples of such keys. In a real world scenario you should specify your own keys, but for development and testing the keys exemplified should be sufficient.
There is a tool available that helps at http://aspnetresources.com/tools/keycreator.aspx that can create a valid machineKey element. The subject of machine key configurations is fairly complex and has been documented in details on the machineKey topic in the MSDN documentation. - The last element to configure is the configuration/system.web/authorization
element. By default this element is missing in a standard Content Studio installation,
so you must add it. This section instructs ASP.NET that all pages require an authenticated
user.
This element is simple to grasp and the following configuration should be sufficient:
<authorization> <deny users="?" /> </authorization>
Note
To turn off custom authentication you omit this element or specifies the following element wich is the default value specified in the Machine.config file:
<authorization> <allow users="*" /> </authorization>
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
- Set up a Content Studio Web site that uses your membership provider.
- Start Visual Studio and select in the File menu: - Open/Web site and select the Content Studio web site.
- If you have developed the membership provider as a compiled .DLL build it now and copy the resulting file to the Bin directory of the site.
- On the other hand if you have implemented the membership provider as a code file uploaded in the App_Code folder you should be able to find this file in you project and open it and set the breakpoints necessary.
Setting up the Authentication provider for debugging
- Set up a Content Studio Web site that uses your membership provider.
- Open your project in Visual Studio.
- Build your provider in Debug mode and copy the resulting file to the library where Content Studio is installed.
- In the debug menu in Content Studio, select the Attach to process... command.
- In the process Window, locate the process named CS5ServiceHost.exe, and press the Attach button.
- Your project now starts and you will be able to set breakpoints in you implementation of the OpenSession method.
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.