urlSession(_:task:didReceive:completionHandler:) does not fire if second request sent within 30 seconds

Greetings,


I'm seeing that authentication challenge is not being issued for client certificate secured URL for second reqeust, if it is being sent within 30 seconds of the first request. Here is the code and steps I'm following,


1. Create a data task from a URL session for a client certificate secured URL.

2. Resume the data task.

3. The authenticateion challenge of type `NSURLAuthenticationMethodClientCertificate` is being issed.

4. Perform default handling on the challenge.

5. The task will finish with an error.

6. Before 30 seconds elapses, preform step 1 and 2 again.

7. The steps 3 and 4 gets skiped and finish the data task as per step 5.


class ViewController: UIViewController, URLSessionTaskDelegate, URLSessionDataDelegate {

    var dataTask: URLSessionDataTask?
    var urlSession: URLSession?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let sessionConfiguration = URLSessionConfiguration.default
        urlSession = Foundation.URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: OperationQueue.main)
    }

    @IBAction func buttonAction(_ sender: Any) {
        let url = URL(string: “Client Certificate Secured URL“)! // ClientCertificate
        //let url = URL(string: “NTLM Secured URL")! // NTLM
        dataTask = urlSession?.dataTask(with: url)
        dataTask?.resume()
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        guard challenge.previousFailureCount == 0 else {
            challenge.sender?.cancel(challenge)
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }
        
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            if let trust = challenge.protectionSpace.serverTrust{
                var trustResult: SecTrustResultType = SecTrustResultType(rawValue: 0)!
                SecTrustEvaluate(trust, &trustResult)
                
                if trustResult == .unspecified || trustResult == .proceed {
                    let credential = URLCredential(trust: trust)
                    completionHandler(.useCredential, credential)
                }
            }
        }
        else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
            print("protectionSpace: \(challenge.protectionSpace)")
            completionHandler(.performDefaultHandling, nil)
        }
        else {
            print("protectionSpace: \(challenge.protectionSpace)")
            completionHandler(.performDefaultHandling, nil)
        }
    }
    
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        let dataString = String(data: data, encoding: .utf8)
        print("dataString: \(String(describing: dataString))")
    }
}

Here are the important things I noticed,


1. If the URL is NTLM secured then there is no issue. The authentication challenge is being issued even if requests are being sent within 30 seconds.

2. Sending request after 30 seconds the authentication challenge is being issued correctly.

3. While debugging I noticed the `URLSessionConfiguration` has an internal property, `_connectionCachePurgeTimeout` which is set to `30`. Is this coming into picture here? I don't see anyother timeout interval set to `30`


Is this a bug or am I missing something?


Sorry, at this time, I do not have public URLs available for you to test this.


Appreciate any help! Thank you!


Regards,

Nimesh

Is this a bug or am I missing something?

You are missing something.

Client certificate authentication applies to each TLS-over-TCP connection. Modern versions of HTTP try to reuse connections as much as possible, because setting up a new connection is slow. In this case it seems you’re using HTTP/1.1, where iOS typically [1] uses a 30 second timeout for its connection cache. If your second request is issued before that timeout it goes over a cached connection and there’s no client identity authentication challenge. In contrast, if your second request is issued after that timeout it goes over a new connection and thus you get another challenge.

Moreover, in many cases the subsequent TLS-over-TCP connection hits the TLS session cache [2], in which case there’s a fast TLS handshake and, again, you see no TLS authentication challenges.

In most cases these ‘missing’ challenges are not a problem. The connection has already been authenticated, so there’s no need to authenticate it again.

Is there a specific reason you need these challenges?

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

[1] The default timeout is not considered API and varies by OS release, by network interface (for WWAN it’s very low, just a few seconds), and by HTTP protocol (HTTP 2 uses a much longer timeout).

[2] QA1727 TLS Session Cache

Thanks for the response!


> Is there a specific reason you need these challenges?


We have our own complex APIs with objects like Credential and Authentication Challenge. As you can see in the code the `performDefaultHandling` happens on the first challenge and it fails with an error. We notifiy the user/dev that they require to provide the credential to make it working. When user/dev provides credential/certificate, we send another request. At this time we expect the authentication challenge to be issued so we can pass on the credential/certificate. However, that does not happen due to this 30 second connection issue.


Is there a work around or fix to overcome this 30 second connection issue?


Regards,

Nimesh

We have our own complex APIs with objects like Credential and Authentication Challenge … We notifiy the user/dev that they require to provide the credential to make it working.

The best way to handle this would be to issue that notification from your challenge handler. Authentication challenge handlers are asynchronous; you don’t have to call the completion handler from the challenge handler itself, you can defer this until you’ve got the credential from your client.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you for the response. I really apprciate it!


It is going to be a big change in our codebase and we'll try to do that in near future but is there any workaround at this time to overcome 30 seconds delay?


Regards,

Nimesh

You can avoid HTTP connection reuse by issuing the second request in a second session. There are circumstances where that’s your only option, but do not do that for every request. HTTP connection reuse is a critical element in HTTP performance, and defeat that element really slows things down.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
urlSession(_:task:didReceive:completionHandler:) does not fire if second request sent within 30 seconds
 
 
Q