18

I have a cocoa class set up that I want to use to connect to a RESTful web service I'm building. I have decided to use HTTP Basic Authentication on my PHP backend like so…

<?php
if (!isset($_SERVER['PHP_AUTH_USER'])) {
    header('WWW-Authenticate: Basic realm="My Realm"');
    header('HTTP/1.0 401 Unauthorized');
    //Stuff that users will see if they click 'Cancel'
    exit;
}
else {
    //Validation Code
    echo "You entered info.";
}
?>

At this point I'm using a synchronous NSURLConnection, which I understand the Apple documentation states has less support for Authentication.

But is it even possible at all? I can do cookie authentication very easily sans NSURLProtectionSpaces or NSURLCredentials or any of the authentication classes. Also, are there any resources where I can read more about the Cocoa Authentication classes?

Thanks.

UPDATE: mikeabdullahuk The code you supplied (the second example) is almost identical to what I had written. I have done some more investigating, and discovered that the NSURLConnection is returning an error…

Error Domain=NSURLErrorDomain Code=-1012 UserInfo=0x1a5170 "Operation could not be completed. (NSURLErrorDomain error -1012.)"

The code corresponds to NSURLErrorUserCancelledAuthentication. So apparently my code is not accessing the NSURLCredentialStorage and instead is canceling the authentication. Could this have anything to do with the PHP HTTP Authentication functions? I'm quite confused at this point.

mazniak
  • 5,849
  • 6
  • 28
  • 23

4 Answers4

62

A synchronous NSURLConnection will absolutely work with NSURLCredentialStorage. Here's how things usually work:

  1. NSURLConnection requests the page from the server
  2. The server replies with a 401 response
  3. NSURLConnection looks to see what credentials it can glean from the URL
  4. If the URL did not provide full credentials (username and password), NSURLConnection will also consult NSURLCredentialStorage to fill in the gaps
  5. If full credentials have still not been determined, NSURLConnection will send the -connection:didReceiveAuthenticationChallenge: delegate method asking for credentials
  6. If the NSURLConnection now finally has full credentials, it retries the original request including authorization data.

By using the synchronous connection method, you only lose out on step 5, the ability to provide custom authentication. So, you can either pre-provide authentication credentials in the URL, or place them in NSURLCredentialStorage before sending the request. e.g.

NSURLRequest *request =
  [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://user:pass@example.com"]];

[NSURLConnection sendSynchronousRequest:request returningResponse:NULL error:NULL];

or:

NSURLCredential *credential = [NSURLCredential credentialWithUser:@"user"
                                                         password:@"pass"
                                                      persistence:NSURLCredentialPersistenceForSession];

NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc]
  initWithHost:@"example.com"
  port:0
  protocol:@"http"
  realm:nil
  authenticationMethod:nil];


[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential
                                                    forProtectionSpace:protectionSpace];
[protectionSpace release];

NSURLRequest *request =
  [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://example.com"]];

[NSURLConnection sendSynchronousRequest:request returningResponse:NULL error:NULL];
Mike Abdullah
  • 14,933
  • 2
  • 50
  • 75
  • 1
    I just came across this. I'm _very_ new to ObjectiveC but I was wondering if any objects (specifically credential and protectionSpace) had to be released. Btw I implemented this method on an asynchronous call (initWithRequest) and still got the didReceiveAuthenticationChallenge callback. – Bill Feb 03 '09 at 22:17
  • @Bill, protectionSpace does need releasing if used in production code as I used +alloc. – Mike Abdullah Feb 10 '09 at 12:24
  • protectionSpace has to be released. alloc will return an object with retainCount == 1 so after [... setDefaultCredential:...] you should release it like [protectionSpace release]; – sliver Nov 09 '10 at 15:50
  • NSURLCredentialStorage didn't work for me, but simply prefixing the server in the http string with user:pass did work, thanks so much, super simple solution. – n13 May 23 '12 at 03:34
  • I've been doing objective-c and a lot of web development for a while now and I never knew you could at the username and password in the url like that +1. You learn something new every day. – Popeye Aug 08 '12 at 08:46
  • 2
    As per the apple documentation for sendSynchronousRequest: 'If authentication is required in order to download the request, the required credentials must be specified as part of the URL...' My own experience would confirm this; that is, that using NSURLCRedentialStorage doesn't work with sendSynchronousRequest. – Christopher King Jan 06 '13 at 19:58
12

In a situation where a 401 or other authentication challenge is unacceptable/impossible, I sometimes use a dummy CFHTTPMessage to generate the authetication line, then copy that back into the NSURLRequest:

// assume NSString *username and *password exist and NSURLRequest *urlRequest
// exists and is fully configured except for HTTP Basic Authentication.. 

CFHTTPMessageRef dummyRequest =
    CFHTTPMessageCreateRequest(
        kCFAllocatorDefault,
        CFSTR("GET"),
        (CFURLRef)[urlRequest URL],
        kCFHTTPVersion1_1);
CFHTTPMessageAddAuthentication(
    dummyRequest,
    nil,
    (CFStringRef)username,
    (CFStringRef)password,
    kCFHTTPAuthenticationSchemeBasic,
    FALSE);
authorizationString =
    (NSString *)CFHTTPMessageCopyHeaderFieldValue(
        dummyRequest,
        CFSTR("Authorization"));
CFRelease(dummyRequest);

[urlRequest setValue:authorizationString forHTTPHeaderField:@"Authorization"];

This may seem completely a bizarre way to do it but it is tolerant of situations where the username/password aren't URL clean and where NSURLRequest refuses to consult the NSURLCredentialStorage because the server isn't actually sending a HTTP 401 (for example it sends a regular page instead).

Matt Gallagher
  • 14,858
  • 2
  • 41
  • 43
  • thanks for this! it's especially helpful in situations where the delegate pattern isn't desirable. – kevboh Dec 22 '10 at 21:04
0

Set your credential as the default credential for the protectionspace:

// Permananent, session, whatever.
NSURLCredential *credential = [NSURLCredential credentialWithUser:username password:password persistence: NSURLCredentialPersistencePermanent];
// Make sure that if the server you are accessing presents a realm, you set it here.
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:@"blah.com" port:0 protocol:@"http" realm:nil authenticationMethod:NSURLAuthenticationMethodHTTPBasic];
// Store it
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:protectionSpace];

At this point, any subsequent NSURLConnection that is challenged using a protection space that matches what you set will use this credential

quellish
  • 21,123
  • 4
  • 76
  • 83
0

I would note mikeabdullahuk's answer is good but also if you use NSURLCredentialPersistencePermanent instead of per session it will store the credentials in the users keychain so next time you can check NSURLCredentialStorage for a non nil value for the default credentials for a protection space and if you get a non nil value you can just pass the credentials in. I am using this method right now for a delicious.com client I am writing and it works very well in my tests.

Colin Wheeler
  • 3,343
  • 2
  • 21
  • 23