WCF Wrapper for ASP.Net Membership
I haven't done a programming article in quite a while. The Sql Server Code Generator article continues to get a decent amount of hits so I thought it would be nice to give again and post some more code from one of my current projects.
A bit of background before I get started. The web site I am working on will require access from a variety of clients. The current list includes web browsers on all OSes and dedicated applications on Windows Phone 7 (via Silverlight), iOS 4(iPhone/iPad/iTouch) and Android. What I wanted to provide to these platforms was a unified method of authenticating against the backend services. Did I mention that I need to get this up and running as quickly as possible. My only other real goal was to build it in such a way that piece of it could be factored out for scalability.
Since I am using ASP.Net to create the web site, I immediately looked to the provider model for help. ASP.Net comes with a pretty robust Sql Server based user database. Will it scale to a million users? I'm not sure but considering that I don't have the first user, I figured it was a good place to start.
The first problem I have with the ASP.Net SqlMembershipProvider is that it requires the web site to have access to SQL Server which precludes the use of a web farm in a DMZ without opening Sql Server ports and exposing my internal architecture. That wasn't going to cut it for my needs. After Googling for solutions, I didn't come up with much so I architected my own. Here's a short image of what that architecture looks like.

public classMembershipService : SqlMembershipProvider
{
private MembershipProvider provider = Membership.Provider;
[OperationContract]
public override String ApplicationName
{
get { return provider.ApplicationName; }
set { provider.ApplicationName = value; }
}
[OperationContract]
public override MembershipUser GetUser(string username, bool userIsOnline)
{
return provider.GetUser(username, userIsOnline);
}
[OperationContract]
public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
{
return provider.GetUser(providerUserKey, userIsOnline);
}
...
}
There’s three things wrong with the above code.
Here’s the proper code:
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
All of the other methods of MembershipProvider translate properly across the web service boundary so let’s take a look at the custom membership provider, WCFMembershipProvider, that we will use on the DMZ web farm to gain access to membership.
public class WCFMembershipProvider : MembershipProvider
There’s a couple of interesting points to make here. The first is to point out how I used the methods GetApplicationName() and SetApplicationName(string) to marshal the property MembershipProvider.ApplicationName. It’s pretty straight forward and easy to implement. What confused me the most was the CreateUser method and any other method with an “out” parameter. Visual Studio 2010 seems to reorder the parameters when generating the service client. All “out” parameters come first when it is clearly visible that they are last in the OperationContract method declaration. I couldn’t figure out why this was the case so I just moved the parameters and went on.
The only other gotcha for me, was the system.serviceModel definitions in the app.config of the DLL need to be copied into the web.config of the calling web site. In the end, here is what my client web.config looked like.
<?xmlversion="1.0"?>
That pretty much does it. One of the interesting ideas I can think of for this code is that you can essentially use the provider in the DLL as the basis for the System.Web.ApplicationServices DLL and instantly have a front facing WCF service that derives from the architecture in this article. Granted, it's a couple of web service calls deep to authenticate but it's scalable, securable and easily made reliable. You can download the source for this project here. I hope you find the code useful and please comment away on the implementation, architecture or design. I’m always open to feedback.
[ServiceContract]
public classMembershipService : SqlMembershipProvider
{
private MembershipProvider provider = Membership.Provider;
public override String ApplicationName
{
get{ return provider.ApplicationName; }
set{ provider.ApplicationName = value; }
}
[OperationContract]
public string GetApplicationName()
{
return this.ApplicationName;
}
[OperationContract]
public void SetApplicationName(String applicationname)
{
this.ApplicationName = applicationname;
}
[OperationContract(Name="GetUserByUserName")]
public override MembershipUser GetUser(string username, bool userIsOnline)
{
return provider.GetUser(username, userIsOnline);
}
...
}
{
private MembershipService.MembershipServiceClient m_proxy = null;
public WCFMembershipProvider()
{
m_proxy = new MembershipService.MembershipServiceClient();
}
public String Uri
{
get { return m_proxy.Endpoint.Address.Uri.ToString(); }
set { m_proxy.Endpoint.Address = new System.ServiceModel.EndpointAddress(value); }
}
public override string ApplicationName
{
get { return m_proxy.GetApplicationName(); }
set { m_proxy.SetApplicationName(value); }
}
public override MembershipUser GetUser(string username, bool userIsOnline)
{
return m_proxy.GetUserByUserName(username, userIsOnline);
}
public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
{
return m_proxy.GetUserByUserKey(providerUserKey, userIsOnline); ;
}
...
}
<!--
For more information on how to configure your ASP.NET application, please visit
http://go.microsoft.com/fwlink/?LinkId=169433
-->
<configuration>
<connectionStrings>
</connectionStrings>
<system.web>
<compilationdebug="true" targetFramework="4.0" />
<authenticationmode="Forms">
<formsloginUrl="~/Account/Login.aspx" timeout="2880" />
</authentication>
<membershipdefaultProvider="WCFMembershipProvider">
<providers>
<clear/>
<addname="WCFMembershipProvider"
type="EPI.Web.WCFApplicationServicesProviders.WCFMembershipProvider"
Uri="http://localhost:3308/MembershipService.svc"
enablePasswordRetrieval="false" enablePasswordReset="true"
requiresQuestionAndAnswer="false" requiresUniqueEmail="false"
maxInvalidPasswordAttempts="5" minRequiredPasswordLength="6"
minRequiredNonalphanumericCharacters="0" passwordAttemptWindow="10"
applicationName="/" />
</providers>
</membership>
<roleManagerenabled="true" defaultProvider="WCFRoleProvider">
<providers>
<clear/>
<addname="WCFRoleProvider"
type="EPI.Web.WCFApplicationServicesProviders.WCFRoleProvider"
Uri="http://localhost:3308/RoleService.svc"
applicationName="/" />
<!--<add name="AspNetWindowsTokenRoleProvider"
type="System.Web.Security.WindowsTokenRoleProvider" applicationName="/" />-->
</providers>
</roleManager>
</system.web>
<system.webServer>
<modulesrunAllManagedModulesForAllRequests="true" />
</system.webServer>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<bindingname="MembershipProviderServicewsHttp" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true"
allowCookies="false">
<readerQuotasmaxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<reliableSessionordered="true" inactivityTimeout="00:10:00"
enabled="false" />
<securitymode="Message">
<transportclientCredentialType="Windows" proxyCredentialType="None"
realm="" />
<messageclientCredentialType="Windows" negotiateServiceCredential="true"
algorithmSuite="Default" />
</security>
</binding>
<bindingname="RoleProviderServicewsHttp" closeTimeout="00:01:00"
openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true"
allowCookies="false">
<readerQuotasmaxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<reliableSessionordered="true" inactivityTimeout="00:10:00"
enabled="false" />
<securitymode="Message">
<transportclientCredentialType="Windows" proxyCredentialType="None"
realm="" />
<messageclientCredentialType="Windows" negotiateServiceCredential="true"
algorithmSuite="Default" />
</security>
</binding>
</wsHttpBinding>
</bindings>
<client>
<endpointaddress="http://localhost:3308/MembershipService.svc"
binding="wsHttpBinding" bindingConfiguration="MembershipProviderServicewsHttp"
contract="MembershipService.MembershipService" name="MembershipProviderServicewsHttp">
<identity>
<userPrincipalNamevalue="usernameremoved" />
</identity>
</endpoint>
<endpointaddress="http://localhost:3308/RoleService.svc" binding="wsHttpBinding"
bindingConfiguration="RoleProviderServicewsHttp" contract="RoleService.RoleService"
name="RoleProviderServicewsHttp">
<identity>
<userPrincipalNamevalue="usernameremoved" />
</identity>
</endpoint>
</client>
</system.serviceModel>
</configuration>
» Trackbacks & Pingbacks
6 Comments
-
Nice example, very usefull. I suppose that this method can be used to wrap the sessionstate providers with as well, thereby moving the aspnetdb.mdf to the webfarm instead of keeping it on the webserver.
-
It could be but I think you will find Windows Identity Foundation and the WS-Federation specification to be of more use. It's far more robust that this implementation and far more flexible. Eventually, I'll get around to a writeup of it and contrast the differences.
-
This is a brilliant example. Now I need to wrap SqlProfileProvider in wcf service, so i have created ProfileService drived from SqlProfileProvider and override all the methods but i am getting following error when i view the service in browser.
Type 'System.Configuration.SettingsPropertyCollection' cannot be serialized. Consider marking it with the DataContractAttribute attribute, and marking all of its members you want serialized with the DataMemberAttribute attribute. If the type is a collection, consider marking it with the CollectionDataContractAttribute. See the Microsoft .NET Framework documentation for other supported types.
The problem is GetPropertyValues() and SetPropertyValues() method uses SettingsPropertyValueCollection which can not be serialized.
Do you know whats is the work around? Or can you guid me whats is the best way to wrap SqlProfileProvider in wcf?
Cheers
-
I have found a work around, may be its not the best way to wrap the SqlProfileProvider in WCF but it works for me.
I have drived a CustomProfileService from SqlProfileProvider and override all the methods except
GetPropertyValues() and SetPropertyValues(), then i added my own implementation of GetPropertyValues() and SetPropertyValues() method and DataContracts (PropertyValueRequest/PropertyValueResponse), see the code below.
[OperationContract]
public PropertyValueResponse GetPropertyValues(string userName)
{
string[] names;
string values;
byte[] buf;
PropertyValueResponse response = null;
SqlConnection conn = null;
SqlDataReader reader = null;
try
{
string connectionString = null;
ConnectionStringSettings connObj = ConfigurationManager.ConnectionStrings["ApplicationServices"];
if (connObj != null)
connectionString = connObj.ConnectionString;
if (connectionString == null)
return null;
conn = new SqlConnection(connectionString);
conn.Open();
SqlCommand cmd = new SqlCommand("dbo.aspnet_Profile_GetProperties", conn);
string applicationName = ConfigurationManager.AppSettings["ApplicationName"];
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(CreateInputParam("@ApplicationName", SqlDbType.NVarChar, applicationName));
cmd.Parameters.Add(CreateInputParam("@UserName", SqlDbType.NVarChar, userName));
cmd.Parameters.Add(CreateInputParam("@CurrentTimeUtc", SqlDbType.DateTime, DateTime.UtcNow));
reader = cmd.ExecuteReader(CommandBehavior.SingleRow);
if (reader.Read())
{
names = reader.GetString(0).Split(':');
values = reader.GetString(1);
int size = (int)reader.GetBytes(2, 0, null, 0, 0);
buf = new byte[size];
reader.GetBytes(2, 0, buf, 0, size);
response = new PropertyValueResponse
{
Names = names,
Values = values,
Buffer = buf
};
}
}
catch (Exception)
{
throw;
}
finally
{
if (conn != null)
conn.Close();
if (reader != null)
reader.Close();
}
return response;
}
[OperationContract]
public void SetPropertyValues(PropertyValueRequest request)
{
SqlConnection conn = null;
string connectionString = null;
try
{
ConnectionStringSettings connObj = ConfigurationManager.ConnectionStrings["ApplicationServices"];
if (connObj != null)
connectionString = connObj.ConnectionString;
if (connectionString == null)
throw new Exception("Can not find connection string");
conn = new SqlConnection(connectionString);<
-
Thanks for the responses guys. While I was working on this, I managed to figure out that what I really wanted to do was implement a Federated ID scenario where I could add Claims based authentication to my API and front end layers. There's a great Federated ID server in ADFS 2.0 that comes with Windows Server 2008 R2 but the only one that exists for SqlMembershipProvider is Thinktecture's codeplex project at startersts.codeplex.com and it has not been updated in a year. There's a promised 1.5 release that I have yet to see.
The problem I ran into with this when used for two different web sites is that certain cryptographic information could not be shared between application without breaking the security model. My code works well for simply n-tiering the basic SqlMembershipProvider so that you can scale a web site a bit better but, you won't achieve Single Sign-On without help or violating some security principles along the way.
I highly suggest that anyone interested in this article, read www.leastprivilege.com/.../default.aspx and get started using Claims based authentication and authorization. It's the way of the future and it took me this project to realize this much. Thanks for the code snippets and good luck.
-
Nice example, very useful, what about cache and security?
1.19.2011 at 2:35 AM