enter image description here

I am a big fan of using WCF services in my apps. Many times I like to have little services as part of my app, but the problem I have is that by default any user on the machine can access those services. Always thinking about security, I want to make sure that only the current user can access their own services.

Consider the following code:

class TestService
{

    private readonly ServiceHost _host;
    public TestService()
    {
        var pipePath = "net.pipe://localhost/Math";
        NetNamedPipeBinding binding = new NetNamedPipeBinding();
        _host = new ServiceHost(typeof(MathService), new Uri(pipePath));

        ServiceMetadataBehavior smb = _host.Description.Behaviors.Find<ServiceMetadataBehavior>();

        smb = new ServiceMetadataBehavior();

        _host.Description.Behaviors.Add(smb);
        _host.AddServiceEndpoint(typeof(IMath), binding, pipePath);
        _host.AddServiceEndpoint(ServiceMetadataBehavior.MexContractName, 
                MetadataExchangeBindings.CreateMexNamedPipeBinding(), pipePath + "/mex");
    }

    public async Task StartAsync()
    {
        await Task.Factory.FromAsync(_host.BeginOpen(null, null), _host.EndOpen);
    }

    public string Endpoint { get { return _host.BaseAddresses.First().AbsoluteUri.ToString(); } }

}

[ServiceContract]
public interface IMath
{
    [OperationContract]
    int Add(int x, int y);
}

public class MathService : IMath
{
    public int Add(int x, int y)
    {
        unchecked
        {
            return x + y;
        }
    }
}

When I start this service, anyone on the machine can talk to the service via its URI (net.pipe://localhost/Math). In order to lock this service down, I did my research and found that you can implement a custom ServiceAuthorizationManager which will allow you to control who has access to your services.

To start, I created a simple implementation that checked the current user against the operation context security user:

public class CurrentUserOnlyAuthorizationManager : ServiceAuthorizationManager
{
    protected override bool CheckAccessCore(OperationContext operationContext)
    {
        var currentUser = WindowsIdentity.GetCurrent()?.User;
        var contextUser = operationContext?.ServiceSecurityContext?.WindowsIdentity?.User;
        if (currentUser == null || contextUser==null)
            return false;

        return currentUser.Equals(contextUser);
    }
}

And when setting up my host, I have it use the new manager:

_host.Authorization.ServiceAuthorizationManager = new CurrentUserOnlyAuthorizationManager();

This works well, until you try and regenerate the service reference. By default the service references are generated using an anonymous request to the service. Since the anonymous user doesn't match the user hosting the service the code prevents the anonymous user from accessing the service. Fortunately there was an MSDN article that addressed this specifically and gave the exact code to handle mex requests. So my final CurrentUserOnlyAuthorizationManager looks like this:

public class CurrentUserOnlyAuthorizationManager : ServiceAuthorizationManager
{
    protected override bool CheckAccessCore(OperationContext operationContext)
    {
        // Allow MEX requests through.
        if (operationContext.EndpointDispatcher.ContractName == ServiceMetadataBehavior.MexContractName &&
            operationContext.EndpointDispatcher.ContractNamespace == "http://schemas.microsoft.com/2006/04/mex" &&
            operationContext.IncomingMessageHeaders.Action == "http://schemas.xmlsoap.org/ws/2004/09/transfer/Get")
            return true;

        var currentUser = WindowsIdentity.GetCurrent()?.User;
        var contextUser = operationContext?.ServiceSecurityContext?.WindowsIdentity?.User;
        if (currentUser == null || contextUser==null)
            return false;

        return currentUser.Equals(contextUser);
    }
}

Once this is in place, then I am able to successfully regenerate the service references as needed through Visual Studio or the svcutil app. The final code for the test service is:

class TestService
{

    private readonly ServiceHost _host;
    public TestService()
    {
        var pipePath = "net.pipe://localhost/Math";
        NetNamedPipeBinding binding = new NetNamedPipeBinding();
        _host = new ServiceHost(typeof(MathService), new Uri(pipePath));

        ServiceMetadataBehavior smb = _host.Description.Behaviors.Find<ServiceMetadataBehavior>();

        smb = new ServiceMetadataBehavior();

        _host.Description.Behaviors.Add(smb);
        _host.AddServiceEndpoint(typeof(IMath), binding, pipePath);
        _host.AddServiceEndpoint(ServiceMetadataBehavior.MexContractName, MetadataExchangeBindings.CreateMexNamedPipeBinding(), pipePath + "/mex");

        _host.Authorization.ServiceAuthorizationManager = new CurrentUserOnlyAuthorizationManager();

    }

    public async Task StartAsync()
    {
        await Task.Factory.FromAsync(_host.BeginOpen(null, null), _host.EndOpen);
    }

    public string Endpoint { get { return _host.BaseAddresses.First().AbsoluteUri.ToString(); } }

}

[ServiceContract]
public interface IMath
{
    [OperationContract]
    int Add(int x, int y);
}

public class MathService : IMath
{
    public int Add(int x, int y)
    {
        unchecked
        {
            return x + y;
        }
    }
}

public class CurrentUserOnlyAuthorizationManager : ServiceAuthorizationManager
{
    protected override bool CheckAccessCore(OperationContext operationContext)
    {
        // Allow MEX requests through.
        if (operationContext.EndpointDispatcher.ContractName == ServiceMetadataBehavior.MexContractName &&
            operationContext.EndpointDispatcher.ContractNamespace == "http://schemas.microsoft.com/2006/04/mex" &&
            operationContext.IncomingMessageHeaders.Action == "http://schemas.xmlsoap.org/ws/2004/09/transfer/Get")
            return true;

        var currentUser = WindowsIdentity.GetCurrent()?.User;
        var contextUser = operationContext?.ServiceSecurityContext?.WindowsIdentity?.User;
        if (currentUser == null || contextUser==null)
            return false;

        return currentUser.Equals(contextUser);
    }
}

Now, if a user other than the user that started the service tries to access the service, they will get a SecurityAccessDeniedException. I have also posted this code up on Github, if you want to download it an play with it.