Reputation: 5022
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%[email protected]/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%[email protected]/v1/user/api_token?format\=json
where the password contains non-ASCII characters?
Upvotes: 0
Views: 676
Reputation: 130132
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%[email protected]"] 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.
Upvotes: 1