7

I have a solution that looks like this:

  • Winform WCF(Client)
  • IdentityService4 self-hosted in Windows Service
  • WCF HTTPS service self-hosted in Windows Service

Both the IdentitySErvice4 and the WCF service is using a function certificate. The client is using a client certificate loaded from the Windows Store. The client service is installed to the Windows Store from a smart card with a third party software. If the card is removed, the certificate is removed from the store.

The flow looks like this:

1 Client loads the certificate from a store and binds it to the tookenhandler like this:

    public override TokenClient GetTokenClient(string host, string port)
            {
                var certificate = SmartCardHandler.GetInstance().Result.CurrentCertificate();
                if (certificate == null)
                    throw new Exception("Certificate is missing");

                var handler = new WebRequestHandler();
                handler.ServerCertificateValidationCallback = PinPublicKey;
                var url = $"https://{host}:{port}/connect/token";

                handler.ClientCertificates.Add(certificate);

                return new TokenClient(url, ClientTypes.Siths, "secret", handler);
            }

token = await tokenClient.RequestCustomGrantAsync(ClientTypes.Siths, "MyApp.wcf offline_access");

2. The client sends the request to the IdentityServices that reads the client certificate and uses it to authenticate and generate a client token that is returned back.

3. The client creates a WCF channel to the service using the client certificate, the token is also attached to this channel like this

private async Task<ChannelFactory<T>> CreateChannelFactory(LoginTypeBase loginType, MyAppToken token)
        {
            var service = await _ConsulService.GetServiceBlocking(loginType.MyAppServicesToUse, forceRefresh: true, token: new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token);

            if (service == null)
                throw new MyAppServiceCommunicationException();

            var cert = loginType.ClientCertificate;
            var uri = loginType.GetMyAppClientServiceURL(service.Address, service.Port);

            var header = AddressHeader.CreateAddressHeader(nameof(MyAppToken), nameof(MyAppToken), token);
            var endpointAddress = new EndpointAddress(uri, header);

            ServiceEndpoint serviceEndpoint = null;
            if (loginType.LoginType == LoginType.SmartCard || loginType.LoginType == LoginType.UsernamePasswordSLL)
            {
                var binding = new NetHttpsBinding("netHttpsBinding");
                binding.Security.Mode = BasicHttpsSecurityMode.Transport;
                if (loginType.LoginType == LoginType.SmartCard)
                    binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
                else
                    binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;

                serviceEndpoint = new ServiceEndpoint(ContractDescription.GetContract(typeof(T)), binding, endpointAddress);
            }
            else
            {
                var binding = new NetHttpBinding("netHttpBinding");
                serviceEndpoint = new ServiceEndpoint(ContractDescription.GetContract(typeof(T)), binding, endpointAddress);
            }

            serviceEndpoint.EndpointBehaviors.Add(new ProtoEndpointBehavior());
            serviceEndpoint.EndpointBehaviors.Add(new CustomMessageInspectorBehavior());


            var v = new ChannelFactory<T>(serviceEndpoint);
            if (loginType.LoginType == LoginType.SmartCard)
            {
                v.Credentials.ClientCertificate.Certificate = cert;
                //v.Credentials.ClientCertificate.SetCertificate(StoreLocation.CurrentUser, StoreName.My, X509FindType.FindByThumbprint, cert.Thumbprint);
            }
            return v;
        }
    }

4. The client sends the first message to the WCF service. The third party software will react and demand a pin for the client certificate when this is granted the message is forwarded to the WCF service where the token is validated against the IdentityService4.

Real proxy is used to make it possible to switch to another service if needed. In this case, it only has URL to the one online WCF service.

 public override IMessage Invoke(IMessage msg)
        {
            var methodCall = (IMethodCallMessage)msg;
            var method = (MethodInfo)methodCall.MethodBase;
            var channel = GetChannel(false);
            var retryCount = 3;
            do
            {
                try
                {
                    var result = method.Invoke(channel, methodCall.InArgs);
                    var returnmessage = new ReturnMessage(result, null, 0, methodCall.LogicalCallContext, methodCall);
                    return returnmessage;
                }
                catch (Exception e)
                {
                    if (e is TargetInvocationException && e.InnerException != null)
                    {
                        if (e.InnerException is FaultException)
                            return new ReturnMessage(ErrorHandler.Instance.UnwrapAgentException(e.InnerException), msg as IMethodCallMessage);

                        if (e.InnerException is EndpointNotFoundException || e.InnerException is TimeoutException)
                            channel = GetChannel(true);
                    }
                    retryCount--;
                }
            } while (retryCount > 0);

            throw new Exception("Retrycount reached maximum. Customproxy Invoke");
        }

5 The client token will be refreshed every 30 min by the client

private async Task RefreshToken(LoginTypeBase loginType)
        {
            if (_MyAppToken == null)
                return;
            var tokenClient = await GetTokenClient(loginType);
            var result = !string.IsNullOrEmpty(_refreshToken) ? await tokenClient.RequestRefreshTokenAsync(_refreshToken, _cancelToken.Token) : await tokenClient.RequestCustomGrantAsync("siths", cancellationToken: _cancelToken.Token);
            if (string.IsNullOrEmpty(result.AccessToken))
                throw new Exception($"Accesstoken har blivit null försökte refresha med {tokenClient.ClientId} {_refreshToken} {DateTime.Now}");
            _MyAppToken.Token = result.AccessToken;
            _refreshToken = result.RefreshToken;
        }

This all works great at the beginning but after a random amount of calls it will fail, some times its the WCF service that it fails to contact and at this point the IdentityService might still be able to communicate with. The exception is thrown in the code shown in point 4 above and looks like this :

e.ToString() "System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.ServiceModel.Security.SecurityNegotiationException: Could not establish secure channel for SSL/TLS with authority '139.107.245.141:44310'. ---> System.Net.WebException: The request was aborted: Could not create SSL/TLS secure channel.\r\n at System.Net.HttpWebRequest.GetResponse()\r\n at System.ServiceModel.Channels.HttpChannelFactory1.HttpRequestChannel.HttpChannelRequest.WaitForReply(TimeSpan timeout)\r\n --- End of inner exception stack trace ---\r\n\r\nServer stack trace: \r\n at System.ServiceModel.Channels.HttpChannelUtilities.ProcessGetResponseWebException(WebException webException, HttpWebRequest request, HttpAbortReason abortReason)\r\n at System.ServiceModel.Channels.HttpChannelFactory1.HttpRequestChannel.HttpChannelRequest.WaitForReply(TimeSpan timeout)\r\n at System.ServiceModel.Channels.RequestChannel.Request(Message message, TimeSpan timeout)\r\n at System.ServiceModel.Dispatcher.RequestChannelBinder.Request(Message message, TimeSpan timeout)\r\n at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)\r\n at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)\r\n at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)\r\n\r\nException rethrown at [0]: \r\n at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)\r\n at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)\r\n at myapp.ServiceContracts.ImyappClientService.LogData(GeneralFault generalFault)\r\n --- End of inner exception stack trace ---\r\n
at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)\r\n at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)\r\n at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)\r\n at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)\r\n at myapp.Client.Main.Classes.Service_Management.CustomProxy`1.Invoke(IMessage msg) in C:\myapp\Produkter\myapp Utveckling\Solution\myapp.Client.Main\Classes\Service Management\CustomProxy.cs:line 74" string

Some times it will first fail the WCF service but then work again and then fail again(exacly the same vall) while when the IdentityService4 connection starts to fail it will fail every time with this exception :

token.Exception.ToString() "System.Net.Http.HttpRequestException: An error occurred while sending the request. ---> System.Net.WebException: The request was aborted: Could not create SSL/TLS secure channel.\r\n at System.Net.HttpWebRequest.EndGetRequestStream(IAsyncResult asyncResult, TransportContext& context)\r\n at System.Net.Http.HttpClientHandler.GetRequestStreamCallback(IAsyncResult ar)\r\n --- End of inner exception stack trace ---\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Net.Http.HttpClient.d__58.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at IdentityModel.Client.TokenClient.d__26.MoveNext()" string

This code is used to test the IdentityService connection while in the client

It's the token.The exception that will hold the exception above.

public static async Task LoginSiths() { try { var host = "139.107.245.141"; var port = 44312; var url = $"https://{host}:{port}/connect/token";

            var handler = new WebRequestHandler();

            handler.ServerCertificateValidationCallback = PinPublicKey;
            handler.ClientCertificates.Add(SmartCardHandler.GetInstance().Result.CurrentCertificate());

            var client = new TokenClient(url, ClientTypes.Siths, "secret.ro101.orbit", handler);
            TokenResponse token = await client.RequestCustomGrantAsync(ClientTypes.Siths, "orbit.wcf offline_access");

            if (token.IsError)
                MessageBox.Show("Failed!");
            else
                MessageBox.Show("Success!");
        }
        catch (Exception ex)
        {
            MessageBox.Show("Exception : " + ex.ToString());
        }
    }

The only way out is to restart the client(none of the services needs to be restarted). After the client have been restarted it can log in as before again and do calls.

If I add this to the start of my client :

 ServicePointManager.MaxServicePointIdleTime = 1;

then I will get problem almost instantly after login(that varies slowly at this point).

Is there any way to know why I get an SSL exception?

Note, this post have been updated 2017-04-07 01:00, this is to give a more collected picture of what's going on. The problem really consisted of two problems, one is still active and is explained above, the other was due to a bug in IdentityService4 that we found a workaround for.

Bryan Roberts
  • 3,449
  • 1
  • 18
  • 22
Banshee
  • 15,376
  • 38
  • 128
  • 219
  • .NET doesn't use Wininet (it can use some of it's config, but not its code). What's the stack frame of the error you get? – Simon Mourier Apr 03 '17 at 11:20
  • Aha? hmm then I dont understans what it is that stops the communication. I have attached the stacktrace in Edit 3. – Banshee Apr 03 '17 at 14:54
  • Can you try to set ServicePointManager.MaxServicePointIdleTime to 1 before anything else, and see if does something? note: in the code above, sp.Certificate is the server certificate, not the client certificate. – Simon Mourier Apr 03 '17 at 17:04
  • See my Edit 4. The client gets vary slow and when opening a function that communicates alot with service I get an exception. I tried to remove and replace the card but it works so far. However My small test usually works but as soon as my application fails to communicate my small testcode fill also fail. This small test code only makes a call to the IndentityService4 so just a couple of rows of code. – Banshee Apr 04 '17 at 09:03
  • I have runned a couple of more tests and if I remote the card from the reader I could still do calls to the service successfully.This is probably due to the certificate is stored in memory. But then after a couple of minutes I do a new call without the certificate it fails. And efter it has failed it will always fail even if I replace the card in the reader. The only way to get ot working again is to restart the application. See Edit 5 for the minimal test code I run within my application to test this. – Banshee Apr 04 '17 at 09:26
  • I dont know if it could be a hint but after it failes to connect it is sometimes not even possible to reconnect after a restart of the client. I get IdentityServer4.Validation.TokenRequestValidator|Scope parameter exceeds max allowed length from the IdentityServer4. To solve it I need to restart the client. The code for connecting is always the same, nothing is changed. – Banshee Apr 04 '17 at 09:58
  • The "couple of minute" delay is probably 100 seconds (the default for service point idle time). Are you sure the problem is not in that TokenClient + RequestCustomGrantAsync code that could cache something? – Simon Mourier Apr 04 '17 at 13:18
  • TokenClient is instansiated as new every time so no cache there. The RequestCustomGrantAsync is a call direcly to the IdentityService but IdentityService might catche data, we are looking in to this. We have built a test client and it is much more stable but it is still possible to reproduce the problem. 1. Make a call - Success 2. Remove the card 3. Wait 5-10 min 4. Replace the card 5. Do call and sometimes this call will fail. Its really hard to know where to look for this problem. – Banshee Apr 05 '17 at 07:37
  • Pleas, see edit 6. – Banshee Apr 05 '17 at 12:48
  • If I understand correctly, the fact the service has to be restarted has nothing to do with the fact your card is removed or not. Looks like the service has its own problems anyway. Can you test the whole thing w/o relying WCF nor IdentityService, only standard .NET code. – Simon Mourier Apr 06 '17 at 06:13
  • We found out thet this problem was really 2 problems, the first one with IdentityService is now solved so the services do not need to restart att all to solve problem 1 that still exists. Pleas read the updated post above. – Banshee Apr 06 '17 at 23:03
  • 1
    Do you have a complete copy of the system, separate from production? Are you able to access every level of every system involved in this app (All source code for your in-house apps and all production controls)? If not, a bounty was a wise move. The fact no answer has been suggested means no one knows. The only path to this answer is your R&D. You need to be able to tear everything down and put it back together, with special programs to emulate responses from systems that might be causing the problem. If nothing was stopping you from doing that, you would have found your answer by now. – DanAllen Apr 07 '17 at 17:10

1 Answers1

0

If your problem is on the client side, I suggest you use AppDomain isolation for the ServicePointManager instead of some reflection hacks (it hurts my eyes :) - similar for example to this .NET https requests with different security protocols across threads

I.E.:

public class Client : MarshalByRefObject {
   public string GetResponse(string address) {
     // you can get crazy with ServicePointManager settings here
     // + actually do the request
   }
}

public sealed class Isolated<T> : IDisposable where T : MarshalByRefObject {
   private AppDomain _domain;

   public Isolated() {
     _domain = AppDomain.CreateDomain("Isolated:" + Guid.NewGuid(), null,
       AppDomain.CurrentDomain.SetupInformation);
     var type = typeof(T);
     Value = (T)_domain.CreateInstanceAndUnwrap(type.Assembly.FullName, type.FullName);
   }

   public T Value { get; private set; }

   public void Dispose() {
     if (_domain == null) return;
     AppDomain.Unload(_domain);
    _domain = null;
   }
}    

And enclose communication into something like:

using (var isolated = new Isolated<Client>()) {
   string response = isolated.Value.GetResponse(url);
}
Community
  • 1
  • 1
Ondrej Svejdar
  • 21,349
  • 5
  • 54
  • 89