Overview
I'm working on a SAML login (single sign-on, similar to openID) solution for an iOS app that involves showing a view controller with a UIWebView
and I'm running into a timing and/or timeout issue when handling HTTP basic/digest auth in the UIWebView
.
Specifically, when the client gets an HTTP auth challenge, I pop an UIAlertView
prompting the user for a userID & password. If the user is able to enter the info quickly (< 10 seconds), it works. However, if the entry takes more than 10 seconds, the connection appears to have been terminated and nothing happens.
Questions
- Is there a timeout on calls to
connection:didReceiveAuthenticationChallenge:
that would prevent me from prompting the user for a userID & password (and having to wait for user input)? Does anyone have a workaround (e.g. some way to extend the connection timeout)? - Is there a better way to handle HTTP basic/digest auth from a
UIWebView
than a subclass ofNSURLProtocol
?
Details & Code
For most of the SAML systems we need to handle, the login will appear as a regular web page in the UIWebView
. However, some of the systems we need to handle fall back to using HTTP basic or HTTP digest authentication for mobile browsers, so we need to be able to handle that as well.
The big challenges start with the fact that UIWebView does not expose the network calls underneath. To get at what I need, I've created a subclass of NSURLProtocol
and registered it, as necessary:
[NSURLProtocol registerClass:[SMURLProtocol class]];
With that, this method on SMURLProtocol
gets called when an HTTP basic/auth challenge is issued, so I return YES we can handle HTTP basic & digest authentication:
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
return ([protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPDigest]
|| [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]);
}
Now I've told the networking stack that SMURLProtocol can handle the auth challenge, so it calls
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
NSString *authenticationMethod = [protectionSpace authenticationMethod];
if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPBasic]
|| [authenticationMethod isEqualToString:NSURLAuthenticationMethodHTTPDigest]) {
// Stash the challenge in an IVAR so we can use it later
_challenge = challenge;
// These network operations are often on a background thread, so we have to make sure to be on the foreground thread
// to interact with the UI. We tried the UIAlertView performSelectorOnMainThread, but ran into issues, so then
// we switched to GCD with a semaphore?
_dsema = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_main_queue(), ^{
// Prompt the user to enter the userID and password
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"AUTHENTICATION_REQUIRED", @"")
message:[protectionSpace host]
delegate:self
cancelButtonTitle:NSLocalizedString(@"CANCEL", @"")
otherButtonTitles:NSLocalizedString(@"LOG_IN", @""), nil];
[alert setAlertViewStyle:UIAlertViewStyleLoginAndPasswordInput];
[alert show];
});
dispatch_semaphore_wait(_dsema, DISPATCH_TIME_FOREVER);
// --> when you get here, the user has responded to the UIAlertView <--
dispatch_release(_dsema);
}
}
As you can see, I'm launching an UIAlertView to prompt the user for a userID and password. I have to do that back on the main thread because (apparently, I don't know for certain) the networking code is running on a background thread. I added the semaphore and explicit Grand Central Dispatch code to work around occasional crashes I was seeing (based upon this thread).
The final piece is the UIAlertView delegate that accepts the userID & password builds the credential for the challenge:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (([alertView alertViewStyle] == UIAlertViewStyleLoginAndPasswordInput) && (buttonIndex == 1)) {
NSString *userID = [[alertView textFieldAtIndex:0] text];
NSString *password = [[alertView textFieldAtIndex:1] text];
// when you get the reply that should unblock the background thread, unblock the other thread:
dispatch_semaphore_signal(_dsema);
// Use the userID and password entered by the user to proceed
// with the authentication challenge.
[_challenge.sender useCredential:[NSURLCredential credentialWithUser:userID
password:password
persistence:NSURLCredentialPersistenceNone]
forAuthenticationChallenge:_challenge];
[_challenge.sender continueWithoutCredentialForAuthenticationChallenge:_challenge];
_challenge = nil;
}
}
As I said in the overview, this all works great if the user is able to input the userID & password in less than about 10 seconds. If it takes longer, the connection appears to get timed out and passing the credentials on to the challenge's sender has no effect.
SFSafariViewController
(introduced in iOS 9). However, most apps that perform OAuth/SAML (including ours) are still using Safari because it is the most reliable and robust way to handle the odd authentication configurations found in many enterprises (e.g. self signed certs, digest auth, etc.). – Loni