iOS/Cocoa - NSURLSession - Handling Basic HTTPS Authorization

筅森魡賤 提交于 2019-11-28 06:31:54

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:

  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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!