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 of NSURLProtocol?
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.
Related
I am using Amazon Cognito User Pools. I am trying to authenticate a user. First he/she will have to enter the phone number and password, there'll be a SMS sent to authenticate the user, upon Authenticating the user is expected to Sign in by giving the phonenumber and password.
1.) I want to popup the User registration Screen if the user is not registered with the app
2.) If the app has gone to the background I want the user to proceed using the app without having to login again. (At the moment the user requires to sign in all the time when they go to the background)
3.) If the user has registered but not authenticated the SMS validation then I want to redirect the user to the confirmation page
I have been stuck in this for nearly a week now. Can someone help me out.
In the app Delegate I have the following code. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
..
AWSServiceConfiguration *serviceConfiguration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUSEast1 credentialsProvider:nil];
//create a pool
AWSCognitoIdentityUserPoolConfiguration *configuration = [[AWSCognitoIdentityUserPoolConfiguration alloc] initWithClientId:#"XXX" clientSecret:#"XXX" poolId:#"us-east-1_XXX"];
[AWSCognitoIdentityUserPool registerCognitoIdentityUserPoolWithConfiguration:serviceConfiguration userPoolConfiguration:configuration forKey:#"UserPool"];
//AWSCognitoIdentityUserPool *pool = [AWSCognitoIdentityUserPool CognitoIdentityUserPoolForKey:#"UserPool"];
[AWSLogger defaultLogger].logLevel = AWSLogLevelVerbose;
AWSCognitoIdentityUserPool *pool =[AWSCognitoIdentityUserPool CognitoIdentityUserPoolForKey:#"UserPool"];
pool.delegate = self;
}
//set up password authentication ui to retrieve username and password from the user
-(id<AWSCognitoIdentityPasswordAuthentication>) startPasswordAuthentication {
//
if(!self.navController){
self.navController = [[UIForViewController getStoryboard] instantiateViewControllerWithIdentifier:#"signupSegueID"];
}
// if(!self.signInViewController){
// self.signInViewController = self.navigationController.viewControllers[0];
// }
dispatch_async(dispatch_get_main_queue(), ^{
//rewind to login screen
//display login screen if it isn't already visibile
if(!(self.navController.isViewLoaded && self.navController.view.window))
{
[self.window.rootViewController presentViewController:self.navController animated:YES completion:nil];
}
});
return nil;
}
Please note that startPasswordAuthentication is never executed unless I add the following code in the APPDELEGATES
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
[[self.user getDetails] continueWithSuccessBlock:^id _Nullable(AWSTask<AWSCognitoIdentityUserGetDetailsResponse *> * _Nonnull task) {
if (task.error) {
//
NSLog(#"Error ");
[[[UIAlertView alloc] initWithTitle:task.error.userInfo[#"__type"]
message:task.error.userInfo[#"message"]
delegate:self
cancelButtonTitle:#"Ok"
otherButtonTitles:nil] show];
return nil;
}
AWSCognitoIdentityUserGetDetailsResponse *response = task.result;
for (AWSCognitoIdentityUserAttributeType *attribute in response.userAttributes) {
//print the user attributes
NSLog(#"Attribute: %# Value: %#", attribute.name, attribute.value);
}
return nil;
}];
1) Cognito doesn't currently expose an API to check if a username exists already. You could work around this by calling a username specific API and acting based on the exception thrown back. If you're thinking more locally, you can check the session based on the username to see if someone is already signed in.
2) The RefreshTokens API is used to get a new access token once the old one has expired. Use the refresh token you get back in authenticating to facilitate this.
3) Being registered doesn't give you access. On user registration, you get no token, but are required to log in afterwards. This is already handled.
I got the following method in a singleton/shared instance and would like update the user with the progress of fetching emails.
- (void)getAllImapEmailsForMailbox:(NSString *)mailbox completionBlock:(void (^)(BOOL success, NSString *errorString, NSArray *emails))block
First I'm unlocking the API with a key, then I'm connecting to their IMAP server, then I'm logging in with their details, then I select a particular mailbox, then I loop through whole mailbox to download the messages.
So I would like to know how would I get these updates from a singleton method to my view controller displaying these messages in a UIAlertView for example.
eg. Connecting.. Logging in.. Selecting mailbox.. Downloading mail 1 of 69..
I'm currently only doing 1 message saying Downloading Emails, but it takes too long and don't want the user to think the app is hanging and not doing anything. This is what I'm doing:
UIAlertView *loadingView = [[UIAlertView alloc] initWithTitle:#"Downloading Emails..." message:nil delegate:nil cancelButtonTitle:nil otherButtonTitles:nil];
[loadingView show];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self getAllEmailsForMailbox:#"Inbox"];
dispatch_async(dispatch_get_main_queue(), ^{
[loadingView dismissWithClickedButtonIndex:-1 animated:YES];
});
});
Thanks!
Add another block param called something like statusBlock. Give the block a string param that will contain a status message. How you get the status to send this block depends on the details of how you do the listed steps, but at an outline level...
- (void)getAllImapEmailsForMailbox:(NSString *)mailbox
statusBlock:(void (^)(NSString *)statusBlock
completionBlock:(void (^)(BOOL success, NSString *errorString, NSArray *emails))block {
statusBlock(#"connecting");
// do connecting stuff
NSInteger numberOfMessagesToFetch = // find this out however you do now
statusBlock([NSString stringWithFormat:#"fetching %d messages", numberOfMessagesToFetch]);
// fetch mail, and so on
On the caller side:
[mailSingleton getAllImapEmailsForMailbox:#"mailbox"
statusBlock:^(NSString *message) { // update UI with message }
completionBlock: ... { // update UI now that you're complete }];
Also , looking at your edit, is it possible to hide the asynch stuff in this method? Would be a lot friendlier for the caller, who could just pass the blocks and assume the asynch.
SHORT STORY
Using gtm-oauth2 for iOS and FOSOAuthServerBundle in Symfony2 to implement my own Oauth2 server I am not getting the callBack finishedSelector to be invoked.
This is where the "special" ViewController is created:
GTMOAuth2ViewControllerTouch * viewController;
viewController = [[GTMOAuth2ViewControllerTouch alloc] initWithAuthentication:myAuth
authorizationURL:authURL
keychainItemName:nil
delegate:self
finishedSelector:#selector(viewController:finishedWithAuth:error:)];
What are the reasons that might make finishedSelector, (the implemented method viewController:finishedWithAuth:error) not to be invoked?
The behavior I get is that the login page is properly rendered, but it acts as the starting point of the whole web application, rendering the rest of the pages once it is logged-in instead of returning the control to the finishedSelector and, finally, to the view controller that has to manage the continuation of the APP workflow.
LONG STORY
Using gtm-oauth2 and FOSOAuthServerBundle in Symfony2, I am experiencing problems trying to make the arquitecture to catch the login and load the authenticated session from my iOS APP.
I am following the instructions described in the gtm-oauth2 documentation, particularly the Signing in to non-Google Services part.
Doing what it is described there, I have this method for creating the auth object:
- (GTMOAuth2Authentication * ) authForMyAPP
{
//This URL is defined by the individual 3rd party APIs, be sure to read their documentation
NSString * url_string = #"http://myHost/oauth/v2/token";
NSURL * tokenURL = [NSURL URLWithString:url_string];
// We'll make up an arbitrary redirectURI. The controller will watch for
// the server to redirect the web view to this URI, but this URI will not be
// loaded, so it need not be for any actual web page. This needs to match the URI set as the
// redirect URI when configuring the app.
NSString * redirectURI = #"http://myHost/oauth/v2/falseCallBack";
GTMOAuth2Authentication * myAuth;
myAuth = [GTMOAuth2Authentication authenticationWithServiceProvider:#"MyAPP"
tokenURL:tokenURL
redirectURI:redirectURI
clientID:kMyClientID
clientSecret:kMyClientSecret
];
//[myAuth setTokenType:#"Bearer"];
return myAuth;
}
And then, this method creates the "special" viewController that should handle the render of the login page and returning the control when the login is performed:
- (void)signInToMyAPP()
{
GTMOAuth2Authentication *myAuth = [self authForMyAPP];
NSString* auth_string = #"http://127.0.0.1/~pgbonino/Symfony/web/app.php/oauth/v2/auth";
NSURL * authURL = [NSURL URLWithString:auth_string];
// Display the authentication view
// Creates the "special" viewController passing the `auth` object, the authorization URL and the finishedSelector
GTMOAuth2ViewControllerTouch * viewController;
viewController = [[GTMOAuth2ViewControllerTouch alloc] initWithAuthentication:myAuth
authorizationURL:authURL
keychainItemName:nil
delegate:self
finishedSelector:#selector(viewController:finishedWithAuth:error:)];
[self.navigationController pushViewController:viewController animated:YES];
}
Finally, I have the method used for that finishedSelector. It should be called once the login is properly performed and the authentication has succeeded (or an error has come). THAT IS WHAT I AM NOT GET DONE:
- (void)viewController:(GTMOAuth2ViewControllerTouch *)viewController
finishedWithAuth:(GTMOAuth2Authentication *)myAuth
error:(NSError *)error
{
if (error != nil)
{
// Authentication failed
UIAlertView *alertView = [ [UIAlertView alloc] initWithTitle:#"Authorization Failed"
message:[error localizedDescription]
delegate:self
cancelButtonTitle:#"Dismiss"
otherButtonTitles:nil];
[alertView show];
}
else
{
// Authentication succeeded
// Assign the access token to the instance property for later use
//self.accessToken = myAuth.accessToken;
[myAuth setShouldAuthorizeAllRequests:YES];
[[Singleton sharedSingleton] setAuth:myAuth];
// Display the access token to the user
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Authorization Succeeded"
message:[NSString stringWithFormat:#"Access Token: %#", myAuth.accessToken]
delegate:self
cancelButtonTitle:#"Dismiss"
otherButtonTitles:nil];
[alertView show];
}
}
This all is supposed to render my login page in a web view and catch the successful login to call the viewController:finishedWithAuth:error and save the session in some shared object.
Nevertheless, the behavior I am getting is that I get rendered the login in the web view, I correctly login and, instead oF the delegated selector gets invoked, it just normally logs in the application and the next page is loaded in the web view, as if it was in a normal browser. So the callback is not performed.
Why am I not getting the selector to be called? Any idea?
IMPORTANT NOTE: the Oauth2 server works perfectly: if I call the token URL and the callBack url from Safari, everything works well. Tokens and auths codes are correctly saved in database.
Forget it.
It was just me.
OAuth2 won't work with Symfony2 and FOSUserBundle while this parameter is set to true in config.yml:
always_use_default_target_path: false
I am developing an app for SharePoint online and wanted to use the SharePoint Rest interfaces in my ios app. Can Some one please tell me the steps to use SharePoint Rest interfaces in iOS
I got it, below are the steps to be followed:
Include RestKit in your ios app.
Create a UIView in your home screen and load the login page.
load http: //server name/Pages/default.aspx in the UIWebView
In webViewDidFinished method find out the Fed Auth token and append it with the request URL
- (void)webViewDidFinishLoad:(UIWebView *)webView {
//Retrive HTTPOnly Cookie
NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
NSArray *cookiesArray = [storage cookies];
//Search for Fed Auth cookie
for (NSHTTPCookie *cookie in cookiesArray) {
if ([[cookie name] isEqualToString:#"FedAuth"]) {
/*** DO WHATEVER YOU WANT WITH THE COOKIE ***/
NSLog(#"Found FedAuth");
NSURL *url=[NSURL URLWithString:#"http://my server/_vti_bin/listdata.svc"];
RKClient *client = [RKClient clientWithBaseURL:url];
client.requestQueue.requestTimeout = 10;
client.cachePolicy = RKRequestCachePolicyNone;
client.authenticationType = RKRequestAuthenticationTypeHTTPBasic;
client.username = #"username";
client.password = #"Password";
NSString *cookieVale=[cookie value];
NSString *getResourcePath=[#"?" stringByAppendingFormat:#"%#",cookieVale];
[client get:getResourcePath delegate:self];
break;
}
}
}
And here you can find the response.
- (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response {
id xmlParser = [[RKParserRegistry sharedRegistry] parserForMIMEType:RKMIMETypeXML];
NSError *error = nil;
id parsedResponse = [xmlParser objectFromString:[response bodyAsString] error:&error];
RKLogInfo(#"Parsed response : %#, error:%#",parsedResponse,error);
if ([response isSuccessful]) {
NSLog(#"%d",[response isCreated]);
// Response status was 200..299
if ([response isCreated] && [response isJSON]) {
// Looks like we have a 201 response of type application/json
RKLogInfo(#"The JSON is %#", [response bodyAsJSON]);
}
} else if ([response isError]) {
// Response status was either 400..499 or 500..599
RKLogInfo(#"Ouch! We have an HTTP error. Status Code description: %#", [response localizedStatusCodeString]);
}
}
The self accepted answer lost me lots of hours of trials and errors. It omits some key aspects like the fact that you also need to grab the rtFa cookie. And what's up with client.username = #"username" and client.password = #"Password" provided in the users code. What is that? Note that the client does not know the username or password at any moment...
AAAnyway, below is a great article which will guide you in the right direction:
http://www.codeproject.com/Articles/571996/Development-of-iPhone-client-application-for-Share
And this describes how to get the cookies without using a UIWebView
http://allthatjs.com/2012/03/28/remote-authentication-in-sharepoint-online/
Send the FedAuth cookie with all your subsequent Requests.
Once authenticated, you can call the REST API, documentation here:
http://msdn.microsoft.com/en-us/library/fp142385(v=office.15).aspx#bk_determining
When the user finish the sign in process towards a Office365 Sharepoint instance, the web view will be redirected in several steps. As one of the final steps before loading the actual Sharepoint web site, the web view will be asked to load "about:blank".
Detect when you web view starts loading "about:blank" and you know when the user finished the sign in process and can close the web view. Example code below.
// Load page in web view
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSLog(#"WebView should start loading %#", request.URL.absoluteString);
// Detect that the user finished the sign in process
if ([request.URL.absoluteString isEqualToString:#"about:blank"]) {
// Do your stuff here
return NO;
}
return YES;
}
The Sharepoint instance will also set the FedAuth cookie if the authentication was successful. The cookie must be included in future requests to the server.
You do not have to append the cookie manually, this will be taken care of by the URL loading system as long as the cookies has been accepted and stored in the NSHTTPCookieStorage and you are sending the request to the same server.
From Apple documentation
The URL loading system automatically sends any stored cookies
appropriate for an NSURLRequest. unless the request specifies not to
send cookies. Likewise, cookies returned in an NSURLResponse are
accepted in accordance with the current cookie acceptance policy.
I am using NSURLAuthenticationChallenge to log in to a webserver through my app. All the server requires is a username and a password. Here is what's happening:
(1) Ping server with POST message containing a User-Agent string in the HTML header
(2) Server responds with an authentication challenge which is detected by the didReceiveAuthenticationChallenge delegate method
(3) Respond by sending a challenge response using username and password:
NSURLCredential *cred = [[NSURLCredential alloc] initWithUser:unameTextField.text
password:pswdTextField.text
persistence:NSURLCredentialPersistenceForSession];
[[challenge sender] useCredential:cred forAuthenticationChallenge:challenge];
(4) If username/password are correct, delegate method connectionDidFinishLoading gets called, detecting that the challenge response was accepted by the server. User is now logged in and can send/receive messages from the server. (If username/password are incorrect, delegate method didFailWithError gets called and user is shown an alert.)
Here's where it's going wrong: the very first time I open my Xcode project and run the app and attempt to login with the correct username/password, there is a time lag of 10-15 seconds between steps 3 and 4. And then even after connectionDidFinishLoading is called, when I send messages to the server requesting files it responds by sending me the HTML login page which is the default behavior for unauthenticated requests...so it seems as though I'm not logged in after all.
If I stop and then run the app again there is no lag and everything works fine.
EDIT: I solved the above problem by clearing the URLCache, all cookies and all credentials before each login attempt. Code for these 3 methods is below:
- (void)clearCookiesForURL
{
NSURL *loginUrl = [NSURL URLWithString:#"https://www.MYURL.com"];
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
NSArray *cookies = [cookieStorage cookiesForURL:loginUrl];
for (NSHTTPCookie *cookie in cookies)
{
[cookieStorage deleteCookie:cookie];
}
}
- (void)eraseCredentials
{
NSString *urlString = #"www.MYURL.com";
NSURLCredentialStorage *credentialsStorage = [NSURLCredentialStorage sharedCredentialStorage];
NSDictionary *allCredentials = [credentialsStorage allCredentials];
if ([allCredentials count] > 0)
{
for (NSURLProtectionSpace *protectionSpace in allCredentials)
{
if ([[protectionSpace host] isEqualToString:urlString])
{
NSDictionary *credentials = [credentialsStorage credentialsForProtectionSpace:protectionSpace];
for (NSString *credentialKey in credentials)
{
[credentialsStorage removeCredential:[credentials objectForKey:credentialKey] forProtectionSpace:protectionSpace];
}
}
}
}
}
- (void)eraseURLCache
{
NSURL *loginUrl = [NSURL URLWithString:#"https://www.MYURL.com"];
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:loginUrl];
[[NSURLCache sharedURLCache] removeCachedResponseForRequest:urlRequest];
[[NSURLCache sharedURLCache] setMemoryCapacity:0];
[[NSURLCache sharedURLCache] setDiskCapacity:0];
}
Another problem: if I wait for a long time between sending message requests to the server while the app is running, the server thinks I've logged out and exhibits the same behavior described above.
EDIT: this 2nd problem remains unsolved. Additional information - it appears that the magic time lag number is 10 seconds. In other words, if I wait more than 10 seconds after the server has authenticated me to request a file from the server, it doesn't recognize my request and sends me the web login page instead, just as it would do for an unauthenticated request.
Any idea what's going on? And no, I can't simply load the webserver login page inside my app, because that doesn't meet the requirements for this project.