Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:[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!

like image 973
Womble Avatar asked Jan 14 '14 03:01

Womble


2 Answers

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

 }];
like image 140
malhal Avatar answered Sep 28 '22 03:09

malhal


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

like image 23
markeissler Avatar answered Sep 28 '22 03:09

markeissler