Adding Users To SharePoint With A Custom Membership Provider
Arguably the most significant function that you will execute against your membership provider data store is creating your SharePoint users that will be authenticated against your custom membership database. In this section, I will detail how the Universal Membership Provider will add users and in the next article we can see important security centric user methods, notably encrypting passwords and password answers, and how to use the machine key or the .NET cryptographic hash algorithm to achieve this task.
Creating New Users in the Membership Provider Data Store
The foremost task in creating a SharePoint user is setting up the class that is going to perform the function with the appropriate parameters. The parameters we are adding should be pretty self evident, we are passing the:
- username string
- password string
- email string
- passwordQuestion string
- passwordAnswer string
- isApprovied boolean value
- userId object
- status out of the MembershipCreateStatus enumeration
- public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object userId, out MembershipCreateStatus status)
There are two important portions to note for this particular class. The first is that we are overriding it because we are modifying the implementation of an inherited method, in this case MembershipUser, derived out of the System.Web.Security namespace, which allows us to update various pieces of information in the user data store.
Following, we are are going to set up the connection to the database by using DBconnection which will embody this connection:
- DbConnection universalDBconnection = null;
Next, we must construct the statement out of an interface provided by the System.Data namespace that will be executed when connected to the custom membership data source. We can use an interface because we are allowing classes to implement the methods defined in IDbCommand, in essence, we will create an instance of a class that will inherit from IDbCommand:
- IDbCommand universalDBcommand = null;
Then we have to determine if the user was created successfully. This is accomplished by using the MembershipCreateStatus enumeration which will parse out the result out of our CreateUser operation:
MembershipCreateStatus sharepointUserStatus = MembershipCreateStatus.Success;
Lastly, we are going to then expose the SharePoint user that we are going to create in the membership data store:
- MembershipUser sharepointUser = null;
Now, when creating the user, there are a a small number of things that we have to validate, and whether these conditions conform to what we require. Establishing an “if” loop to iterate through various problems prior to executing the creation of the user so we can throw any pre-commitment exceptions if they should occur during the processing. What we are going to check for is:
- if the password question is valid
- if the password answer is valid
- if the username is valid
- if the password is valid
This is done by using the MembershipCreateStatus enumeration (talked about in the above section) and ProviderException out of the System.Configuration.Provider namespace to throw intuitive messages if a problem does occur :
- if (this.RequiresQuestionAndAnswer && string.IsNullOrEmpty(passwordQuestion))
- sharepointUserStatus = MembershipCreateStatus.InvalidQuestion;
- throw new ProviderException("The question entered is not valid");
- if (this.RequiresQuestionAndAnswer && string.IsNullOrEmpty(passwordAnswer))
- sharepointUserStatus = MembershipCreateStatus.InvalidAnswer;
- throw new ProviderException("The answer entered is not valid.");
- if (string.IsNullOrEmpty(username))
- sharepointUserStatus = MembershipCreateStatus.InvalidUserName;
- throw new ProviderException("The user name is not valid.");
- if (!this.IsStrongPassword(password, out userStorage))
- sharepointUserStatus = MembershipCreateStatus.InvalidPassword;
- throw new ProviderException(userStorage);
Now we are almost ready to create the user. We have checked their password, it met our conditions, and all their other information seems to fit in with enterprise standards. The next part is actually performing hashing on the password and passwordanswer. This is the subject for the next article, however at this point we will call the hashing function by:
- password = this.hashPassword(password);
- passwordAnswer = this.hashPassword(passwordAnswer);
Since this calls a separate portion, it is discussed more exhaustively in a subsequent section. For the time being it is safe to just be aware that exists.
Now we are ready to start talking to the database. We are going to start by opening up a channel to the custom database to start making the relevant changes that we need while maintaining a proper locking behavior for the connection mechanism (using IsolationLevel). Firstly, we are going to simply going to create a new instance of the connection to the MOSS / WSS v3 membership data store and make the relevant connection string settings.
- universalDBconnection = this.provider.CreateConnection();
- universalDBconnection.ConnectionString = this.connectionString;
Then we are going to open the MOSS / WSS v3 membership data store by using those same connection string settings by calling Open() on the connection. Following, we are going to call the base class for the transaction and start the database connection by using shared locks to avoid dirty reads (this could however result in phantom data, but is a proper isolation level for this implementation purpose).
- DbTransaction sharepointMembershipTransaction = universalDBconnection.BeginTransaction(IsolationLevel.ReadCommitted);
Now we are at a pivotal portion of the user creation process since we are going to be writing to the database. Before we massage the data in, we are going to have to build some compensation in for a userID since it is the unique identifier for all user records.
- if ((userId != null) && !(userId is Guid))
- throw new ArgumentException("userId");
- if (userId == null)
- userId = Guid.NewGuid();
If a new GUID is created successfully, then we can call the CreateUser object and observe the success of our initial transaction (to check whether is was successful or not).
- this.usersProvider.CreateUser(oracleDBconnection, false, username, userId, true, out status);
- if (status != MembershipCreateStatus.Success)
- sharepointUserStatus = status;
- return sharepointUser;
Now, we are going to add the relevant information about the user, such as their:
Most of this should look very familiar, is is resembling the sqlmembershipprovider database setup by the SQL application server wizard when registering the initial database creation, see the table here.
This is where things will begin to get very database specific. We are going to have our first instance of using whatever database query language that you are using for your custom provider in this section. Looking at the code, it is pretty simple.
- universalDBcommand = this.CreateCommand("INSERT INTO aspnet_membership (email, passwordquestion, passwordanswer, isapproved, islockedout, creationdate, lastpasswordchangeddate, lastlockoutdate, userid, password) VALUES ()", universalDBconnection);
In this example, I am using what could either by Oracle or PostgreSQL, however this should translate into nearly every other syntax. We are calling CreateCommand to create a command object associated with the universalDBconnection, the last parameter between the (). The VALUES section is left intentionally blank, since this will often vary between systems.
Following, we are going to Get the IDataParameter collection and use add to add items to the IList. These items are all the relevant fields to a user as defined in the INSERT statement from above.
- universalDBcommand.Parameters.Add(this.CreateParameter("email", DbType.String, 0xff, email));
- universalDBcommand.Parameters.Add(this.CreateParameter("passwordquestion", DbType.String, 0xff, passwordQuestion));
- universalDBcommand.Parameters.Add(this.CreateParameter("passwordanswer", DbType.String, 0xff, passwordAnswer));
- universalDBcommand.Parameters.Add(this.CreateParameter("isapproved", DbType.Boolean, isApproved));
- universalDBcommand.Parameters.Add(this.CreateParameter("islockedout", DbType.Boolean, false));
- universalDBcommand.Parameters.Add(this.CreateParameter("creationdate", DbType.DateTime, currentTime)); universalDBcommand.Parameters.Add(this.CreateParameter("lastpasswordchangeddate", DbType.DateTime, currentTime));
- universalDBcommand.Parameters.Add(this.CreateParameter("lastlockoutdate", DbType.DateTime, currentTime));
- universalDBcommand.Parameters.Add(this.CreateParameter("userid", DbType.Guid, 0xff, userId));
- universalDBcommand.Parameters.Add(this.CreateParameter("password", DbType.String, 0xff, password));
The only part of this that may appear confusing is the 0xff. This is simply defining 255 characters, which should be plenty of space.
Finally, we are going to call ExecuteNonQuery() in order to execute all of statements against the connection object, and cause an affect on the relevant rows. Once the ExecuteNonQuery() has completed, we can create a New MembershipUser object by:
- sharepointUser = new MembershipUser(this.Name, username, userId, email, passwordQuestion, null, isApproved, false, currentTime, currentTime, currentTime, currentTime, currentTime);
and lastly by committing the database transaction by calling Commit().
We do also have to compensate for exceptions as the occur, which can be done by simply doing a standard exception block, and then using Rollback() to push the transaction back to its pending state.
- catch (Exception userTransactionException)
- throw userTransactionException;
In the next exception handler, we have to determine whether the provider returned an error that is not described by other MembershipCreateStatus enumeration values:
- catch (ProviderException createUserStatusNotDescribedException)
- if (this.WriteExceptionsToEventLog)
- this.WriteToEventLog(createUserStatusNotDescribedException, "CreateSharePointUser");
- if (sharepointUserStatus == MembershipCreateStatus.Success)
- sharepointUserStatus = MembershipCreateStatus.ProviderError;
- throw createUserStatusNotDescribedException;
And lastly, our final exception during the Create User process, we have to handled internal errors that may occur within the provider that may not map to any pre-existing exception handling routine by calling ProviderException:
- catch (Exception generalProviderExceptionError)
- if (this.WriteExceptionsToEventLog)
- this.WriteToEventLog(generalProviderExceptionError, "CreateSharePointUser");
- throw new ProviderException(this.exceptionMessage, generalProviderExceptionError);
The last step is to not leave the database connection open since we are through using it:
- if (universalDBconnection != null)