1

I have a ROR application with an API secured by Devise + simple_token_authentication - all working fine. Now I'm building an iOS application using NSURLSession to access the API and handle authentication, which is where i get into trouble.

On load i call the following method to retrieve data from the server. As i understand it, the didReceiveChallenge delegate should be called when getting a 401 unauthorized but nothing happens. I am fairly new to iOS and i might be doing it completely wrong, but i hope someone can help me get past this issue. Thanks!:)

- (void)fetch:(id)sender {

    NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    config.HTTPAdditionalHeaders = @{ @"Accept":@"application/json"};

    self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];

    NSURLSessionTask *dataTask = [self.session dataTaskWithURL:[NSURL URLWithString:@"http://localhost:3000/api/v1/tasks"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        // handle response
        NSLog(@"data %@", data);
    }];

    [dataTask resume];

}

This method never gets called, even after receiving a 401 header.

-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {

    NSString *user = @"email";
    NSString *password = @"secretpass";

    NSLog(@"didReceiveChallenge");

    // should prompt for a password in a real app but we will hard code this baby
    NSURLCredential *secretHandshake = [NSURLCredential credentialWithUser:user password:password persistence:NSURLCredentialPersistenceForSession];

    // use block
    completionHandler(NSURLSessionAuthChallengeUseCredential,secretHandshake);

}
user3108730
  • 33
  • 1
  • 5

2 Answers2

1

If you include a completionHandler in the dataTaskWithURL, then that is what is used and the delegate is never called.

Try setting the completionHandler to Nil:

NSURLSessionTask *dataTask = [self.session dataTaskWithURL:[NSURL URLWithString:@"http://localhost:3000/api/v1/tasks"] completionHandler:Nil];

Then the delegate methods will be used.

On testing this further, you need to return a WWW-Authenticate header to trigger the didReceiveChallenge delegate method. From Apple docs:

Important: The URL loading system classes do not call their delegates to handle request challenges unless the server response contains a WWW-Authenticate header. Other authentication types, such as proxy authentication and TLS trust validation do not require this header.

You can do this by adding a custom authenticate method to your rails app e.g.

  before_filter :authenticate_user!

  private

    def authenticate_user!
      unless current_user
          headers["WWW-Authenticate"] = %(Basic realm="My Realm")          
          render :json => {:message =>I18n.t("errors.messages.authorization_error")}, :status => :unauthorized
      end
    end

I'm still working through this on my app but the above approach does fire the didReceiveChallenge delegate. Hope you find it useful.

A bit more info, the following code in didReceiveChallenge (same as in original question) will handle the logon to the Rails server:

-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
 //   NSLog(@"didReceiveChallenge");

    NSString *ftfID    = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:@"ftfID"];
    NSString *ftfPassword    = (NSString *)[[NSUserDefaults standardUserDefaults] objectForKey:@"ftfPassword"];
    NSString *user = ftfID;
    NSString *password = ftfPassword;

    NSURLCredential *secretHandshake = [NSURLCredential credentialWithUser:user password:password persistence:NSURLCredentialPersistenceForSession];

    // use block
    completionHandler(NSURLSessionAuthChallengeUseCredential,secretHandshake);

}

To download working examples of a Rails and iOS app communicating via json requests see https://github.com/petetodd/bright-green-star-client and https://github.com/petetodd/bright-green-star-server.

Peter Todd
  • 8,561
  • 3
  • 32
  • 38
  • Thanks greentor! this is very helpful. didReceiveChallenge is now working. If you find out some more good stuff, please post it:] i will accept your answer soon – user3108730 Dec 19 '13 at 14:54
  • Glad you found it useful. If you're looking to Post data using NSURLSession you might find this answer useful: [Using NSDictionary to Post params](http://stackoverflow.com/questions/19099448/send-post-request-using-nsurlsession/19101084#19101084) In my iPhone app I register/logon users then post data to my Rails app. – Peter Todd Dec 21 '13 at 10:06
  • Hey @greentor, can i ask how you are handling the json login process with devise? – user3108730 Jan 05 '14 at 15:26
  • A bit more info now added to the answer showing how to deal with logon challenge. – Peter Todd Jan 06 '14 at 08:14
  • Hi @user3108730 - I think you are already doing what I added for didReceiveChallenge. I attempt a post, a challenge is received, logon successful and the post continues (with Rails app using the User I logged on as). If your didReceiveChallenge fires what response are you getting from devise? (use didCompleteWithError delegate to get details) – Peter Todd Jan 06 '14 at 08:33
  • The problem is that i can't access the credentials from the request in rails. I see them in Charles under Authentication -> Server -> basic authentication, but they are not in the params? I am using your example of didReceiveChallenge. – user3108730 Jan 10 '14 at 11:31
  • I'll see if I can create and share a demo on github - ideally both iOS and Rails app. Might take me a couple of days but I'll add another comment and link here when done. – Peter Todd Jan 10 '14 at 20:48
  • Thanks a lot! Its hard to find examples using both Rails and iOS – user3108730 Jan 10 '14 at 21:24
  • Have a look at https://github.com/petetodd/bright-green-star-client and https://github.com/petetodd/bright-green-star-server. This is a very cut down version of what I use but I've tested it for Creating an Account and Creating "Trips" associated with the Account. Your issue is probably in the devise.rb initialiser or in the session or registration controllers (maybe not including respond_to :json)? – Peter Todd Jan 12 '14 at 19:06
0

Be sure you're using the :database_authenticatable strategy in your User model and config.http_authenticatable = true in the devise initializer:

# models/user.rb
...
devise :database_authenticatable, # and more
...


# config/initializers/devise.rb
...
config.http_authenticatable = true
...
Troy
  • 5,319
  • 1
  • 35
  • 41
  • Hey Troy, thanks for your answer. I didn't have config.http_authenticatable = true, but it still doesn't work. I think the problem might be more related to the iOS part and the didReceiveChallenge delegate – user3108730 Dec 17 '13 at 11:38
  • Try adding the other delegate methods to see what's going on - i.e. – URLSession:task:didCompleteWithError: and – URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler: – Troy Dec 17 '13 at 16:37