URLSession Credentials Caching Allowing Authentication with Incorrect Credentials

839 views Asked by At

I am trying to communicate with my company's API in my iOS app. I am using the standard URLSession.

The API will load balance and redirect to a different server automatically, so I've implemented the URLSessionDelegate and URLSessionTaskDelegate methods which handle the redirects.

When I initially login I will get redirected from http://our.api.com to http://our1.api.com or some other version of the API with a different server number. The first time I authenticate with http://our1.api.com it will honor the passed in Authorization header and challenged URLCredential. But if I try to authenticate against the same API again with known bad credentials, the old URLCredential is used and I am able to get into the API when I should not be able to.

Is there a way to force URLSession to never use the cached URLCredential, or otherwise clear out the cached URLCredentials?

Creating the URLSession

let config = URLSessionConfiguration.ephemeral
    config.httpAdditionalHeaders = ["Accept":"application/xml",
                                    "Accept-Language":"en",
                                    "Content-Type":"application/xml"]
    config.requestCachePolicy = .reloadIgnoringLocalCacheData
    config.urlCache = nil
    self.urlSession = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)

Calling to the API

var request = URLRequest(url: thePreRedirectedUrl)
    request.httpMethod = "GET"
    request.addValue("Basic username:password", forHTTPHeaderField: "Authorization")

let task = urlSession?.dataTask(with: request, completionHandler: { (data, response, error) in                        
        // pass endpoint results to completion block
        completionBlock(data, response, error)
    })

    // run the task
    if let task = task {
        task.resume()
    }

URLSessionDelegate and URLSessionTaskDelegate

extension ApiManager: URLSessionDelegate, URLSessionTaskDelegate {

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

    if challenge.previousFailureCount == 0 {
        completionHandler(.useCredential, URLCredential(user: username, password: password, persistence: .none))

    } else {
        completionHandler(.performDefaultHandling, nil)
    }
}

func urlSession(_ session: URLSession,
                task: URLSessionTask,
                willPerformHTTPRedirection response: HTTPURLResponse,
                newRequest request: URLRequest,
                completionHandler: @escaping (URLRequest?) -> Void) {

        var newRequest = URLRequest(url: request.url!)
        newRequest.addValue("Basic username:password", forHTTPHeaderField: "Authorization")
        newRequest.httpMethod = task.originalRequest?.httpMethod
        newRequest.httpBody = task.originalRequest?.httpBody
        completionHandler(newRequest)
    }
}
1

There are 1 answers

0
dgatwood On

The most reliable way is to delete the credential from the user's (macOS) or app's (iOS) keychain.

See Updating and Deleting Keychain Items on Apple's developer website for details, but basically:

NSDictionary *matchingDictionary = @{
    kSecClass: kSecClassInternetPassword,
    kSecAttrSecurityDomain: @"example.com" // <-- This may not be quite the
                                           //     right format for the domain.
};
SecItemDelete((__bridge CFDictionaryRef) matchingDictionary);