3

Sorry for the long title.

This one is about WCF configuration and security. I have the following scenario:

  • one server (IIS)
  • N clients (WPF applications)
  • web services to communicate between clients and the server
  • everything is in the same LAN and the same domain
  • I need some duplex communication so the server can notifies all clients (no polling, but usage of WCF callbacks)
  • user credentials (login/password) are managed by an active directory
  • user authentication is "per-user", not "per-Windows account" nor "per-machine"

Ideally, I would like to have 2 different services:

One that requires a WCF session so I can use callbacks:

[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IService1Callback))]
public interface IService1
{
    [OperationContract(IsOneWay = false, IsInitiating = true)]
    void Subscribe();

    [OperationContract(IsOneWay = false, IsTerminating = true)]
    void Unsubscribe();
}

One that doesn't, so I can use the good old way to write stateless, efficient and maintainable services: each WCF call is authenticated, authorized, and produces on server side a new fresh instance of the Service implementation:

[ServiceContract]
public interface IService2
{
    [OperationContract]
    int DoSomeStuff();
}

The thing is, we want every client to authenticate against an active directory. So on client application startup, every client must specify login, password and domain. Problem is AD authentication is quite time-consuming:

using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "theDomain"))
{
    if (!pc.ValidateCredentials("theUser", "thePassword"))
    {
        throw new SecurityException();
    }
}

My first idea was to use the IService1 service (that is responsible of sending callbacks) as the authentication service:

[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IService1Callback))]
public interface IService1
{
    [OperationContract(IsOneWay = false, IsInitiating = true)]
    void Login(string login, string password, string domain);

    [OperationContract(IsOneWay = false, IsTerminating = true)]
    void Logout();
}

So inside the Login operation implementation, we could check the user login/password against the AD only once, and rely on the WCF session for the next WCF requests from the client. Problem is, I don't want to make other requests against IService1 service because of session mode that is Required, and InstanceContextMode that is PerSession (yes, the ugly client-proxy singleton pattern I want to avoid for other operations than just login/logout/client notifications).

So the question is: how could I build and configure my services, so:

  • I don't need to request against AD on each WCF request
  • I have some server callback
  • I only have singleton pattern/session required/instance context per session, for the callback service (not the "actual" IService2 service)

I was thinking of wsHttpBinding for IService2 and netTcpBinding for IService1. Concerning the choice of transfer security (transport or message) and the choice of credential type (Windows, UserName,.. ?) I'm not sure.

Any help would be greatly appreciated.

ken2k
  • 48,145
  • 10
  • 116
  • 176

2 Answers2

0

You could try this:

Create a custom message inspector that takes two parameters - UserName and PWD that you authenticate on your AD server the first time the client hits your host. But before returning the response to the client, add a time-limited cookie. When the client requests again, check for the existence of the cookie in the request's OperationContext and skip the AD authentication.

There are two things to know regarding this solution. First, when you use a message inspector, every request (not just Login) will hit procedure. That's how they work. But because of the cookie check, you can skip past the subsequent AD checks as long as the cookie is valid. And it eliminiates the necessity for a Login procedure.

Something like this:

public class SchemaLoggingMessageInspector : Dispatcher.IDispatchMessageInspector
{

public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request,
                        System.ServiceModel.IClientChannel channel, 
                        System.ServiceModel.InstanceContext instanceContext)    {

    var cookieHeader = WebOperationContext.Current.IncomingRequest.Headers[System.Net.HttpRequestHeader.Cookie];
    if (!String.IsNullOrEmpty(cookieHeader)){
        var match = cookieHeader.Split(';').Select(cookie => cookie.Split('=')).FirstOrDefault(kvp => kvp[0] == "BigCookie");
        if (match != null)
        {
            return True
        }
    }

    if (WcfBaseFunctionality.eSecurityType == SecurityEnum.authentication) {
        string username = AuthenticationBehavior.GetHeaderData("Username");
        string password = AuthenticationBehavior.GetHeaderData("Password");

        if (ValidateAuthentication(username, password){
            WebOperationContext.Current.OutgoingResponse.Headers[System.Net.HttpResponseHeader.SetCookie] = Cookie="BigCookie"; 
            return True;
        }else{
            return False;
        };
    }

    return False;
}

    private bool ValidateAuthentication(string UserName, string PWD)
    {
        //add code to check ID/password against AD server
        return true;
    }
}

As far as the client callback is concerned, I've never tried that. Sorry. Good luck with your web service.

Brian
  • 3,653
  • 1
  • 22
  • 33
  • Thanks for the answer. Actually I was thinking about something very similar: mimicking ASP.Net sessions and cookies, with a server-side session storage (basically a list of tuples of ) and check against those values on each request (if request is the login, the create authenticate against the AD and create a tuple). What would be the security configuration (I mean, web.config and client app.config) for such a solution as we manually manage the authentication? – ken2k Jun 17 '13 at 17:16
  • Btw, as it is a very *custom* implementation, is there any official documentation/guideline about such architectures? I mean, is it something officially recommended to build its own session storage when we have such specific requirements? I'd like to avoid reinventing the wheel, if possible. – ken2k Jun 17 '13 at 17:18
  • This isn't as custom as it looks. I took this from MS documentation concerning the topic. I think I may have used this as a guide: http://msdn.microsoft.com/en-us/library/ff647820.aspx – Brian Jun 18 '13 at 07:42
0

if I understood correctly :

  • you need a service (1)
    • responsible for authentication,token, whatever and
    • also provides a callback per session
  • you need an actual worker service which
    • can do the processing
    • can initiate the callback
    • can work only if authenticated

As far as I see the authentication is coupled to the session question such way it's not the hard problem (would use ADFS if you need federation (token based identity) and AD at the same time). I tried it with this setup with two simple services

  • service1 : wsHttpBinding (for the sake of session)
  • service2 : basicHttpBinding (for the sake of simplicity, could be net tcp also)

The important point was to make sure that the instancing of the second is not connected to the first (session based).

Considering that somehow the end of the processing (responsibility of service2) should trigger the callback (responsibility of service1) there's no way to avoid sharing some kind of callback information (which also means sharing either session id or anything identifying the service1 active session instance).

I tried the most simple approach of using a cache to store the callback instance against the session key. (I know a lot of counterexamples of why this is evil but it was for the poc only). Such way the two services' lifecycles were decoupled and still the callback could be called.

I'm sure that generating the proper token and binding the token check to the processing service is well documented (ADFS and its consumers).

Still I think the key point is using the shared callback there.

Let me know if I completely misunderstood your problem.

Thanks, Nicolai

Nicolai Ustinov
  • 541
  • 4
  • 5