Archive

Archive for April, 2014

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);