Question

Here is a method I've written to connect to a server and get a user auth token:

+ (void)getAuthTokenForUsername:(NSString *)username
                       password:(NSString *)password
              completionHandler:(void (^)(NSString *, NSError *))completionHandler
{
    username = [username URLEncodedString];
    password = [password URLEncodedString];

    NSString *format = @"https://%@:%@@api.example.com/v1/user/api_token?format=json";
    NSString *string = [NSString stringWithFormat:format, username, password];
    NSURL *URL = [NSURL URLWithString:string];
    [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:URL]
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse *URLResponse, NSData *data, NSError *error)
    {
        NSString *token;
        if (data) {
            NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
            token = [NSString stringWithFormat:@"%@:%@", username, dictionary[@"result"]];
        }
        completionHandler(token, error);
    }];
}

A URL then looks something like this: https://username:hello%C2%B0@api.example.com/v1/user/api_token?format\=json, where the password is hello°. The URLEncodedString method properly encodes everything as in the example, but the request never works. The problem is not with escaping or the server, because I can curl the same URL and I get nice JSON and authentication works, even though there is a non-ASCII character in the password. It also works from other programming languages like ruby or python. But the same url never works with NSURLConnection and it also doesn't work in Safari, which of course uses NSURLConnection. I get an 'The operation could not be completed' with a '401 Forbidden' every time.

(My code works fine when the password just contains ASCII characters. I also tried using the NSURLCredential methods, same problem.)

What do I need to do for NSURLConnection to work with such a URL as https://username:hello%C2%B0@api.example.com/v1/user/api_token?format\=json where the password contains non-ASCII characters?

Was it helpful?

Solution

I have just performed several tests against my mockup server and I think I have a solution for you.

First of all, when you add username & password to an URL, they are not actually send to the server as part of the URL. They are sent as part of the Authorization header (see Basic access authentication).

The fastest workaround for you is to do

NSURLRequest* request = [NSURLRequest requestWithURL:URL];
NSString* usernamePassword = [[NSString stringWithFormat:@"%@:%@", username, password] base64Encode];
[request setValue:[NSString stringWithFormat:@"Basic %@", usernamePassword] forHTTPHeaderField:@"Authorization"] 

To understand the problem, let's go a bit deeper. Let's forget NSURLConnection sendAsynchronousRequest: and let us create an old-fashioned connection with a NSURLConnectionDelegate. Then in the delegate, let's define the following methods:

- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
    return YES;
}

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    NSLog(@"Proposal: %@ - %@", challenge.proposedCredential.user, challenge.proposedCredential.password);

    NSURLCredential* credential = [NSURLCredential credentialWithUser:@"username"    
                                                             password:@"hello°"
                                                          persistence:NSURLCredentialPersistenceNone];

    [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
}

If you don't create these methods, the username & password from your URL won't ever be added to the HTTP header. If you add them, you'll see that the proposed password is hello%C2%B0. Obviously, that's wrong.

You can see the problem directly from

NSLog(@"Password: %@", [[NSURL URLWithString:@"http://username:hello%C2%B0@www.google.com"] password]);

which prints

hello%C2%B0  

I believe this is a bug in the framework. NSURL returns password encoded and NSURLCredential doesn't decode it so we are left with an invalid password.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top