Sin, Sin, Sin: How to do Simple, Webby, and Completely Insecure ASP.NET Membership Authentication and Role Authorization with WCF
We're all sinners. Lots of the authentication mechanisms on the Web are not even "best effort", but rather just cleartext transmissions of usernames and passwords that are easily intercepted and not secure at all. We're security sinners by using them and even more so by allowing this. However, the reality is that there's very likely more authentication on the Web done in an insecure fashion and in cleartext than using any other mechanism. So if you are building WCF apps and you decide "that's good enough" what to do?
WCF is - rightfully - taking a pretty hard stance on these matters. If you try to use any of the more advanced in-message authN and authZ mechnanisms such as the integration with the ASP.NET membership/role provider models, you'll find yourself in security territory and our security designers took very good care that you are not creating a config that results in the cleartext transmission of credentials. And for that you'll need certificates and you'll also find that it requires full trust (even in 3.5) to use that level of robust on-wire security.
dasBlog has (we're sinners, too) a stance on authentication that's about as lax as everyone else's stance in blog-land. There are not many MetaWeblog API endpoints running over https (as they rather should) that I've seen.
So what I need for a bare minimum dasBlog install where the user isn't willing to get an https certificate for their site is a very simple, consciously insecure, bare-bones authentication and authorization mechanism for WCF services that uses the ASP.NET membership/role model (dasBlog will use that model as we switch to the .NET Framework 3.5 later this year). The It also needs to get completely out of the way when the service is configured with any real AuthN/AuthZ mechanism.
So here's a behavior (some C# 3.0 syntax, but easy to fix) that you can add to channel factories (client) and service endpoints (server) that will do just that. If you care about confidentiality of credentials on the wire don't use it. For this to work, you need to put the behavior on both ends. The behavior will do nothing (as intended) when the binding isn't the BasicHttpBinding with BasicHttpSecurityMode.None). The header will not show up in WSDL.
On the client, you simply add the behavior and otherwise set the credentials as you would usually do for UserName authentication. This makes sure that the client code stays compatible when you upgrade the wire protocol to a more secure (yet still username-based) binding via config.
MyClient remoteService = new MyClient();
remoteService.ChannelFactory.Endpoint.Behaviors.Add(new SimpleAuthenticationBehavior());
remoteService.ClientCredentials.UserName.UserName = "admin";
remoteService.ClientCredentials.UserName.Password = "!adminadmin";
On the server, you just configure your ASP.NET membership and role database. With that in place, you can even use role-based security attributes or any other authorization mechnanism you are accustomed to in ASP.NET. Just as on the client, the behavior goes out of the way and gives way for the "real thing" once you turn on security.
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Security;
using System.Threading;
using System.Web.Security;
using System.Xml.Serialization;
namespace dasBlog.Storage
{
[DataContract(Namespace = Names.DataContractNamespace)]
class SimpleAuthenticationHeader
{
[DataMember]
public string UserName;
[DataMember]
public string Password;
}
public class SimpleAuthenticationBehavior : IEndpointBehavior
{
#region IEndpointBehavior Members
public void AddBindingParameters(ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint,
ClientRuntime clientRuntime)
{
if (endpoint.Binding is BasicHttpBinding &&
((BasicHttpBinding)endpoint.Binding).Security.Mode == BasicHttpSecurityMode.None )
{
var credentials = endpoint.Behaviors.Find<ClientCredentials>();
if (credentials != null && credentials.UserName != null && credentials.UserName.UserName != null)
{
clientRuntime.MessageInspectors.Add(new ClientMessageInspector(credentials.UserName));
}
}
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
if (endpoint.Binding is BasicHttpBinding &&
((BasicHttpBinding)endpoint.Binding).Security.Mode == BasicHttpSecurityMode.None)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new DispatchMessageInspector());
}
}
public void Validate(ServiceEndpoint endpoint)
{
}
#endregion
class DispatchMessageInspector : IDispatchMessageInspector
{
#region IDispatchMessageInspector Members
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
int headerIndex = request.Headers.FindHeader("simpleAuthenticationHeader", "https://dasblog.info/2007/08/security");
if (headerIndex >= 0)
{
var header = request.Headers.GetHeader<SimpleAuthenticationHeader>(headerIndex);
request.Headers.RemoveAt(headerIndex);
if ( Membership.ValidateUser(header.UserName, header.Password) )
{
var identity = new FormsIdentity(new FormsAuthenticationTicket(header.UserName, false, 15));
Thread.CurrentPrincipal = new RolePrincipal(identity);
}
}
return null;
}
public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
}
#endregion
}
class ClientMessageInspector : IClientMessageInspector
{
#region IClientMessageInspector Members
UserNamePasswordClientCredential creds;
public ClientMessageInspector(UserNamePasswordClientCredential creds)
{
this.creds = creds;
}
public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
}
public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, IClientChannel channel)
{
request.Headers.Add(MessageHeader.CreateHeader("simpleAuthenticationHeader", https://dasblog.info/2007/08/security,
new SimpleAuthenticationHeader{ UserName = creds.UserName, Password = creds.Password }));
return null;
}
#endregion
}
}
}
Comments
- Anonymous
August 23, 2007
I could get used to this rolling out of bed into my office thing BizTalk Server The highly anticipated