Reputation: 2811
My problem is the following.
A client interacts with my WCF service via a web interface (HTTP). Some service operations require the client to authenticate by providing username and password. Let's suppose for simplicity that these info are passed via query string parameters (or in the Authorization header as in HTTP Basic Auth).
E.g., a service operation may be called via http://myhost.com/myservice/myop?user=xxx&password=yyy
As multiple service operations require such sort of authentication, I would like to factor the authentication code out of the single operations.
By looking around, I read about service behaviors and came up with the following code:
public class MyAuthBehaviorAttribute : Attribute, IServiceBehavior, IDispatchMessageInspector {
/********************/
/* IServiceBehavior */
public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase) {
// It’s called right after the runtime was initialized
foreach (ChannelDispatcher chDisp in serviceHostBase.ChannelDispatchers) {
foreach (EndpointDispatcher epDisp in chDisp.Endpoints) {
epDisp.DispatchRuntime.MessageInspectors.Add(new MyAuthBehaviorAttribute());
}
}
}
/*...*/
/*****************************/
/* IDispatchMessageInspector */
public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request,
System.ServiceModel.IClientChannel channel,
System.ServiceModel.InstanceContext instanceContext) {
object correlationState = null;
var prop = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];
var parts = HttpUtility.ParseQueryString(prop.QueryString);
string user = parts["user"];
string password = parts["password"];
if (AuthenticateUser(user,password)) {
// ???????????????????????????
}
else {
throw new Exception("...");
}
return correlationState;
}
/*...*/
}
Then, the service is annotated through
[MyAuthBehavior]
public class Service : IContract
{
// implementation of the IContract interface
}
Now, I manage to execute my behavior before ANY service operation. However, I have the following issues:
Regarding the last point, I looked at IOperationBehavior, but in that case I can just attach IParameterInspectors and not IDispatchMessageInspectors. It would be undesirable because I may need to look at the message headers, for example in case I decide to consider the Authorization HTTP header when supporting HTTP Basic Authentication.
As a related question, I would also ask what you think about my approach, and if there are better (non-overcomplicated) approaches.
Upvotes: 1
Views: 1150
Reputation: 2811
After some research, here is my current solution.
First of all, I mark my service operations with a custom attribute:
public class RequiresAuthAttribute : Attribute { }
public partial class MyService {
[RequiresAuth]
WebGet(UriTemplate = "...")]
public Tresult MyServiceOperation(...){ ... }
Then I retrieve this information to decide if the behavior has to be executed or not
public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request,
System.ServiceModel.IClientChannel channel,
System.ServiceModel.InstanceContext instanceContext) {
if(AuthenticationNeeded()){ ... }
}
public bool AuthenticationNeeded() {
// 1) Get the current operation's description
OperationDescription od = GetOperationDescription(OperationContext.Current);
// 2) Check if the service operation is annotated with the [RequiresAuth] attribute
Type contractType = od.DeclaringContract.ContractType;
object[] attr = contractType.GetMethod(od.Name).GetCustomAttributes(typeof(RequiresAuthAttribute), false);
if (attr == null || attr.Length == 0) return false;
return true;
}
// See http://www.aspnet4you.com/wcf/index.php/2013/01/30/message-interception-auditing-and-logging-at-wcf-pipeline/
private OperationDescription GetOperationDescription(OperationContext operationContext) {
OperationDescription od = null;
string bindingName = operationContext.EndpointDispatcher.ChannelDispatcher.BindingName;
string methodName;
if (bindingName.Contains("WebHttpBinding")) {
//REST request
methodName = (string)operationContext.IncomingMessageProperties["HttpOperationName"];
}
else {
//SOAP request
string action = operationContext.IncomingMessageHeaders.Action;
methodName = operationContext.EndpointDispatcher.DispatchRuntime.Operations.FirstOrDefault(o => o.Action == action).Name;
}
EndpointAddress epa = operationContext.EndpointDispatcher.EndpointAddress;
ServiceDescription hostDesc = operationContext.Host.Description;
ServiceEndpoint ep = hostDesc.Endpoints.Find(epa.Uri);
if (ep != null) {
od = ep.Contract.Operations.Find(methodName);
}
return od;
}
The service behavior will do something as
OperationContext.Current.IncomingMessageProperties.Add("myInfo", myInfo);
while the service operation will do something as
object myInfo = null;
OperationContext.Current.IncomingMessageProperties.TryGetValue("myInfo", out myInfo);
Alternatively, it is also possible to set a value for the service operation's parameters via
WebOperationContext.Current.IncomingRequest.UriTemplateMatch.BoundVariables["MYPARAM"] = myParam;
Upvotes: 3
Reputation: 1539
I would suggest isolating all of your methods that don't require authentication into their own service. For example:
IPublicService.cs and PublicService.svc
and the ones that require authentication:
IPrivateService.cs and PrivateService.svc
For authentication for PrivateService.svc, I'd suggest using MessageCredential using Username for that binding:
<wsHttpBinding>
<binding name="wsHttpEndpointBinding" closeTimeout="00:30:00" openTimeout="00:30:00" receiveTimeout="00:30:00" sendTimeout="00:30:00" maxReceivedMessageSize="500000000">
<readerQuotas maxDepth="500000000" maxStringContentLength="500000000" maxArrayLength="500000000" maxBytesPerRead="500000000" maxNameTableCharCount="500000000" />
<security mode="MessageCredential">
<message clientCredentialType="UserName" />
</security>
</binding>
</wsHttpBinding>
and add a custom username validator class:
public class CustomUserNameValidator : UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
if (username!="test" && password!="test")
{
throw new FaultException("Unknown username or incorrect password.");
}
return;
}
}
And register your class in web.config:
<behaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the values below to false before deployment -->
<serviceMetadata httpsGetEnabled="true" />
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="MyProgram.CustomUserNameValidator,MyProgram" />
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
Upvotes: 2