- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { NSLog(@"got auth challange"); if ([challenge previousFailureCount] == 0) { [[challenge sender] useCredential:[NSURLCredential credentialWithUser:[usernameTextField text] password:[passwordTextField text] persistence:NSURLCredentialPersistenceForSession] forAuthenticationChallenge:challenge]; } else { [[challenge sender] cancelAuthenticationChallenge:challenge]; } }The persistence argument of the NSURLCredential constructor tells the system how to persist the credentials (D'uh!). NSURLCredentialPersistenceForSession means for the whole session, NSURLCredentialPersistencePermanent means permanently (will be saved in the keychain, but doesn't work in the iPhone simulator) and NSURLCredentialPersistenceNone SHOULD mean not at all. The problem is: it doesn't. When you specify NSURLCredentialPersistenceForSession the credentials are stored in [[NSURLCredentialStorage sharedCredentialStorage] allCredentials]. Naive as I am, I figured removing the credentials should prompt the connection:didReceiveAuthenticationChallenge: delegate method to be called again upon the next request:
// reset the credentials cache... NSDictionary *credentialsDict = [[NSURLCredentialStorage sharedCredentialStorage] allCredentials]; if ([credentialsDict count] > 0) { // the credentialsDict has NSURLProtectionSpace objs as keys and dicts of userName => NSURLCredential NSEnumerator *protectionSpaceEnumerator = [credentialsDict keyEnumerator]; id urlProtectionSpace; // iterate over all NSURLProtectionSpaces while (urlProtectionSpace = [protectionSpaceEnumerator nextObject]) { NSEnumerator *userNameEnumerator = [[credentialsDict objectForKey:urlProtectionSpace] keyEnumerator]; id userName; // iterate over all usernames for this protectionspace, which are the keys for the actual NSURLCredentials while (userName = [userNameEnumerator nextObject]) { NSURLCredential *cred = [[credentialsDict objectForKey:urlProtectionSpace] objectForKey:userName]; NSLog(@"cred to be removed: %@", cred); [[NSURLCredentialStorage sharedCredentialStorage] removeCredential:cred forProtectionSpace:urlProtectionSpace]; } } }Of course, it didn't. I also tried setting the Authentication header directly on the request and setting up false default credentials in the hope that it would prompt another authentication challenge. All to no avail. So my last resort was this: I thought maybe adding a random different anchor to the end of the url on each request would prompt a new authentication challenge since it would always be a "different" URL. Turns out that I didn't even have to go that far: adding just a single "#" to the end of the URL did the trick! So instead of saying
[NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://localhost:3000/something.json"]]you say
[NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://localhost:3000/something.json#"]]Then it behaves exactly the way you want it to. With NSURLCredentialPersistenceNone it really calls the connection:didReceiveAuthenticationChallenge: on every request and with NSURLCredentialPersistenceForSession it stores it until you remove the credentials with the code posted above! So now HTTP Basic "logout" with NSURLConnection actually works! You can find the sample code here: http://gist.github.com/27421.
Comments
macsphere said...
Excellent work, man! Thanks for sharing!
Nick
Johannes Fahrenkrug said...
Yeah, I know it's crazy. It's definitely a bug and I've filed a bug report with Apple already. Glad it saved you from going berzerk :)
January 16, 2009 06:49 AMAnonymous said...
Wow, you just saved me so much time. Everything you tried, I had tried, scratching my head each time it didn't work. Somehow that little # makes my headaches go away. Crazy. Thanks for the assist!
January 15, 2009 09:20 PMJohannes Fahrenkrug said...
Hello "L",
I'm glad you enjoyed the article. Well, I'm sure there's an easy explanation for your problem: You are loading the data asynchronously, meaning your program doesn't block until NSURLConnection has finished. That's a Good Thing (tm). If you want your program to continue only when NSURLConnection is done, then wait for the connectionDidFinishLoading: delegate method to be called and carry on initializing your program.
- Johannes
Evil Doer said...
GREAT post. I really appreciated it. I too am trying to access a RESTful datasource. There in lies my problem.
I've put my NSURLConnection functionality into a custom routine that I've built that gets called at the start of the program. However, the method: didReceiveAuthenticationChallenge
and the other NSURLConnection methods get called AFTER my program has gone on to the rest of my program. I'm just a little stumped as to what I'm doing wrong. (sorry if this is WAY off topic.)
Thank you,
L.
Johannes Fahrenkrug said...
Hey Jerry,
that's great that you can confirm that this works. Yes, I'm filed a bug already, but it might not hurt for you to file another one and attach the demo project and possibly point to this article.
It must be a bug in nsurlconnection.
- Johannes
Jerry Krinock said...
Great work, Johannes. I have confirmed your result using the test project which I put together when I discovered this same problem myself about 6 months ago. Appending Fahrenkrug's # makes it work as expected.
In my previous work, I was not sure if the bug was in NSURLConnection or in the server I am contacting, del.icio.us. But I see that you've reproduced the problem on localhost. So I'd say it's definitely a bug in NSURLConnection.
Have you reported this bug to Apple? I have a demo project I should submit with the bug report.
Thanks,
Jerry Krinock
Trevor said...
It looks like your bug report did the trick! In iPhone 3.0 you can now use:
October 02, 2009 04:59 PM[NSURLCredential credentialWithUser:[usernameTextField text] password:[passwordTextField text] persistence:NSURLCredentialPersistenceNone] forAuthenticationChallenge:challenge]
Trevor