iOS/Cocoa - NSURLSession - Handling Basic HTTPS Authorization iOS/Cocoa - NSURLSession - Handling Basic HTTPS Authorization ios ios

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:

  1. 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.

  2. 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.

  3. 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.