Search code examples
objective-cnsurlconnectioncredentialsntlmnsurlcredential

What is the appropriate NSURLConnection/Credential pattern for doing NTLM web service calls?


I have a "traditional" enterprise iPad application that needs to make many different web service calls over its lifetime using NTLM authentication. Upon start up of the application, I anticipate getting the user name and password off of a keychain (which the app saves the first time its used since the keychain does not have the user name and subsequently is updated when the password fails to work due to updates).

On startup, various web service calls are needed to get initial data for the application. The user then will be presented with a tabbed controller to choose the functionality they want which in turn will, of course, do more web service calls.

I believe I have a tactic for dealing with each class receiving data through a custom data delegate as presented in this StackOverflow answer (How do you return from an asynchronous NSURLConnection to the calling class?). However, I'm still a bit confused as to how to properly use the -(void)useCredential:(NSURLCredential *)credential forAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge functionality.

In didReceiveAuthenticationChallenge, I have code like this

[[challenge sender]  useCredential:[NSURLCredential credentialWithUser:@"myusername"
                          password:@"mypassword"
                       persistence:NSURLCredentialPersistencePermanent]
        forAuthenticationChallenge:challenge];

Since I'm setting permanent persistence, I would expect to not to have to constantly pass in the user name and password in the functionality. Is there a pattern that's used to initially setup the user's NTLM credentials (and/or check to see if they're already there) and then just use the "permanent" credentials for subsequent web service calls?

Also, as a secondary question/part to this. What is the appropriate/elegant approach to passing around a username/password throughout an Objective-C application? I'm thinking either global var's or a singleton instance (which seems a bit overkill for just a couple of needed var's).


Solution

  • It has been awhile since we've tackled this issue and successfully solved it. I thought it was time to put up the answer here. The code below belongs in its own class and will not work quite out of the box but should get you a long ways towards what you need. For the most part, it all should work fine, but you'll just need to make sure the various areas such as alert views, data stores, etc. are all set up the way you need.

    A major stumbling block in our understanding of the way that Objective-C & iOS deals with NTLM communications is figuring out its normal process of communicating with a URL.

    First contact with a URL is done anonymously. Of course, in a Windows secure environment this will fail. This is when the application will attempt to contact the URL again but this time with any credentials it has for that URL already on the keychain and utilize the willSendRequestForAuthenticationChallenge method. This was very confusing for us since this method didn't fire until AFTER the first call failed. It finally dawned on us what was happening with that first call being anonymous.

    Part of the pattern you'll see here is that the connection will be attempted with any credentials already on the keychain. If those fail/missing, then we will popup a view that requests the user to enter the username and password and then we retry with that.

    There's a number of idiosyncrasies that we needed to account for as you'll see throughout the code. It took many iterations and lots of testing to get this stable. Much of this was based on patterns that have been posted all over the internet for doing pretty much what we were trying to do but didn't quite take us all the way there.

    The code we did generalizes GET/POST calls. This is my first major code post to StackOverflow and my apologies if I'm missing some conventions and I'll correct what I need to when brought to my attention.

    #import "MYDataFeeder.h"
    #import "MYAppDelegate.h"
    #import "MYDataStore.h"
    #import "MYAuthenticationAlertView.h"
    #import "MYExtensions.h"
    
    @interface MYDataFeeder () <NSURLConnectionDelegate>
        @property (strong, nonatomic) void (^needAuthBlock)(NSString *, NSString *);
        @property (strong, nonatomic) void (^successBlock)(NSData *);
        @property (strong, nonatomic) void (^errorBlock)(NSError *);
    @end
    
    
    @implementation MYDataFeeder{
        NSMutableData *_responseData;
        NSString *_userName;
        NSString *_password;
        NSString *_urlPath;
        BOOL _hasQueryString;
    }
    
    + (void)get: (NSString *)requestString
       userName: (NSString *)userName
       password: (NSString *)password
    hasNewCredentials: (BOOL)hasNewCredentials
    successBlock: (void (^)(NSData *))successBlock
     errorBlock: (void (^)(NSError *))errorBlock
    needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock
    {
        MYDataFeeder *x = [[MYDataFeeder alloc] initWithGetRequest:requestString userName:userName password:password hasNewCredentials:hasNewCredentials successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock];
    }
    
    + (void)post: (NSString *)requestString
        userName: (NSString *)userName
        password: (NSString *)password
    hasNewCredentials: (BOOL)hasNewCredentials
      jsonString: (NSString *)jsonString
    successBlock: (void (^)(NSData *))successBlock
      errorBlock: (void (^)(NSError *))errorBlock
    needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock
    {
        MYDataFeeder *x = [[MYDataFeeder alloc] initWithPostRequest:requestString userName:userName password:password hasNewCredentials:hasNewCredentials jsonString:jsonString successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock];
    }
    
    - (instancetype)initWithGetRequest: (NSString *)requestString
                              userName: (NSString *)userName
                              password: (NSString *)password
                     hasNewCredentials: (BOOL)hasNewCredentials
                          successBlock: (void (^)(NSData *))successBlock
                            errorBlock: (void (^)(NSError *))errorBlock
                         needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock
    {
        return [self initWithRequest:requestString userName:userName password:password hasNewCredentials:hasNewCredentials isPost:NO jsonString:nil successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock];
    }
    
    -(instancetype)initWithPostRequest: (NSString *)requestString
                              userName: (NSString *)userName
                              password: (NSString *)password
                     hasNewCredentials: (BOOL)hasNewCredentials
                            jsonString: (NSString *)jsonString
                          successBlock: (void (^)(NSData *))successBlock
                            errorBlock: (void (^)(NSError *))errorBlock
                         needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock
    {
        return [self initWithRequest:requestString userName:userName password:password hasNewCredentials:hasNewCredentials isPost:YES jsonString:jsonString successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock];
    }
    
    //Used for NTLM authentication when user/pwd needs updating
    - (instancetype)initWithRequest: (NSString *)requestString
                           userName: (NSString *)userName
                           password: (NSString *)password
                  hasNewCredentials: (BOOL)hasNewCredentials
                             isPost: (BOOL)isPost
                           jsonString: (NSString *)jsonString
                       successBlock: (void (^)(NSData *))successBlock
                         errorBlock: (void (^)(NSError *))errorBlock
                      needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock //delegate:(id<MYDataFeederDelegate>)delegate
    {
        self = [super init];
    
        requestString = [requestString stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
    
        if(self) {
            if (!errorBlock || !successBlock || !needAuthBlock) {
                [NSException raise:@"MYDataFeeder Error" format:@"Missing one or more execution blocks. Need Success, Error, and NeedAuth blocks."];
            }
    
            _responseData = [NSMutableData new];
            _userName = userName;
            _password = password;
            _successBlock = successBlock;
            _hasNewCredentials = hasNewCredentials;
            _errorBlock = errorBlock;
            _needAuthBlock = needAuthBlock;
            NSString *host = [MYDataStore sharedStore].host; //Get the host string
            int port = [MYDataStore sharedStore].port; //Get the port value
            NSString *portString = @"";
    
            if (port > 0) {
                portString = [NSString stringWithFormat:@":%i", port];
            }
    
            requestString = [NSString stringWithFormat:@"%@%@/%@", host, portString, requestString];
            NSURL *url = [NSURL URLWithString:requestString];
    
            NSString *absoluteURLPath = [url absoluteString];
            NSUInteger queryLength = [[url query] length];
            _hasQueryString = queryLength > 0;
            _urlPath = (queryLength ? [absoluteURLPath substringToIndex:[absoluteURLPath length] - (queryLength + 1)] : absoluteURLPath);
    
            NSTimeInterval timeInterval = 60; //seconds (60 default)
    
            NSMutableURLRequest *request;
    
            if (isPost) {
                request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:timeInterval];
    
                NSData *requestData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
    
                [request setHTTPMethod:@"POST"];
                [request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
                [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
                [request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)requestData.length] forHTTPHeaderField:@"Content-Length"];
                [request setHTTPBody: requestData];
                [request setHTTPShouldHandleCookies:YES];
            }
            else {
                request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:timeInterval];
            }
    
            NSURLConnection* connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
        }
    
        return self;
    }
    
    - (instancetype)initWithRequest: (NSString *)requestString
                       successBlock: (void (^)(NSData *))successBlock
                         errorBlock: (void (^)(NSError *))errorBlock
                      needAuthBlock: (void (^)(NSString *, NSString *))needAuthBlock //delegate:(id<MYDataFeederDelegate>)delegate
    {
        return [self initWithRequest:requestString userName:NULL password:NULL hasNewCredentials:NO isPost:NO jsonString:nil successBlock:successBlock errorBlock:errorBlock needAuthBlock:needAuthBlock]; //delegate:delegate];
    }
    
    #pragma mark - Connection Events
    
    - (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
        return YES;
    }
    
    - (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection {
        return YES;
    }
    
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
    {
        if (response){
            NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
            NSInteger code = httpResponse.statusCode;
    
            if (code == 401){
                NSLog(@"received 401 response");
                [MYAuthenticationAlertView showWithCallback:_needAuthBlock];
                [connection cancel];
            }
        }
    }
    
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
        _successBlock(_responseData);
    }
    
    -(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
        [_responseData appendData:data];
    }
    
    
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
    {
        _errorBlock(error);
    }
    
    - (void) connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
    {
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodNTLM])
        {
            BOOL hasConnectionCredentials = [[MYDataStore sharedStore] hasConnectionCredentials]; //Determines if there's already credentials existing (see method stub below)
            long previousFailureCount = [challenge previousFailureCount];
    
            BOOL hasFailedAuth = NO;
    
            //If the application has already gotten credentials at least once, then see if there's a response failure...
            if (hasConnectionCredentials){
                //Determine if this URL (sans querystring) has already been called; if not, then assume the URL can be called, otherwise there's probably an error...
                if ([[MYDataStore sharedStore] isURLUsed:_urlPath addURL:YES] && !_hasQueryString){
                    NSURLResponse *failureResponse = [challenge failureResponse];
    
                    if (failureResponse){
                        NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)[challenge failureResponse];
                        long code = [httpResponse statusCode];
    
                        if (code == 401){
                            hasFailedAuth = YES;
                        }
                    }
                }
            }
            else{
                //Need to get user's credentials for authentication...
                NSLog(@"Does not have proper Credentials; possible auto-retry with proper protection space.");
            }
    
            /*    This is very, very important to check.  Depending on how your security policies are setup, you could lock your user out of his or her account by trying to use the wrong credentials too many times in a row.    */
            if (!_hasNewCredentials && ((previousFailureCount > 0) || hasFailedAuth))
            {
                NSLog(@"prompt for new creds");
                NSLog(@"Previous Failure Count: %li", previousFailureCount);
                [[challenge sender] cancelAuthenticationChallenge:challenge];
                [MYAuthenticationAlertView showWithCallback:_needAuthBlock];
                [connection cancel];
            }
            else
            {
                if (_hasNewCredentials){
                    //If there's new credential information and failures, then request new credentials again...
                    if (previousFailureCount > 0) {
                        NSLog(@"new creds failed");
                        [MYAuthenticationAlertView showWithCallback:_needAuthBlock];
                        [connection cancel];
                    } else {
                        NSLog(@"use new creds");
                        //If there's new credential information and no failures, then pass them through...
                        [[challenge sender]  useCredential:[NSURLCredential credentialWithUser:_userName password:_password persistence:NSURLCredentialPersistencePermanent] forAuthenticationChallenge:challenge];
                    }
                } else {
                    NSLog(@"use stored creds");
                    //...otherwise, use any stored credentials to call URL...
                    [[challenge sender] performDefaultHandlingForAuthenticationChallenge:challenge];
                }
            }
        }
        else if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { // server trust challenge
            // make sure challenge came from environment host
            if ([[MYDataStore sharedStore].host containsString:challenge.protectionSpace.host]) {
                [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
            }
            [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
        }
        else {
            // request has failed
            [[challenge sender] cancelAuthenticationChallenge:challenge];
        }
    }
    
    @end
    
    -(BOOL) hasConnectionCredentials
    {
        NSDictionary *credentialsDict = [[NSURLCredentialStorage sharedCredentialStorage] allCredentials];
        return ([credentialsDict count] > 0);
    }
    
    //Sample use of Data Feeder and blocks:
    -(void)myMethodToGetDataWithUserName:(NSString*)userName password:(NSString*)password{
    //do stuff here
    [MYDataFeeder get:@"myURL"
    userName:userName
    password:password
    hasNewCredentials:(userName != nil)
    successBlock:^(NSData *response){ [self processResponse:response]; }
                errorBlock:^(NSError *error) { NSLog(@"URL Error: %@", error); }
             needAuthBlock:^(NSString *userName, NSString *password) { [self myMethodToGetDataWithUserName:username withPassword:password]; }
    ];
    }
    
    //The needAuthBlock recalls the same method but now passing in user name and password that was queried from within an AlertView called from within the original DataFeeder call