8

I'm trying to extend functionality of SocketRocket library. I want to add authentication feature.

Since this library is using CFNetwork CFHTTPMessage* API for HTTP functionality (needed to start web socket connection) I'm trying to utilize this API to provide authentication.
There is perfectly matching function for that: CFHTTPMessageAddAuthentication, but it doesn't work as I'm expecting (as I understand documentation).

Here is sample of code showing the problem:

- (CFHTTPMessageRef)createAuthenticationHandShakeRequest: (CFHTTPMessageRef)chalengeMessage {
    CFHTTPMessageRef request = [self createHandshakeRequest];
    BOOL result = CFHTTPMessageAddAuthentication(request,
                                                 chalengeMessage,
                                                 (__bridge CFStringRef)self.credentials.user,
                                                 (__bridge CFStringRef)self.credentials.password,
                                                 kCFHTTPAuthenticationSchemeDigest, /* I've also tried NULL for use strongest supplied authentication */
                                                 NO);
    if (!result) {
        NSString *chalengeDescription = [[NSString alloc] initWithData: CFBridgingRelease(CFHTTPMessageCopySerializedMessage(chalengeMessage))
                                                              encoding: NSUTF8StringEncoding];
        NSString  *requestDescription = [[NSString alloc] initWithData: CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request))
                                                              encoding: NSUTF8StringEncoding];
        SRFastLog(@"Failed to add authentication data `%@` to a request:\n%@After a chalenge:\n%@",
                  self.credentials, requestDescription, chalengeDescription);
    }
    return request;
}

requestDescription content is:

GET /digest-auth/auth/user/passwd HTTP/1.1
Host: httpbin.org
Sec-WebSocket-Version: 13
Upgrade: websocket
Sec-WebSocket-Key: 3P5YiQDt+g/wgxHe71Af5Q==
Connection: Upgrade
Origin: http://httpbin.org/

chalengeDescription contains:

HTTP/1.1 401 UNAUTHORIZED
Server: nginx
Content-Type: text/html; charset=utf-8
Set-Cookie: fake=fake_value
Access-Control-Allow-Origin: http://httpbin.org/
Access-Control-Allow-Credentials: true
Date: Mon, 29 Jun 2015 12:21:33 GMT
Proxy-Support: Session-Based-Authentication
Www-Authenticate: Digest nonce="0c7479b412e665b8685bea67580cf391", opaque="4ac236a2cec0fc3b07ef4d628a4aa679", realm="me@kennethreitz.com", qop=auth
Content-Length: 0
Connection: keep-alive

user and password values are valid ("user" "passwd").

Why CFHTTPMessageAddAuthentication returns NO? There is no clue what is the problem. I've also try updated with credentials an empty request but without luck.

I've used http://httpbin.org/ just for testing (functionality of web socket is irrelevant at this step).

Please not that used code doesn't use (and never will) NSURLRequst or NSURLSession or NSURLConnection/


I've tried to use different functions: CFHTTPAuthenticationCreateFromResponse and CFHTTPMessageApplyCredentials with same result. At least CFHTTPMessageApplyCredentials returns some error information in form of CFStreamError. Problem is that this error information is useless: error.domain = 4, error.error = -1000 where those values are not documented anywhere.
The only documented values looks like this:
typedef CF_ENUM(CFIndex, CFStreamErrorDomain) {
    kCFStreamErrorDomainCustom = -1L,      /* custom to the kind of stream in question */
    kCFStreamErrorDomainPOSIX = 1,        /* POSIX errno; interpret using <sys/errno.h> */
    kCFStreamErrorDomainMacOSStatus      /* OSStatus type from Carbon APIs; interpret using <MacTypes.h> */
};

CFHTTPAuthenticationCreateFromResponse returns invalid object, which description returns this:

<CFHTTPAuthentication 0x108810450>{state = Failed; scheme = <undecided>, forProxy = false}

I've found in documentation what those values means: domain=kCFStreamErrorDomainHTTP, error=kCFStreamErrorHTTPAuthenticationTypeUnsupported (thanks @JensAlfke I've found it before your comment). Why it is unsupported? Documentation claims that digest is supported there is a constant kCFHTTPAuthenticationSchemeDigest which is accepted and expected by CFHTTPMessageAddAuthentication!


I've dig up source code of CFNetwork authentication and trying figure out what is the problem.

I have to do some mistake since this simple tast application also fails:

#import <Foundation/Foundation.h>
#import <CFNetwork/CFNetwork.h>

static NSString * const kHTTPAuthHeaderName = @"WWW-Authenticate";

static NSString * const kHTTPDigestChallengeExample1 = @"Digest realm=\"testrealm@host.com\", "
    "qop=\"auth,auth-int\", "
    "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
    "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";

static NSString * const kHTTPDigestChallengeExample2 = @"Digest nonce=\"b6921981b6437a4f138ba7d631bcda37\", "
    "opaque=\"3de7d2bd5708ac88904acbacbbebc4a2\", "
    "realm=\"me@kennethreitz.com\", "
    "qop=auth";

static NSString * const kHTTPBasicChallengeExample1 = @"Basic realm=\"Fake Realm\"";

#define RETURN_STRING_IF_CONSTANT(a, x) if ((a) == (x)) return @ #x

NSString *NSStringFromCFErrorDomain(CFIndex domain) {
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainHTTP);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainFTP);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainSSL);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainSystemConfiguration);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainSOCKS);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainPOSIX);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainMacOSStatus);

    return [NSString stringWithFormat: @"UnknownDomain=%ld", domain];
}

NSString *NSStringFromCFErrorError(SInt32 error) {
    RETURN_STRING_IF_CONSTANT(error, kCFStreamErrorHTTPAuthenticationTypeUnsupported);
    RETURN_STRING_IF_CONSTANT(error, kCFStreamErrorHTTPAuthenticationBadUserName);
    RETURN_STRING_IF_CONSTANT(error, kCFStreamErrorHTTPAuthenticationBadPassword);

    return [NSString stringWithFormat: @"UnknownError=%d", (int)error];
}

NSString *NSStringFromCFHTTPMessage(CFHTTPMessageRef message) {
    return [[NSString alloc] initWithData: CFBridgingRelease(CFHTTPMessageCopySerializedMessage(message))
                                 encoding: NSUTF8StringEncoding];
}

void testAuthenticationHeader(NSString *authenticatiohHeader) {
    CFHTTPMessageRef response = CFHTTPMessageCreateResponse(kCFAllocatorDefault,
                                                            401,
                                                            NULL,
                                                            kCFHTTPVersion1_1);
    CFAutorelease(response);

    CFHTTPMessageSetHeaderFieldValue(response,
                                     (__bridge CFStringRef)kHTTPAuthHeaderName,
                                     (__bridge CFStringRef)authenticatiohHeader);


    CFHTTPAuthenticationRef authData = CFHTTPAuthenticationCreateFromResponse(kCFAllocatorDefault, response);
    CFAutorelease(authData);

    CFStreamError error;
    BOOL validAuthData = CFHTTPAuthenticationIsValid(authData, &error);

    NSLog(@"testing header value: %@\n%@authData are %@   error.domain=%@  error.error=%@\n\n",
          authenticatiohHeader, NSStringFromCFHTTPMessage(response),
          validAuthData?@"Valid":@"INVALID",
          NSStringFromCFErrorDomain(error.domain), NSStringFromCFErrorError(error.error));
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        testAuthenticationHeader(kHTTPDigestChallengeExample1);
        testAuthenticationHeader(kHTTPDigestChallengeExample2);
        testAuthenticationHeader(kHTTPBasicChallengeExample1);
    }
    return 0;
}

Logs show:

2015-07-01 16:33:57.659 cfauthtest[24742:600143] testing header value: Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"

authData are INVALID   error.domain=kCFStreamErrorDomainHTTP  error.error=kCFStreamErrorHTTPAuthenticationTypeUnsupported

2015-07-01 16:33:57.660 cfauthtest[24742:600143] testing header value: Digest nonce="b6921981b6437a4f138ba7d631bcda37", opaque="3de7d2bd5708ac88904acbacbbebc4a2", realm="me@kennethreitz.com", qop=auth
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest nonce="b6921981b6437a4f138ba7d631bcda37", opaque="3de7d2bd5708ac88904acbacbbebc4a2", realm="me@kennethreitz.com", qop=auth

authData are INVALID   error.domain=kCFStreamErrorDomainHTTP  error.error=kCFStreamErrorHTTPAuthenticationTypeUnsupported

2015-07-01 16:33:57.660 cfauthtest[24742:600143] testing header value: Basic realm="Fake Realm"
HTTP/1.1 401 Unauthorized
Www-Authenticate: Basic realm="Fake Realm"

authData are INVALID   error.domain=kCFStreamErrorDomainHTTP  error.error=kCFStreamErrorHTTPAuthenticationTypeUnsupported


edit after my own answer:

Alternative solution

Other possible solution is to manually parse WWW-Authenticate response header and precess it and generate Authorization header for new request.

Is there some simple library or sample code I could use in commercial application which will do this (only this)? I could do this my self but this will take a precious time. Bounty is still available :).

Marek R
  • 32,568
  • 6
  • 55
  • 140
  • Please note that this is low level API `CFHTTPMessage` which operates on `CFStream` and you are referring to higher level API `NSURLConnection` or `NSURLSession`. For some strange reason `CFHTTPMessageAddAuthentication` has refused to add authentication data to my request and there is no information why. – Marek R Jun 29 '15 at 13:17
  • I see what you mean about the CFStream. Have you tried passing in the Auth object within the `NSURLRequest` (actually an `NSMutableURLRequest` object) with `- (id)initWithURLRequest:(NSURLRequest *)request;`? Just not sure that when it runs through `_urlRequest.allHTTPHeaderFields` it will correctly add the Auth object using `CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);` – sbarow Jun 29 '15 at 13:38
  • @sbarow: aparently you don't understand the problem. `NSURLRequest` here is not available since HTTP is used only as a handshake to start web socket connection so lower level API is used for HTTP. – Marek R Jun 29 '15 at 14:07
  • You're using Socket Rocket right? I don't know the in's and out's of your implementation but if you look in the header file (`SRWebSocket.h`) at line `64` you will see `- (id)initWithURLRequest:(NSURLRequest *)request;` so `NSURLRequest` is in fact available. Like I say I don't know your implementation, so thats all the advise I can give. Good luck. – sbarow Jun 29 '15 at 18:50
  • see [the source code of initWithURLRequest](https://github.com/square/SocketRocket/blob/master/SocketRocket/SRWebSocket.m#L286). `NSURLRequest` is used only as temporary storage for headers and URL and nothing else. For HTTP protocol CFNetwork API is used only. – Marek R Jun 29 '15 at 22:03
  • I looked up error -1000 on osstatus.com — it's kCFStreamErrorHTTPAuthenticationTypeUnsupported (as defined in CFHTTPAuthentication.h.) Which implies digest auth isn't supported; that's strange. Also, domain 4 is kCFStreamErrorDomainHTTP. – Jens Alfke Jul 01 '15 at 04:50
  • Should the origin start as https to do the upgrade to wss? – uchuugaka Jul 01 '15 at 05:37
  • @uchuugaka in this stage (HTTP handshake) web socket functionality is unimportant. It is digest authentication over HTTP. – Marek R Jul 01 '15 at 07:32
  • @JensAlfke thanks! I've found this values in documentation yesterday. I agree that this is strange since documentation clearly states that digest is supported. I've also added a [link to source code of `CFHTTPAuthentication.c`](http://www.opensource.apple.com/source/CFNetwork/CFNetwork-128/HTTP/CFHTTPAuthentication.c) – Marek R Jul 01 '15 at 07:35

3 Answers3

4

Answering own question :(

Apple CFNetwork API sucks

Problem is that response in CFHTTPMessageRef have hidden property URL. You can read it: CFHTTPMessageCopyRequestURL not set it and it is needed to properly create authentication object from CFHTTPMessageRef. If URL property is empty authentication will fail.

So how come that is some cases response with authentication challenge contains URL in other cases not? This working response comes from CFReadStreamRef created by CFReadStreamCreateForHTTPRequest as property of this stream. Here is crappy example. So since SocketRocket doesn't use CFReadStreamCreateForHTTPRequest this is a big problem which can't be simply overcome.

What is sad that CFHTTPMessageAddAuthentication could fetch this URL from request it modifies if it can't be found in response.

Workaround

There is perfectly working workaround on this issue! But it involves use of private API (so most probably it will not pass Apple review). Here is full sample code with workaround (same as in question but applying this workaround), the workaround it self it just two lines: exposing private API and using it.

#import <Foundation/Foundation.h>
#import <CFNetwork/CFNetwork.h>

static NSString * const kHTTPAuthHeaderName = @"WWW-Authenticate";

static NSString * const kHTTPDigestChallengeExample1 = @"Digest realm=\"testrealm@host.com\", "
    "qop=\"auth,auth-int\", "
    "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
    "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";

static NSString * const kHTTPDigestChallengeExample2 = @"Digest nonce=\"b6921981b6437a4f138ba7d631bcda37\", "
    "opaque=\"3de7d2bd5708ac88904acbacbbebc4a2\", "
    "realm=\"me@kennethreitz.com\", "
    "qop=auth";

static NSString * const kHTTPBasicChallengeExample1 = @"Basic realm=\"Fake Realm\"";

#define RETURN_STRING_IF_CONSTANT(a, x) if ((a) == (x)) return @ #x

NSString *NSStringFromCFErrorDomain(CFIndex domain) {
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainHTTP);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainFTP);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainSSL);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainSystemConfiguration);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainSOCKS);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainPOSIX);
    RETURN_STRING_IF_CONSTANT(domain, kCFStreamErrorDomainMacOSStatus);

    return [NSString stringWithFormat: @"UnknownDomain=%ld", domain];
}

NSString *NSStringFromCFErrorError(SInt32 error) {
    RETURN_STRING_IF_CONSTANT(error, kCFStreamErrorHTTPAuthenticationTypeUnsupported);
    RETURN_STRING_IF_CONSTANT(error, kCFStreamErrorHTTPAuthenticationBadUserName);
    RETURN_STRING_IF_CONSTANT(error, kCFStreamErrorHTTPAuthenticationBadPassword);

    return [NSString stringWithFormat: @"UnknownError=%d", (int)error];
}

NSString *NSStringFromCFHTTPMessage(CFHTTPMessageRef message) {
    return [[NSString alloc] initWithData: CFBridgingRelease(CFHTTPMessageCopySerializedMessage(message))
                                 encoding: NSUTF8StringEncoding];
}

// exposing private API for workaround
extern void _CFHTTPMessageSetResponseURL(CFHTTPMessageRef, CFURLRef);

void testAuthenticationHeader(NSString *authenticatiohHeader) {
    CFHTTPMessageRef response = CFHTTPMessageCreateResponse(kCFAllocatorDefault,
                                                            401,
                                                            NULL,
                                                            kCFHTTPVersion1_1);
    CFAutorelease(response);

    // workaround: use of private API
    _CFHTTPMessageSetResponseURL(response, (__bridge CFURLRef)[NSURL URLWithString: @"http://some.test.url.com/"]);

    CFHTTPMessageSetHeaderFieldValue(response,
                                     (__bridge CFStringRef)kHTTPAuthHeaderName,
                                     (__bridge CFStringRef)authenticatiohHeader);


    CFHTTPAuthenticationRef authData = CFHTTPAuthenticationCreateFromResponse(kCFAllocatorDefault, response);
    CFAutorelease(authData);

    CFStreamError error;
    BOOL validAuthData = CFHTTPAuthenticationIsValid(authData, &error);

    NSLog(@"testing header value: %@\n%@authData are %@   error.domain=%@  error.error=%@\n\n",
          authenticatiohHeader, NSStringFromCFHTTPMessage(response),
          validAuthData?@"Valid":@"INVALID",
          NSStringFromCFErrorDomain(error.domain), NSStringFromCFErrorError(error.error));
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        testAuthenticationHeader(kHTTPDigestChallengeExample1);
        testAuthenticationHeader(kHTTPDigestChallengeExample2);
        testAuthenticationHeader(kHTTPBasicChallengeExample1);
    }
    return 0;
}

And result in logs looks like that:

2015-07-03 11:47:02.849 cfauthtest[42766:934054] testing header value: Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"

authData are Valid   error.domain=UnknownDomain=0  error.error=UnknownError=0

2015-07-03 11:47:02.852 cfauthtest[42766:934054] testing header value: Digest nonce="b6921981b6437a4f138ba7d631bcda37", opaque="3de7d2bd5708ac88904acbacbbebc4a2", realm="me@kennethreitz.com", qop=auth
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest nonce="b6921981b6437a4f138ba7d631bcda37", opaque="3de7d2bd5708ac88904acbacbbebc4a2", realm="me@kennethreitz.com", qop=auth

authData are Valid   error.domain=UnknownDomain=0  error.error=UnknownError=0

2015-07-03 11:47:02.852 cfauthtest[42766:934054] testing header value: Basic realm="Fake Realm"
HTTP/1.1 401 Unauthorized
Www-Authenticate: Basic realm="Fake Realm"

authData are Valid   error.domain=UnknownDomain=0  error.error=UnknownError=0

So workaround works.

I will keep looking for other workaround which will use public API only. At least now I know what is the problem.

Marek R
  • 32,568
  • 6
  • 55
  • 140
0

If you're getting kCFStreamErrorHTTPAuthenticationTypeUnsupported

Does kCFHTTPAuthenticationSchemeBasic work?

Just a thought?

edit another thought, I've seen this when using the wrong protocol & port i.e.

http://myauth.com/auth/.../foobar (on port 443 despite being http)

and

https://myauth.com/auth/.../foobar (on port 80 despite being https)

Adrian Sluyters
  • 2,186
  • 1
  • 16
  • 21
  • I've tested this also for Basic authentication with same result. I've to do something wrong and error reporting doesn't show what exactly is a problem. Checkout last update of question, there is full code of sample application showing that something is wrong. – Marek R Jul 01 '15 at 17:15
  • This is not port problem for sure. I used this URL in other test (where `NSURLSession`, `POCO library` and other libraries on other platforms) and it was working. Also proble is not network communication but parsing the authentication data (my code proves that if you read logs). – Marek R Jul 02 '15 at 06:41
  • Have you confirmed that with tcpdump or another packet analyser like packetpeeper form packetpeeper.org ? Knowing if its on the net side or on the auth data side would cut down a lot of places to look :) – Adrian Sluyters Jul 02 '15 at 10:57
  • This is not a network problem. I'm receiving proper authentication challenge and by debugging I've narrow down problem to issue with adding credentials when building new request. Please read question more carefully. – Marek R Jul 02 '15 at 11:24
0

I wrote some CFHTTPAuthentication code several months ago, and vaguely recall similar weirdness. I think the calls only worked correctly in combination with CFStream.

Meaning, kCFStreamPropertyHTTPResponseHeader was somehow different from a CFHTTPMessage created via CFHTTPMessageCreateEmpty or CFHTTPMessageCreateResponse.

I'm not 100% on that though & don't have time to test right now.

  • socket rocket uses `CFStreamCreatePairWithSocketToHost` and `CFHTTPMessageCreateEmpty` so this might be a some clue. I will investigate this. – Marek R Jul 03 '15 at 07:56
  • Form other source (tech lead from company I cooperate with) I have explanation why it doesn't work. He claims that there is not strait workaround, but your clue give me idea how I could do a workaround without implementing digest authentication my self. This might solve this problem but it could break other stuff. I will post an answer when I will have clear view of problem and solution. – Marek R Jul 03 '15 at 08:26