NSURLCredentialStorage setDefaultCredential: doesn't work for [NSURLSession sharedSession] - ios

I'm stumped: my app needs to connect to a server which uses self-signed certificates for HTTPS and requires client-side authentication. Worse, I actually need the iOS media player to connect to that server, so I have followed Apple's instruction for this to the letter:
credential = [NSURLCredential credentialWithIdentity:identity certificates:certs persistence:NSURLCredentialPersistenceForSession];
NSURLProtectionSpace *space = [[NSURLProtectionSpace alloc] initWithHost:#"server.com"
port:0
protocol:NSURLProtectionSpaceHTTPS
realm:nil
authenticationMethod:NSURLAuthenticationMethodClientCertificate];
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:space];
But it just won't work. So I tried to do a request to the server manually:
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:#"https://server.com"]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(#"Done : %#", error ? error : #"OK");
}];
all I get is this error:
2016-06-13 08:22:37.767 TestiOSSSL[3172:870700] CFNetwork SSLHandshake failed (-9824 -> -9829)
2016-06-13 08:22:37.793 TestiOSSSL[3172:870700] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9829)
2016-06-13 08:22:37.815 TestiOSSSL[3172:870685] Done : Error Domain=NSURLErrorDomain Code=-1206 "The server “ server.com” requires a client certificate." UserInfo={NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x13de519b0>, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9829, NSUnderlyingError=0x13de4f280 {Error Domain=kCFErrorDomainCFNetwork Code=-1206 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=1, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x13de519b0>, _kCFNetworkCFStreamSSLErrorOriginalValue=-9829, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9829, kCFStreamPropertySSLPeerCertificates=<CFArray 0x13dda0a70 [0x1a0dc2150]>{type = immutable, count = 2, values = (
0 : <cert(0x13dda4970) s: Server.com i: Localhost CA>
1 : <cert(0x13dda50d0) s: Localhost CA i: Localhost CA>
)}}}, NSErrorPeerCertificateChainKey=<CFArray 0x13dda0a70 [0x1a0dc2150]>{type = immutable, count = 2, values = (
0 : <cert(0x13dda4970) s: Server.com i: Localhost CA>
1 : <cert(0x13dda50d0) s: Localhost CA i: Localhost CA>
)}, NSLocalizedDescription=The server “server.com” requires a client certificate., NSErrorFailingURLKey=https://server.com/, NSErrorFailingURLStringKey=https://server.com/, NSErrorClientCertificateStateKey=1}
Now if I set up my own NSURLSession and use the URLSession:didReceiveChallenge:completionHandler: callback:
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
theSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
NSURLSessionDataTask *task = [theSession dataTaskWithURL:[NSURL URLWithString:#"https://server.com"]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(#"Done : %#", error ? error : #"OK");
}];
and then:
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
NSURLCredential *credential))completionHandler
{
NSLog(#"Asking for credential");
NSURLCredential *conf = [session.configuration.URLCredentialStorage defaultCredentialForProtectionSpace:challenge.protectionSpace];
completionHandler(NSURLSessionAuthChallengeUseCredential, conf);
}
Notice how I'm using [session.configuration.URLCredentialStorage defaultCredentialForProtectionSpace:challenge.protectionSpace], which is what I suppose the default implementation of the NSURLSession does when it gets an authentication challenge.
That works, for this particular connection! Which proves that the credential is OK and that it is properly registered as the default credential in the default NSURLCredentialStorage.
But any solution hinging around the didReceiveChallenge: callback is no good because I can't control which NSURLSession the media player is using.
I've tried the CustomHTTPProtocol hack and that doesn't work either.
Any suggestion? I've gone through all the similar posts on SO, I can't find a solution for this. This post is really close, but the accepted answer doesn't make sense to me and clearly contradicts Apple's documentation.

Although lots of functionality is shared between the default session and NSURLConnection, apparently that bit isn't. Have you tried calling that method on [NSURLSession sharedSession].configuration.URLCredentialStorage?
The other possibility is that the requests are happening in a separate task, in which case it may not be possible to do it in the way that you're trying, because it will involve a different shared session. If that's the case, you'll probably have to store the credential into the keychain yourself and trust that the other process will share the keychain and fetch the credential properly.

Related

iOS app binary rejected - IPv6

I submitted an app to the store which was subsequently rejected, as it connects to an external server to load a json feed, which runs on IPv4. There are two separate internet connections where I work. The app successfully loads the json feed on one of the connections, but returns a 404 not found error on the other. Obviously when the app was in review it must have returned a 404 error. I am using a NSURLSession to connect to the api, as I understand it this is able to handle IPv4 to IPv6 mapping. What other method can I use to prevent this 404 not found error? The following is a snippet of my code:
NSURLSession *session = [NSURLSession sharedSession];
NSLog(#"%#", session.configuration);
[[session dataTaskWithURL:[NSURL URLWithString:jSONURLString]
completionHandler:^(NSData *rawData,
NSURLResponse *response,
NSError *error) {
if ((rawData != nil) && (error == nil)) {
//NSLog(#"Data: %#", rawData);
//NSLog(#"%#",response);
dispatch_async(dispatch_get_main_queue(), ^{
[self performSelector:#selector(returnRawData:) withObject:rawData];
});
}
else {
NSLog(#"error...");
dispatch_async(dispatch_get_main_queue(), ^{
[self performSelector:#selector(noFeedReturned)];
});
}
// handle response
}] resume];
Solution was to reconfigure the server to be compatible with both IPv4 and IPv6

NSURLSession - Ignore SSL certificate warning

I am working on an iOS 8+ App that should allow the users to communicate with a WebService on their own servers. Of course using a HTTPS connection would best practice to do this but in reality there will be a lot of users who do not have a (trusted) SSL certificate on their server.
I would like to allow the users to decide on their own whether they want to use plain HTTP or HTTPS. Additionally HTTPS should work, even if the server has now valid SSL certificate. Since the certificate "only" ensures the identity of the server but has no effect on the encryption of the connection it self, untrustes HTTPS sill has its advantages over plain HTTP:
I know that certificates have been invented for a good reason
I know about the risks of MITM attacks
I agree that using a valid SSL cert would be the best option
I still believe, that using HTTPS without a cert should be an option for the users.
So, how to do this?
First I worked with NSURLConnection sendAsynchronousRequest but since this does not allow any control about the cert checking process (and because it is deprecated in iOS 9) I switched to NSURLSession.
I followed Apples docs to trust my server but had no success:
- (id)init {
...
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:[NSOperationQueue mainQueue]];
...
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
SecTrustRef trust = challenge.protectionSpace.serverTrust;
NSURLCredential *credential = [NSURLCredential credentialForTrust:trust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
}
- (void)sendRequest:(NSURL *)URL {
NSURLRequest* request = [[NSURLRequest alloc] initWithURL:url];
[[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
// ERROR
--> Log error
} else {
// SUCCESS
}
}] resume];
}
- (void)test {
// HTTP works fine
[self sendRequest:[NSURL URLWithString:#"http://my.test.page.xy"]];
// Error with HTTPS
[self sendRequest:[NSURL URLWithString:#"https://my.test.page.xy"]];
// Error Domain=NSURLErrorDomain
// Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made."
// UserInfo={
// _kCFStreamErrorCodeKey=-9824,
// NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?,
// NSUnderlyingError= {
// Error Domain=kCFErrorDomainCFNetwork
// Code=-1200
// UserInfo={
// _kCFStreamPropertySSLClientCertificateState=0,
// _kCFNetworkCFStreamSSLErrorOriginalValue=-9824,
// _kCFStreamErrorDomainKey=3,
// _kCFStreamErrorCodeKey=-9824
// }
// },
// NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made.,
// NSErrorFailingURLKey=https://my.test.page.xy,
// NSErrorFailingURLStringKey=https://my.test.page.xy,
// _kCFStreamErrorDomainKey=3
// }
}
Whether I handle URLSession:session task:didReceiveChallenge:challenge completionHandler: or not makes no difference. The error is the same.
So, any idea how to use HTTPS on servers without a certificate? According to the Apples docs this should work, shouldn't it?
I think that you have to actually evaluate the trust object once (though the actual result of that evaluation is ignored).
With that said, please check to make sure it is actually your cert before you accept it. See Overriding SSL Chain Validation Correctly for examples of how to do this.

NSURLSession Not Reaching Server

I've created an iPhone app for my Arduino, and basically, the Arduino can communicate over the local network using very basic commands provided by a 3rd party REST API. I've successfully been able to use the API via my computer's web browser, but when trying to send a request to it via an iPhone app, it doesn't seem to want to work. Also keep in mind, I can get the API to respond properly via Safari on my iPhone. The only response I'm getting (inside the console) is:
{ URL: http://192.168.0.216/mode/7/0 } { status code: 200, headers {
Connection = close;
"Content-Type" = "application/json";
} } : <7b226d65 73736167 65223a20 2250696e 20443722 6964223a 20223030 38222c20 226e616d 65223a20 226d6967 6874795f 63617422 2c202263 6f6e6e65 63746564 223a2074 7275657d 0d0a>
The API is indeed supposed to return JSON data, but the response on the web browser actually affects my Arduino's LED.
Code for Turning the LED on
NSURL *modeSet = [NSURL URLWithString:[NSString stringWithFormat:#"http://192.168.0.216/digital/%d/1", _pin]];
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:modeSet
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSLog([NSString stringWithFormat:#"%# : %#", response, data]);
}] resume];
EDIT: I decided to print out the 'Error' variable to see if it was holding anything back from me, and I found this:
Error Domain=NSURLErrorDomain Code=-1001 "The operation couldn’t be completed.
(NSURLErrorDomain error -1001.)" UserInfo=0x17807b840 {NSErrorFailingURLStringKey=http://192.168.0.216/mode/7/o,
NSUnderlyingError=0x178449450 "The operation couldn’t be completed.
(kCFErrorDomainCFNetwork error -1001.)", NSErrorFailingURLKey=http://192.168.0.216/mode/7/o}
Pre-iOS 9 Answer
Answering my own question so if anyone finds this by Google sometime, they won't have to ask.
All I did was formatted my string correctly with NSUTF8Encoding like so:
NSString *modeSetString = [[NSString stringWithFormat:#"http://192.168.0.216/mode/%d/o", _pin] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *modeSet = [NSURL URLWithString:modeSetString];
iOS 9 Update
stringByReplacingPercentEscapesUsingEncoding: is now deprecated and stringByRemovingPercentEncoding should be used instead like so:
NSString *modeSetString = [[NSString stringWithFormat:#"http://192.168.0.216/mode/%d/o", _pin] stringByRemovingPercentEncoding];
NSURL *modeSet = [NSURL URLWithString:modeSetString];

NSURLErrorDomain Code -1002 downloading pdf

I'm trying to cache a webpage that I can then later show using a UIWebView.
I have the relevant NSURLSessionDataTask inside a for loop (trying to cache 6 webpages) inside the completion block of another NSURLSessionDataTask. When I run, I keep getting this error:
Ayy there was error downloading, data:<>
response:(null)
error:Error Domain=NSURLErrorDomain Code=-1002 "The operation couldn’t be completed. (NSURLErrorDomain error -1002.)" UserInfo=0xdd89d30 {NSUnderlyingError=0xdd89ba0 "The operation couldn’t be completed. (kCFErrorDomainCFNetwork error -1002.)"}
Here's a snippet of what I'm calling
for (MAClass *class in [myDictResult objectForKey:#"classes"]) {
NSString *PRURL = [[[class assignments] objectAtIndex:[[class assignments] count]-1] assignmentName];
NSLog(#"PRURL is %#", PRURL);
NSURLSessionDataTask *progressReportTask = [defaultSession dataTaskWithURL:[NSURL URLWithString:PRURL] completionHandler:^(NSData *progressReportData, NSURLResponse *progressReportResponse, NSError *progressReportError) {
if ([progressReportData length] > 0 && progressReportError == nil) {
NSLog(#"got dat data");
} else NSLog(#"Error with getting data data:%#\nresponse:%#\nerror:%#", progressReportData, progressReportResponse, progressReportError);
}];
[progressReportTask resume];
NSLog(#"After request");
}
I've made sure that the URL is valid, seeing that was the cause for other people with getting the same error (my urls are like https://mistar.oakland.k12.mi.us/novi/StudentPortal/Home/PrintProgressReport/20152193^HS4, which are valid when I put them into a browser)
What am I doing wrong?
-1002 is NSURLErrorUnsupportedURL/kCFURLErrorUnsupportedURL. In the future, you can either search the Xcode documentation for NSURLErrorDomain or use quick open (shift+command+O) to browser the headers for the definition of NSURLErrorDomain. Either technique would have lead you to discover that -1002 in NSURLErrorDomain is NSURLErrorUnsupportedURL.
The reason for this error is that your URL contains some characters that have to be percent escaped. And web browsers will frequently do the necessary percent-escaping for you, which is why it works there.
You can use stringByAddingPercentEscapesUsingEncoding to convert the URL to an acceptable format:
NSString *urlString = #"https://mistar.oakland.k12.mi.us/novi/StudentPortal/Home/PrintProgressReport/20152193^HS4";
NSURL *url = [NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSURLSessionTask *task = [defaultSession dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
...
}];
By the way, when reconciling web browser results against the app, Charles is very useful. Run the request from browser and again from the app and compare the results in Charles. If you had compared these, you would have seen that you needed to percent escape the URL.
By the way, you can also refer to section 2 of RFC 3986 for a technical description of what characters in URLs must be percent escaped.
If your urlString contains a query string, also consider using NSURLQueryItem to build the queryString.
It will create the URL in its acceptable format.
Here is an example of how to put it into use: Building URLs with NSURLQueryItems and NSURLComponents.

iOS/Cocoa - NSURLSession - Handling Basic HTTPS Authorization

[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!
You don't need to implement a delegate method for this, simply set the authorization HTTP header on the request, e.g.
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"https://whatever.com"]];
NSString *authStr = #"username:password";
NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding];
NSString *authValue = [NSString stringWithFormat: #"Basic %#",[authData base64EncodedStringWithOptions:0]];
[request setValue:authValue forHTTPHeaderField:#"Authorization"];
//create the task
NSURLSessionDataTask* task = [NSURLSession.sharedSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
}];
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.
//
// REST and unprompted HTTP Basic Authentication
//
// 1 - define credentials as a string with format:
// "username:password"
//
NSString *username = #"USERID";
NSString *password = #"SECRET";
NSString *authString = [NSString stringWithFormat:#"%#:%#",
username,
secret];
// 2 - convert authString to an NSData instance
NSData *authData = [authString dataUsingEncoding:NSUTF8StringEncoding];
// 3 - build the header string with base64 encoded data
NSString *authHeader = [NSString stringWithFormat: #"Basic %#",
[authData base64EncodedStringWithOptions:0]];
// 4 - create an NSURLSessionConfiguration instance
NSURLSessionConfiguration *sessionConfig =
[NSURLSessionConfiguration defaultSessionConfiguration];
// 5 - add custom headers, including the Authorization header
[sessionConfig setHTTPAdditionalHeaders:#{
#"Accept": #"application/json",
#"Authorization": authHeader
}
];
// 6 - create an NSURLSession instance
NSURLSession *session =
[NSURLSession sessionWithConfiguration:sessionConfig delegate:self
delegateQueue:nil];
// 7 - create an NSURLSessionDataTask instance
NSString *urlString = #"https://API.DOMAIN.COM/v1/locations";
NSURL *url = [NSURL URLWithString:urlString];
NSURLSessionDataTask *task = [session dataTaskWithURL:url
completionHandler:
^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) {
if (error)
{
// do something with the error
return;
}
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode == 200)
{
// success: do something with returned data
} else {
// failure: do something else on failure
NSLog(#"httpResponse code: %#", [NSString stringWithFormat:#"%ld", (unsigned long)httpResponse.statusCode]);
NSLog(#"httpResponse head: %#", httpResponse.allHeaderFields);
return;
}
}];
// 8 - resume the task
[task resume];
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's Info.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, then completionHandler block of the NSURLSessionDataTask 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 the completionHandler 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 the delegate-based approach seems more robust.
If you use the NSURLSession with the delegate specified (and no completionHandler parameter when you create the data task), you can examine the nature of the error in didReceiveChallenge, namely examine the challenge.error and the challenge.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 of challenge.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:
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
But rather, it calls the NSURLSessionTaskDelegate method:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
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.

Resources