user3108730
user3108730

Reputation: 33

NSURLSession authentication with Rails/Devise API

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

}

Upvotes: 1

Views: 1839

Answers (2)

Peter Todd
Peter Todd

Reputation: 8651

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.

Upvotes: 1

Troy
Troy

Reputation: 5399

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

Upvotes: 0

Related Questions