[edited to provide more information]
(I'm not using AFNetworking for this project. I may do so in future, but wish to resolve this problem/misunderstanding first.)
SERVER SETUP
I cannot provide the real service here, but it is a simple, reliable service that returns XML according to a URL such as:
https://username:password@example.com/webservice
I want to connect to the URL over HTTPS using GET, and determine any authentication failures (http status code 401).
I have confirmed that the web service is available, and that I can successfully (http status code 200) grab XML from the url using a specified username and password. I have done this with a web browser, and with AFNetworking 2.0.3, and by using NSURLConnection.
I have also confirmed that I am using the correct credentials at all stages.
Given the correct credentials and the the following code:
// Note: NO delegate provided here.
self.sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfig
delegate:nil
delegateQueue:nil];
NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:self.requestURL completionHandler: ...
The above code will work. It will successfully connect to the server, get a http status code of 200, and return the (XML) data.
PROBLEM 1
This simple approach fails in cases where the credentials are invalid. In that case, the completion block is never called, no status code (401) is provided, and eventually, the Task times out.
ATTEMPTED SOLUTION
I assigned a delegate to the NSURLSession, and am handling the following callbacks:
-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
if (_sessionFailureCount == 0) {
NSURLCredential *cred = [NSURLCredential credentialWithUser:self.userName password:self.password persistence:NSURLCredentialPersistenceNone];
completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
_sessionFailureCount++;
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
if (_taskFailureCount == 0) {
NSURLCredential *cred = [NSURLCredential credentialWithUser:self.userName password:self.password persistence:NSURLCredentialPersistenceNone];
completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
_taskFailureCount++;
}
PROBLEM 1 WHEN USING ATTEMPTED SOLUTION
Please note the use of ivars _sessionFailureCount and _taskFailureCount. I am using these because the challenge object's @previousFailureCount property is never advanced! It always remains at zero, no matter how many times these callback methods are called.
PROBLEM 2 WHEN USING ATTEMPTED SOLUTION
Despite the use of correct credentials (as proven by their successful use with a nil delegate), authentication is failing.
The following callbacks occur:
URLSession:didReceiveChallenge:completionHandler:
(challenge @ previousFailureCount reports as zero)
(_sessionFailureCount reports as zero)
(completion handler is called with correct credentials)
(there is no challenge @error provided)
(there is no challenge @failureResponse provided)
URLSession:didReceiveChallenge:completionHandler:
(challenge @ previousFailureCount reports as **zero**!!)
(_sessionFailureCount reports as one)
(completion handler is called with request to cancel challenge)
(there is no challenge @error provided)
(there is no challenge @failureResponse provided)
// Finally, the Data Task's completion handler is then called on us.
(the http status code is reported as zero)
(the NSError is reported as NSURLErrorDomain Code=-999 "cancelled")
(The NSError also provides a NSErrorFailingURLKey, which shows me that the URL and credentials are correct.)
Any suggestions welcome!
Prompted vs Unprompted HTTP Authentication
It seems to me that all documentation on NSURLSession and HTTP Authentication skips over the fact that the requirement for authentication can be prompted (as is the case when using an .htpassword file) or unprompted (as is the usual case when dealing with a REST service).
For the prompted case, the correct strategy is to implement the delegate method:
URLSession:task:didReceiveChallenge:completionHandler:
; for the unprompted case, implementation of the delegate method will only provide you with the opportunity to verify the SSL challenge (e.g. the protection space). Therefore, when dealing with REST, you will likely need to add Authentication headers manually as @malhal pointed out.Here is a more detailed solution that skips the creation of an NSURLRequest.
Hopefully this will help anyone that runs into this poorly documented difference. I finally figured it out using test code, a local proxy ProxyApp and forcibly disabling
NSAppTransportSecurity
in my project'sInfo.plist
file (necessary for inspecting SSL traffic via a proxy on iOS 9/OSX 10.11).Short answer: The behavior you describe is consistent with a basic server authentication failure. I know you've reported that you've verified that it's correct, but I suspect some fundamental validation problem on the server (not your iOS code).
Long answer:
If you use
NSURLSession
without the delegate and include the userid/password in the URL, thencompletionHandler
block of theNSURLSessionDataTask
will be called if the userid/password combination is correct. But, if the authentication fails,NSURLSession
appears to repeatedly attempt to make the request, using the same authentication credentials every time, and thecompletionHandler
doesn't appear to get called. (I noticed that by watching the connection with Charles Proxy).This doesn't strike me as very prudent of
NSURLSession
, but then again the delegate-less rendition can't really do much more than that. When using authentication, using thedelegate
-based approach seems more robust.If you use the
NSURLSession
with thedelegate
specified (and nocompletionHandler
parameter when you create the data task), you can examine the nature of the error indidReceiveChallenge
, namely examine thechallenge.error
and thechallenge.failureResponse
objects. You might want to update your question with those results.As an aside, you appear to be maintaining your own
_failureCount
counter, but you can probably avail yourself ofchallenge.previousFailureCount
property, instead.Perhaps you can share some particulars about the nature of the authentication your server is using. I only ask, because when I secure a directory on my web server, it does not call the
NSURLSessionDelegate
method:But rather, it calls the
NSURLSessionTaskDelegate
method:Like I said, the behavior you describe is consist with an authentication failure on the server. Sharing the details about the nature of the authentication setting on your server and the particulars of the
NSURLAuthenticationChallenge
object might help us diagnose what's going on. You might also want to type the URL with the userid/password in a web browser and that might also confirm whether there is a basic authentication problem.You don't need to implement a delegate method for this, simply set the authorization HTTP header on the request, e.g.