iOS/Cocoa - NSURLSession - Handling Basic HTTPS Authorization
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 taskNSURLSessionDataTask* 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, 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:- (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.