4

I am attempting to download a PDF from a URL.

private func downloadSessionWithFileURL(_ url: URL){
    var request = URLRequest(url: url)
    
    request.addValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")
   
    let sessionConfig = URLSessionConfiguration.default
    
    let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
    session.downloadTask(with: request).resume()      
}

This calls its delegate method

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    if challenge.previousFailureCount > 0 {
          completionHandler(Foundation.URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
    }
    if let serverTrust = challenge.protectionSpace.serverTrust {
      completionHandler(Foundation.URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: serverTrust))
} else {
      print("unknown state. error: \(String(describing: challenge.error))")
   }
}

The URLAuthenticationChallenges protectionSpace is always serverTrust. When the URL of the PDF is attempted to be accessed it redirects user to a login screen. I would have thought there would be another call to

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

requiring user to enter their credentials but there isn't. So the download task attempts to download the contents of the redirected URL which is a login screen.

My Questions are.

  1. What triggers a URLAuthenticationChallenge for a username and password. is it a specific header value in the HTML?

  2. Which URLAuthenticationChallenge protectionSpace should I be expecting for a username password request from a server.

pkamb
  • 33,281
  • 23
  • 160
  • 191
RyanTCB
  • 7,400
  • 5
  • 42
  • 62

2 Answers2

8

There are two different delegate protocols: for the URLSession itself, and its tasks.

URLSessionDelegate has: public func urlSession(_:didReceive:completionHandler:) URLSessionTaskDelegate has: public func urlSession(_:task:didReceive:completionHandler:)

The URLSessionDelegate is used for server trust issues (e.g. allowing SSL trust when running through Charles or other proxy). The URLSessionTaskDelegate is used for authentication of an individual task.

So to get your authentication challenge, add this to your class:

extension MyClass: URLSessionTaskDelegate {

    public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodDefault ||
            challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {

            let credential = URLCredential(user: self.basicAuthUserName,
                                           password: self.basicAuthPassword,
                                           persistence: .forSession)

            completionHandler(.useCredential, credential)
        }
        else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
}
Ben
  • 1,881
  • 17
  • 20
  • where to get `basicAuthUserName` and `basicAuthPassword` ? – Awais Fayyaz Jul 15 '20 at 13:20
  • @AwaisFayyaz in this case, they've defined those two as instance variables within the class. But you could easily just take them from a `View` with some `TextField` inputs. – nonamorando Jan 13 '21 at 01:26
3

Some basics of SSL:

How SSL works? When client establishes the connection with server (called SSL handshake):
Client connects to server and requests server identify itself.
Server sends certificate to client (include public key)
Client checks if that certificate is valid. If it is, client creates a symmetric key (session key), encrypts with public key, then sends back to server
Server receives encrypted symmetric key, decrypts by its private key, then sends acknowledge packet to client
Client receives ACK and starts the session

1.What triggers a URLAuthenticationChallenge for a username and password. is it a specific header value in the HTML?

If you an have https connection, these methods will be triggered. These are for security purpose to prevent the man in the middle attack. For e.g, I can set up charles proxy server, install the public certificate on simulator/device and can monitor all the request that the app is sending to the actual server and thus obtain the sensitive information(API Keys, token, request Headers, request body etc) which I need to hide from attackers.

Which URLAuthenticationChallenge protectionSpace should I be expecting for a username password request from a server.

You can either Compare the server certificate with the local certificates that you have in your apps:

if let serverCertificate = SecTrustGetCertificateAtIndex(trust, 0) {

       let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data
       let localCer = Bundle.main.path(forResource: "fileName", ofType: "cer")

        if let localCer = localCer {

             if localCer.isEqual(to: serverCertificate) {                                             completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust))
                 return
             }
         }
  }

or you can compare the public keys:

 if let serverCertificate = SecTrustGetCertificateAtIndex(trust, 0), let serverCertificateKey = publicKey(for: serverCertificate) {
        if pinnedKeys().contains(serverCertificateKey) {
            completionHandler(.useCredential, URLCredential(trust: trust))
            return
        }
    }

Comparing public keys is a better approach as when comparing certificates, you have to keep a copy of the local certificate in the app and when the certificates expires which will have to update the certificates in the app, which require an update in the app store.

Deep Arora
  • 1,900
  • 2
  • 24
  • 40