iOS/Cocoa - NSURLSession - Handling Basic HTTPS Authorization

Womble picture Womble · Jan 14, 2014 · Viewed 24.1k times · Source

[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:[email protected]/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!

Answer

malhal picture malhal · Aug 9, 2014

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) {

 }];