Archive

Posts Tagged ‘Membership Providers’

SharePoint: Resolve user through the particular Membership Provider

April 28th, 2014 No comments

    We developed a few SharePoint-based applications comprised of two parts: internal and public. The internal one is accessible for Domain users only, while the public one points to the Internet and virtually available for everyone. Each of the applications uses the Claims Based Authentication and is extended to have two zones: the Default zone represents the internal part, while the Internet zone is for the public one. The Claims Based Authentication of the Default and Internet zones operates over the NTLM Integrated Windows Authentication and the Forms Based Authentication (FBA), respectively. For FBA we used our custom Membership provider derived from the SqlMembershipProvider and Role manager derived from the SqlRoleProvider, while the users’ email addresses served as the logins to sign in to the system. The problem came out when users having a Domain account had used their Domain emails to register and sign in to the system through the public part.

Let’s say there is a Domain user SOMEDOMAIN\firstname.lastname with the email lastname@somedomain. The user decides to test our application and registers in the public part, entering his email lastname@somedomain. During the registration the code similar to the listed below is being performed:

string userEmail    = "lastname@somedomain";
string userPassword = "1234567";
string providerName = "SomeCustomProvider";
...
SqlMembershipProvider customProvider = 
                           Membership.Providers[providerName] as SqlMembershipProvider;
...
MembershipCreateStatus membershipCreateStatus;
MembershipUser membershipUser = customProvider.CreateUser(userEmail, 
									userPassword, userEmail, 
									null, null, true, Guid.NewGuid(), 
									out membershipCreateStatus);
...
if (membershipUser != null && membershipCreateStatus == MembershipCreateStatus.Success)
{
	SPWeb spWeb = ...;
	...
	SPUser spUser = spWeb.EnsureUser(membershipUser.UserName);
	...
	// add user to a SharePoint group
	SPGroup publicUsersGroup = ...;
	publicUsersGroup.AddUser(spUser);
	...
	// give spUser unique permissions and so on
}

So, right after the customProvider.CreateUser has been called, we still have the Domain account SOMEDOMAIN\firstname.lastname with the email lastname@somedomain plus we have a new user with the login lastname@somedomain and the identical email. The latter one is stored in database and managed by the custom membership provider.

To get the proper instance of SPUser, we call EnsureUser (it else could be SafeUserEnsure) and pass user’s login which, in the given case, is actually the user’s email. Behind the scenes SharePoint attempts to sequentially resolve the user through all of available membership providers. Besides our custom membership provider, there is another one provided by SharePoint itself, namely, SPClaimsAuthMembershipProvider (Microsoft.SharePoint.Administration.Claims.SPClaimsAuthMembershipProvider defined in Microsoft.SharePoint) which is the default provider (when you add your custom membership provider to the web.config files it’s strongly recommended to keep the SPClaimsAuthMembershipProvider as a default provider to avoid unexpected behaviour). Apparently the default provider is the first one resorted to resolve the user. The pitfall here is that the SPClaimsAuthMembershipProvider finds and returns the Domain user (evidently in this case the user is resolved by email). So, following our example, the received SPUser will reference the SOMEDOMAIN\firstname.lastname with the email lastname@somedomain. Having gotten the wrong SPUser, we add it to groups and grant some permissions. Of course, once the registered user tries to sign in to the public part of the application, he stumbles upon Access Denied error as he doesn’t have any permissions since all required ones were provided to the Domain user. So, below is how to resolve this ambiguity.

If we take a look at how the user registered through the custom membership provider looks like in the SharePoint we’ll see something like the following:

i:0#.f|somecustomprovider|lastname@somedomain

More information regarding this format of Claims you’ll find here. The most valuable fact for us right now is that such encoded user name contains the name of the membership provider which manages the user (following the example above, it’s somecustomprovider). Obviously, if we pass such encoded user name into EnsureUser, the right membership provider will be applied and, therefore, the right SPUser will be returned. So, let’s find a way to turn the usual user name (lastname@somedomain) into the encoded one (i:0#.f|somecustomprovider|lastname@somedomain).

Such transformation might be easily done by means of SPClaimProviderManager that allows managing available claims providers and supplies various utility methods to encode and decode claims. So, the method below accepts usual user name and name of membership provider and returns the encoded user name:

public static string GetFbaEncodedUserName(string userName, string membershipProviderName)
{
	if (!SPClaimProviderManager.IsEncodedClaim(userName))
	{
		SPClaim claim = SPClaimProviderManager.CreateUserClaim(userName, 
											SPOriginalIssuerType.Forms, 
											membershipProviderName);
		return claim.ToEncodedString();
	}
	return userName;
}

The method creates a claim and returns its encoded representation. The Fba in the method’s name implies that we deal with the Form Based Authentication hence the SPOriginalIssuerType.Forms type is passed to the SPClaimProviderManager.CreateUserClaim.

The code below illustrates how to use the method:

string userName     = "lastname@somedomain";
string providerName = "SomeCustomProvider";

// get the user name resembling i:0#.f|somecustomprovider|lastname@somedomain
string encodedUserName = GetFbaEncodedUserName(userName, providerName);

SPWeb spWeb;

// get the right SPUser instance
SPUser spUser = spWeb.EnsureUser(encodedUserName);

SharePoint: SqlMembershipProvider – Lock User

July 21st, 2013 No comments

    In addition to the article SharePoint: SqlMembershipProvider – Get All Users In Role, here is one more method to extend the SqlMembershipProvider with. It’s found out that the SqlMembershipProvider doesn’t provide a method to lock user. By default a user can be automatically locked after several frequent and failed attempts to login. To unlock such users the SqlMembershipProvider supplies with the UnlockUser method. But what if administrator wants to temporarily lock user for some reason? Unfortunately, there is no such method out-of-box.

So, let’s try to implement our own LockUser method. Two obvious steps for that are as follows: to create a Stored Procedure in database; to extend a class derived from the SqlMembershipProvider with the proper method.

LockUser Stored Procedure

The stored procedure is very simple as we need just to update one field in the aspnet_Membership table for appropriate user. Below is the script to create such procedure. Run the script on MembershipProvider database, in my case it’s aspnetdb.

USE [aspnetdb]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

-- =============================================
-- Author:      .Net Follower
-- Description:	Locks User
-- =============================================
CREATE PROCEDURE [dbo].[aspnet_Membership_LockUser]
	@ApplicationName                         nvarchar(256),
    @UserName                                nvarchar(256)
AS
BEGIN
	DECLARE @UserId uniqueidentifier
    SELECT  @UserId = NULL
    SELECT  @UserId = u.UserId
    FROM    dbo.aspnet_Users u, dbo.aspnet_Applications a, dbo.aspnet_Membership m
    WHERE   LoweredUserName = LOWER(@UserName) AND
            u.ApplicationId = a.ApplicationId  AND
            LOWER(@ApplicationName) = a.LoweredApplicationName AND
            u.UserId = m.UserId

    IF ( @UserId IS NULL )
        RETURN 1

    UPDATE dbo.aspnet_Membership 
    SET IsLockedOut = 1 WHERE @UserId = UserId

    RETURN 0
END

Custom Membership Provider

Now we can add the LockUser method to the custom Membership Provider called SqlMembershipProviderEx and shown in the article. The SqlMembershipProviderEx with the LockUser is listed below. Note that the methods mentioned in the previous article are skipped.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Globalization;
using System.Reflection;
using System.Web.Security;
using Microsoft.SharePoint;

namespace dotNetFollower
{
    public class SqlMembershipProviderEx : SqlMembershipProvider
    {       
	    ...
		
        public bool LockUser(string username)
        {
            bool flag = false;
            CheckParameter(ref username, true, true, true, 0x100, "username");

            DoInSqlConnectionContext(delegate(SqlConnection connection)
            {
                //this.CheckSchemaVersion(connection.Connection);
                SqlCommand command = new SqlCommand("dbo.aspnet_Membership_LockUser", connection)
                {
                    CommandTimeout = CommandTimeout,
                    CommandType    = CommandType.StoredProcedure
                };
                command.Parameters.Add(CreateInputParam("@ApplicationName", SqlDbType.NVarChar, ApplicationName));
                command.Parameters.Add(CreateInputParam("@UserName", SqlDbType.NVarChar, username));
                SqlParameter parameter = new SqlParameter("@ReturnValue", SqlDbType.Int)
                {
                    Direction = ParameterDirection.ReturnValue
                };
                command.Parameters.Add(parameter);

                command.ExecuteNonQuery();
                flag = ((parameter.Value != null) ? ((int)parameter.Value) : -1) == 0;
            });

            return flag;
        }

        protected internal static void CheckParameter(ref string param, bool checkForNull, bool checkIfEmpty, bool checkForCommas, int maxSize, string paramName)
        {
            if (param == null)
            {
                if (checkForNull)
                    throw new ArgumentNullException(paramName);
            }
            else
            {
                param = param.Trim();
                if (checkIfEmpty && (param.Length < 1))
                    throw new ArgumentException(string.Format("The parameter '{0}' must not be empty.", new object[] { paramName }), paramName);

                if ((maxSize > 0) && (param.Length > maxSize))
                    throw new ArgumentException(string.Format("The parameter '{0}' is too long: it must not exceed {1} chars in length.", new object[] { paramName, maxSize.ToString(CultureInfo.InvariantCulture) }), paramName);

                if (checkForCommas && param.Contains(","))
                    throw new ArgumentException(string.Format("The parameter '{0}' must not contain commas.", new object[] { paramName }), paramName);
            }
        }

        ...
        
    }    
}

The latest version of the SqlMembershipProviderEx along with all used additional classes are available to download here.

Related posts:

SharePoint: SqlMembershipProvider – Get All Users In Role

June 30th, 2013 No comments

    In the SharePoint application I’m currently working on, I configured Form Based Authentication (FBA) using the SqlMembershipProvider and SqlRoleProvider. Implementing some user management functionality, I run into the lack of a method to get the users in particular role by portions (so-called pagination). The SqlRoleProvider exposes the GetUsersInRole method which returns only names of users in the passed role and doesn’t support pagination. The direct way in this case is to get user names and then get appropriate users, calling the GetUser method of SqlMembershipProvider one time per name. This approach results in a bunch of requests to the database: one request is to get names of users in a role and a number of requests are to get each user by his name. In addition, we have somehow to implement pagination ourselves. The approach is acceptable, but let’s try to reduce requests to the database and borrow somewhere the pagination logic.

GetAllUsersInRole Stored Procedure

It’s interesting that the SqlMembershipProvider provides the GetAllUsers method that supports pagination. On the database level, every call of SqlMembershipProvider.GetUsersInRole and SqlMembershipProvider.GetAllUsers ends with executing such Stored Procedures as aspnet_UsersInRoles_GetUsersInRoles and aspnet_Membership_GetAllUsers respectively. So, we know that the aspnet_UsersInRoles_GetUsersInRoles searches for names of users in a role while the aspnet_Membership_GetAllUsers is able to return users by portions. Let’s combine these two Stored Procedures and create another one which would select users in a role and return a required portion of the result. The sql script below creates such Stored Procedure, I named it aspnet_Membership_GetAllUsersInRole. Note the script should be executed on MembershipProvider database, it’s aspnetdb in my case.

USE [aspnetdb]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

-- =============================================
-- Author:		.Net Follower
-- Description:	Returns users in role by portions
-- =============================================
CREATE PROCEDURE [dbo].[aspnet_Membership_GetAllUsersInRole]
    @ApplicationName       nvarchar(256),
    @PageIndex             int,
    @PageSize              int,
    @RoleName              nvarchar(256)
AS
BEGIN
    DECLARE @ApplicationId uniqueidentifier
    SELECT  @ApplicationId = NULL
    SELECT  @ApplicationId = ApplicationId FROM dbo.aspnet_Applications WHERE LOWER(@ApplicationName) = LoweredApplicationName
    IF (@ApplicationId IS NULL)
        RETURN 0

	DECLARE @RoleId uniqueidentifier
    SELECT  @RoleId = NULL

    SELECT  @RoleId = RoleId
    FROM    dbo.aspnet_Roles
    WHERE   LOWER(@RoleName) = LoweredRoleName AND ApplicationId = @ApplicationId

    IF (@RoleId IS NULL)
		RETURN 0

    -- Set the page bounds
    DECLARE @PageLowerBound int
    DECLARE @PageUpperBound int
    DECLARE @TotalRecords   int
    SET @PageLowerBound = @PageSize * @PageIndex
    SET @PageUpperBound = @PageSize - 1 + @PageLowerBound

    -- Create a temp table TO store the select results
    CREATE TABLE #PageIndexForUsers
    (
        IndexId int IDENTITY (0, 1) NOT NULL,
        UserId uniqueidentifier
    )

    -- Insert into our temp table
    INSERT INTO #PageIndexForUsers (UserId)
    SELECT u.UserId
    FROM   dbo.aspnet_Membership m, dbo.aspnet_Users u, dbo.aspnet_UsersInRoles ur
    WHERE  u.ApplicationId = @ApplicationId AND u.UserId = m.UserId AND 
		   u.UserId = ur.UserId AND @RoleId = ur.RoleId
    ORDER BY u.UserName

    SELECT @TotalRecords = @@ROWCOUNT

    SELECT u.UserName, m.Email, m.PasswordQuestion, m.Comment, m.IsApproved,
            m.CreateDate,
            m.LastLoginDate,
            u.LastActivityDate,
            m.LastPasswordChangedDate,
            u.UserId, m.IsLockedOut,
            m.LastLockoutDate
    FROM   dbo.aspnet_Membership m, dbo.aspnet_Users u, #PageIndexForUsers p
    WHERE  u.UserId = p.UserId AND u.UserId = m.UserId AND
           p.IndexId >= @PageLowerBound AND p.IndexId <= @PageUpperBound
    ORDER BY u.UserName
    RETURN @TotalRecords
END

Custom Membership Provider

Now let’s extend our Membership Provider with a new method that deals with the aspnet_Membership_GetAllUsersInRole. I created a class SqlMembershipProviderEx derived from SqlMembershipProvider and containing the target GetAllUsersInRole method. The class is demonstrated below, but first of all a few remarks on the code:

  • I had to use Reflection to get values of some important fields (like Connection String to the database, for example) as Microsoft makes everything private or internal;
  • The SqlMembershipProvider elevates privileges when opening SqlConnection. Since the extended Membership Provider is going to be used in SharePoint application, I did the same by means of SPSecurity.RunWithElevatedPrivileges. Note however that if you want to use the extended Membership Provider in a pure ASP.Net application you will need to deal with such internal (of course) classes as SqlConnectionHolder and ApplicationImpersonationContext through Reflection;
  • The code of GetAllUsersInRole method is mainly based on the GetAllUsers of the parent SqlMembershipProvider class.
using System;
using System.Data;
using System.Data.SqlClient;
using System.Reflection;
using System.Web.Security;
using Microsoft.SharePoint;

namespace dotNetFollower
{
    public class SqlMembershipProviderEx : SqlMembershipProvider
    {
        protected string _connectionString;
        protected int?   _sqlCommandTimeout;

        protected string ConnectionString
        {
            get 
            { 
                return _connectionString ?? 
                    (_connectionString = Convert.ToString(this.GetFieldValue("_sqlConnectionString"))); 
            }
        }

        protected int CommandTimeout
        {
            get
            {
                if (_sqlCommandTimeout == null)
                    _sqlCommandTimeout = Convert.ToInt32(this.GetFieldValue("_CommandTimeout"));
                return _sqlCommandTimeout.Value;
            }
        }

        public MembershipUserCollection GetAllUsersInRole(string role, int pageIndex, int pageSize, out int totalRecords)
        {
            if (pageIndex < 0)
                throw new ArgumentException("The pageIndex must be greater than or equal to zero.", "pageIndex");
            if (pageSize < 1)
                throw new ArgumentException("The pageSize must be greater than zero.", "pageSize");
            
            long num = ((pageIndex * pageSize) + pageSize) - 1;
            if (num > 0x7fffffff)
                throw new ArgumentException("The combination of pageIndex and pageSize cannot exceed the maximum value of System.Int32.", "pageIndex and pageSize");
            
            MembershipUserCollection users = new MembershipUserCollection();
            int recordsAmount = 0;

            DoInSqlConnectionContext(delegate(SqlConnection connection)
                {
                    //this.CheckSchemaVersion(connection.Connection);
                    SqlCommand command     = new SqlCommand("dbo.aspnet_Membership_GetAllUsersInRole", connection);
                    SqlDataReader reader   = null;
                    SqlParameter parameter = new SqlParameter("@ReturnValue", SqlDbType.Int);
                    command.CommandTimeout = CommandTimeout;
                    command.CommandType    = CommandType.StoredProcedure;
                    command.Parameters.Add(CreateInputParam("@ApplicationName", SqlDbType.NVarChar, ApplicationName));
                    command.Parameters.Add(CreateInputParam("@PageIndex", SqlDbType.Int, pageIndex));
                    command.Parameters.Add(CreateInputParam("@PageSize", SqlDbType.Int, pageSize));
                    command.Parameters.Add(CreateInputParam("@RoleName", SqlDbType.NVarChar, role));
                    parameter.Direction = ParameterDirection.ReturnValue;
                    command.Parameters.Add(parameter);
                    try
                    {
                        reader = command.ExecuteReader(CommandBehavior.SequentialAccess);
                        while (reader.Read())
                        {
                            string   nullableString          = GetNullableString(reader, 0);
                            string   email                   = GetNullableString(reader, 1);
                            string   passwordQuestion        = GetNullableString(reader, 2);
                            string   comment                 = GetNullableString(reader, 3);
                            bool     boolean                 = reader.GetBoolean(4);
                            DateTime creationDate            = reader.GetDateTime(5).ToLocalTime();
                            DateTime lastLoginDate           = reader.GetDateTime(6).ToLocalTime();
                            DateTime lastActivityDate        = reader.GetDateTime(7).ToLocalTime();
                            DateTime lastPasswordChangedDate = reader.GetDateTime(8).ToLocalTime();
                            Guid     providerUserKey         = reader.GetGuid(9);
                            bool     isLockedOut             = reader.GetBoolean(10);
                            DateTime lastLockoutDate         = reader.GetDateTime(11).ToLocalTime();
                            users.Add(new MembershipUser(Name, nullableString, providerUserKey, email, passwordQuestion,
                                                         comment, boolean, isLockedOut, creationDate, lastLoginDate,
                                                         lastActivityDate, lastPasswordChangedDate, lastLockoutDate));
                        }
                    }
                    catch (Exception ex)
                    {
                        EventLogger.WriteError(ex);
                        throw;
                    }
                    finally
                    {
                        if (reader != null)
                            reader.Close();
                        if (parameter.Value is int)
                            recordsAmount = (int)parameter.Value;
                    }
                });
            totalRecords = recordsAmount;
            return users;
        }

        protected void DoInSqlConnectionContext(Action<SqlConnection> action)
        {
            SqlConnection connection = null;
            try
            {
                connection = new SqlConnection(ConnectionString);
                SPSecurity.RunWithElevatedPrivileges(connection.Open);
                action(connection);
            }
            finally
            {
                if (connection != null)
                    connection.Close();
            }
        }

        protected SqlParameter CreateInputParam(string paramName, SqlDbType dbType, object objValue)
        {
            SqlParameter parameter = new SqlParameter(paramName, dbType);
            if (objValue == null)
            {
                parameter.IsNullable = true;
                parameter.Value      = DBNull.Value;
                return parameter;
            }
            parameter.Value = objValue;
            return parameter;
        }

        protected string GetNullableString(SqlDataReader reader, int col)
        {
            return !reader.IsDBNull(col) ? reader.GetString(col) : null;
        }
    }
}

Note the EventLogger class is described in the post SharePoint: Simple Event Logger while the GetFieldValue method is provided by ReflectionHelper described in the C#: How to set or get value of a private or internal field through the Reflection and C#: How to set or get value of a private or internal property through the Reflection.

The latest version of the SqlMembershipProviderEx along with all used additional classes are available to download here.

Related posts: