CFHTTPMessageAddAuthentication fails to add authentication data to request
Asked Answered
C

3

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="[email protected]", 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=\"[email protected]\", "
    "qop=\"auth,auth-int\", "
    "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
    "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";

static NSString * const kHTTPDigestChallengeExample2 = @"Digest nonce=\"b6921981b6437a4f138ba7d631bcda37\", "
    "opaque=\"3de7d2bd5708ac88904acbacbbebc4a2\", "
    "realm=\"[email protected]\", "
    "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="[email protected]", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest realm="[email protected]", 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="[email protected]", qop=auth
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest nonce="b6921981b6437a4f138ba7d631bcda37", opaque="3de7d2bd5708ac88904acbacbbebc4a2", realm="[email protected]", 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 :).

Cheatham answered 29/6, 2015 at 12:39 Comment(9)
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.Cheatham
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);Sandra
@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.Cheatham
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.Sandra
see the source code of initWithURLRequest. NSURLRequest is used only as temporary storage for headers and URL and nothing else. For HTTP protocol CFNetwork API is used only.Cheatham
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.Immoral
Should the origin start as https to do the upgrade to wss?Tonina
@Tonina in this stage (HTTP handshake) web socket functionality is unimportant. It is digest authentication over HTTP.Cheatham
@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.cCheatham
C
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=\"[email protected]\", "
    "qop=\"auth,auth-int\", "
    "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
    "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";

static NSString * const kHTTPDigestChallengeExample2 = @"Digest nonce=\"b6921981b6437a4f138ba7d631bcda37\", "
    "opaque=\"3de7d2bd5708ac88904acbacbbebc4a2\", "
    "realm=\"[email protected]\", "
    "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="[email protected]", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest realm="[email protected]", 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="[email protected]", qop=auth
HTTP/1.1 401 Unauthorized
Www-Authenticate: Digest nonce="b6921981b6437a4f138ba7d631bcda37", opaque="3de7d2bd5708ac88904acbacbbebc4a2", realm="[email protected]", 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.

Cheatham answered 3/7, 2015 at 11:30 Comment(0)
L
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)

Lonilonier answered 1/7, 2015 at 15:37 Comment(4)
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.Cheatham
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).Cheatham
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 :)Lonilonier
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.Cheatham
M
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.

Mcmahan answered 3/7, 2015 at 2:31 Comment(2)
socket rocket uses CFStreamCreatePairWithSocketToHost and CFHTTPMessageCreateEmpty so this might be a some clue. I will investigate this.Cheatham
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.Cheatham

© 2022 - 2024 — McMap. All rights reserved.