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
/
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.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
!
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
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 :).
Answering own question :(
CFNetwork
API sucksProblem 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With