iOS - problems with NSURLAuthenticationChallenge - connection successful but can't access server - ios

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.

Related

iOS/Cocoa - NSURLSession - Handling Basic HTTPS Authorization

[edited to provide more information]
(I'm not using AFNetworking for this project. I may do so in future, but wish to resolve this problem/misunderstanding first.)
SERVER SETUP
I cannot provide the real service here, but it is a simple, reliable service that returns XML according to a URL such as:
https://username:password#example.com/webservice
I want to connect to the URL over HTTPS using GET, and determine any authentication failures (http status code 401).
I have confirmed that the web service is available, and that I can successfully (http status code 200) grab XML from the url using a specified username and password. I have done this with a web browser, and with AFNetworking 2.0.3, and by using NSURLConnection.
I have also confirmed that I am using the correct credentials at all stages.
Given the correct credentials and the the following code:
// Note: NO delegate provided here.
self.sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfig
delegate:nil
delegateQueue:nil];
NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:self.requestURL completionHandler: ...
The above code will work. It will successfully connect to the server, get a http status code of 200, and return the (XML) data.
PROBLEM 1
This simple approach fails in cases where the credentials are invalid. In that case, the completion block is never called, no status code (401) is provided, and eventually, the Task times out.
ATTEMPTED SOLUTION
I assigned a delegate to the NSURLSession, and am handling the following callbacks:
-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
if (_sessionFailureCount == 0) {
NSURLCredential *cred = [NSURLCredential credentialWithUser:self.userName password:self.password persistence:NSURLCredentialPersistenceNone];
completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
_sessionFailureCount++;
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
if (_taskFailureCount == 0) {
NSURLCredential *cred = [NSURLCredential credentialWithUser:self.userName password:self.password persistence:NSURLCredentialPersistenceNone];
completionHandler(NSURLSessionAuthChallengeUseCredential, cred);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
_taskFailureCount++;
}
PROBLEM 1 WHEN USING ATTEMPTED SOLUTION
Please note the use of ivars _sessionFailureCount and _taskFailureCount. I am using these because the challenge object's #previousFailureCount property is never advanced! It always remains at zero, no matter how many times these callback methods are called.
PROBLEM 2 WHEN USING ATTEMPTED SOLUTION
Despite the use of correct credentials (as proven by their successful use with a nil delegate), authentication is failing.
The following callbacks occur:
URLSession:didReceiveChallenge:completionHandler:
(challenge # previousFailureCount reports as zero)
(_sessionFailureCount reports as zero)
(completion handler is called with correct credentials)
(there is no challenge #error provided)
(there is no challenge #failureResponse provided)
URLSession:didReceiveChallenge:completionHandler:
(challenge # previousFailureCount reports as **zero**!!)
(_sessionFailureCount reports as one)
(completion handler is called with request to cancel challenge)
(there is no challenge #error provided)
(there is no challenge #failureResponse provided)
// Finally, the Data Task's completion handler is then called on us.
(the http status code is reported as zero)
(the NSError is reported as NSURLErrorDomain Code=-999 "cancelled")
(The NSError also provides a NSErrorFailingURLKey, which shows me that the URL and credentials are correct.)
Any suggestions welcome!
You don't need to implement a delegate method for this, simply set the authorization HTTP header on the request, e.g.
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"https://whatever.com"]];
NSString *authStr = #"username:password";
NSData *authData = [authStr dataUsingEncoding:NSUTF8StringEncoding];
NSString *authValue = [NSString stringWithFormat: #"Basic %#",[authData base64EncodedStringWithOptions:0]];
[request setValue:authValue forHTTPHeaderField:#"Authorization"];
//create the task
NSURLSessionDataTask* task = [NSURLSession.sharedSession dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
}];
Prompted vs Unprompted HTTP Authentication
It seems to me that all documentation on NSURLSession and HTTP Authentication skips over the fact that the requirement for authentication can be prompted (as is the case when using an .htpassword file) or unprompted (as is the usual case when dealing with a REST service).
For the prompted case, the correct strategy is to implement the delegate method:
URLSession:task:didReceiveChallenge:completionHandler:; for the unprompted case, implementation of the delegate method will only provide you with the opportunity to verify the SSL challenge (e.g. the protection space). Therefore, when dealing with REST, you will likely need to add Authentication headers manually as #malhal pointed out.
Here is a more detailed solution that skips the creation of an NSURLRequest.
//
// REST and unprompted HTTP Basic Authentication
//
// 1 - define credentials as a string with format:
// "username:password"
//
NSString *username = #"USERID";
NSString *password = #"SECRET";
NSString *authString = [NSString stringWithFormat:#"%#:%#",
username,
secret];
// 2 - convert authString to an NSData instance
NSData *authData = [authString dataUsingEncoding:NSUTF8StringEncoding];
// 3 - build the header string with base64 encoded data
NSString *authHeader = [NSString stringWithFormat: #"Basic %#",
[authData base64EncodedStringWithOptions:0]];
// 4 - create an NSURLSessionConfiguration instance
NSURLSessionConfiguration *sessionConfig =
[NSURLSessionConfiguration defaultSessionConfiguration];
// 5 - add custom headers, including the Authorization header
[sessionConfig setHTTPAdditionalHeaders:#{
#"Accept": #"application/json",
#"Authorization": authHeader
}
];
// 6 - create an NSURLSession instance
NSURLSession *session =
[NSURLSession sessionWithConfiguration:sessionConfig delegate:self
delegateQueue:nil];
// 7 - create an NSURLSessionDataTask instance
NSString *urlString = #"https://API.DOMAIN.COM/v1/locations";
NSURL *url = [NSURL URLWithString:urlString];
NSURLSessionDataTask *task = [session dataTaskWithURL:url
completionHandler:
^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) {
if (error)
{
// do something with the error
return;
}
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode == 200)
{
// success: do something with returned data
} else {
// failure: do something else on failure
NSLog(#"httpResponse code: %#", [NSString stringWithFormat:#"%ld", (unsigned long)httpResponse.statusCode]);
NSLog(#"httpResponse head: %#", httpResponse.allHeaderFields);
return;
}
}];
// 8 - resume the task
[task resume];
Hopefully this will help anyone that runs into this poorly documented difference. I finally figured it out using test code, a local proxy ProxyApp and forcibly disabling NSAppTransportSecurity in my project's Info.plist file (necessary for inspecting SSL traffic via a proxy on iOS 9/OSX 10.11).
Short answer: The behavior you describe is consistent with a basic server authentication failure. I know you've reported that you've verified that it's correct, but I suspect some fundamental validation problem on the server (not your iOS code).
Long answer:
If you use NSURLSession without the delegate and include the userid/password in the URL, then completionHandler block of the NSURLSessionDataTask will be called if the userid/password combination is correct. But, if the authentication fails, NSURLSession appears to repeatedly attempt to make the request, using the same authentication credentials every time, and the completionHandler doesn't appear to get called. (I noticed that by watching the connection with Charles Proxy).
This doesn't strike me as very prudent of NSURLSession, but then again the delegate-less rendition can't really do much more than that. When using authentication, using the delegate-based approach seems more robust.
If you use the NSURLSession with the delegate specified (and no completionHandler parameter when you create the data task), you can examine the nature of the error in didReceiveChallenge, namely examine the challenge.error and the challenge.failureResponse objects. You might want to update your question with those results.
As an aside, you appear to be maintaining your own _failureCount counter, but you can probably avail yourself of challenge.previousFailureCount property, instead.
Perhaps you can share some particulars about the nature of the authentication your server is using. I only ask, because when I secure a directory on my web server, it does not call the NSURLSessionDelegate method:
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
But rather, it calls the NSURLSessionTaskDelegate method:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
Like I said, the behavior you describe is consist with an authentication failure on the server. Sharing the details about the nature of the authentication setting on your server and the particulars of the NSURLAuthenticationChallenge object might help us diagnose what's going on. You might also want to type the URL with the userid/password in a web browser and that might also confirm whether there is a basic authentication problem.

Has anybody found a way to load HTTPS pages with an invalid server certificate using UIWebView?

If a user attempts to load a https web page in Mobile Safari and the server's certificate validation check fails (its expired, revoked, self-signed etc.) then the user is presented is presented with a warning message and asked if they want to continue or not.
Similarly NSURLConnection offers the ability for the implementator to decide firstly how to check the certificate and then decide how to proceed if it fails, so in this situation too it would be possible to display a warning to the user and offer them the opportunity to continue loading the page or not.
However it seems when loading a https page in UIWebView that fails a certificate check the behaviour is just to fail to load the page - didFailLoadWithError: gets called with kCFURLErrorServerCertificateUntrusted however nothing gets displayed to the user.
This is inconsistent - surely the UIWebView behaviour should behave in a similar way to Safari to be consistent within iPhone itself?
Its also a daft that NSURLConnection allows total flexibility with this yet NSURLRequest:setAllowsAnyHTTPSCertificate is private.
Is there anyway to implement behaviour which is consistent with Safari, can this default behavior be customized in a similar way to NSURLConnection allows?
Cheers
P.S.
Please refrain from getting into patronizing side discussions about why would anybody want to do this, thank you very much.
I found out how to do this:
1) When the page is loaded it will fail, thus add something like the following to didFailLoadWithError:
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
if ([error.domain isEqualToString: NSURLErrorDomain])
{
if (error.code == kCFURLErrorServerCertificateHasBadDate ||
error.code == kCFURLErrorServerCertificateUntrusted ||
error.code == kCFURLErrorServerCertificateHasUnknownRoot ||
error.code == kCFURLErrorServerCertificateNotYetValid)
{
display dialog to user telling them what happened and if they want to proceed
2) If the user wants to load the page then you need to connect using an NSURLConnection:
NSURLRequest *requestObj = [NSURLRequest requestWithURL:self.currentURL cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10.0];
self.loadingUnvalidatedHTTPSPage = YES;
[self.webView loadRequest:requestObj];
3) Then make this change to shouldStartLoadWithRequest
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
if (self.loadingUnvalidatedHTTPSPage)
{
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[self.connection start];
return NO;
}
4) Implement the NSURLConnectionDelegate as:
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
SecTrustRef trust = challenge.protectionSpace.serverTrust;
NSURLCredential *cred;
cred = [NSURLCredential credentialForTrust:trust];
[challenge.sender useCredential:cred forAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
{
NSURLRequest *requestObj = [NSURLRequest requestWithURL:self.currentURL cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10.0];
self.loadingUnvalidatedHTTPSPage = NO;
[self.webView loadRequest: requestObj];
[self.connection cancel];
}
It all seems to work fine.
From the horse's mouth:
"UIWebView does not provide any way for an app to customize its HTTPS server trust evaluations. It is possible to work around this limitation using public APIs, but it is not easy. If you need to do this, please contact Developer Technical Support (dts#apple.com)
Source: http://developer.apple.com/library/ios/#technotes/tn2232/_index.html

Steps to use SharePoint Rest interfaces in iOS

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.

NSURLConnection's delegate method, asking for authenticationChallenge is not getting fired?

I use NSURLConnection to call a webservice and there is a client certificate present in my keychain which I set as the credential in - (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
After I removed this certificate and adding a new one to keychain, the NSURLConnection still maintains the credential that I give already and gives me back with 417 status error code, which was 200, before I remove the old certificate.
Is there a way to make the NSURLConnection ask for credential, forcefully.? or how can close the existing SSL connection or the authentication challenge's credentials.
NSURLConnection is fickle beast, and I've been having a similar problem for the past couple of days. But I've found a solution to my problem and a couple of suggestions to what could possibly be the reason for an issue like the one your having.
TLS
First of there is a possibility that what is happening to you is the TLS layer caching the credentials. That is due to it being computationally expensive to establish the TLS connection [1].
Possible solutions to this are to change the DNS name you are using, by for example adding a dot (.) to the end of the string since the DNS protocol accepts a string ending in dots as fully qualified DNS name. I've also seen people adding a hashtag (#) to all URL requests and thus tricking the system to never look for a stored credential but just initiate the didRecieveAuthenticationChallenge call instead [2].
Cookies
Another possibility is that the server is setting a cookie that you would need to clear. You can do that by doing the following:
-(void)clearCookies:(NSString *)urlString{
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedCookieStorage];
NSURL *url = [[NSURL alloc] initWithString urlString];
NSArray *cookies = [cookieStorage cookiesForURL:tempURL];
for(NSHTTPCookie *cookie in cookies){
//iterate over all cookies and delete them
[cookieStorage deleteCookie:cookie];
}
}
NSURLCredentialStorage
Now it could be that the credentials are still being stored in the sharedCredentialStorage and thus should be erased by doing the following:
NSURLCredentialStorage *store = [NSURLCredentialStorage sharedCredentialStorage];
if(store !=nil){
for(NSURLProtectionSpace *protectionSpace in [store allCredentials]){
NSDictionary *map = [[NSURLCredentialStorage sharedCredentialStorage]
credentialsForProtectionSpace:protectionSpace];
if(map!=nil){
for(NSString *user in map){
NSURLCredential *cred = [map objectForKey:user];
[store removeCredential:cred forProtectionSpace:protectionSpace];
}
}
}
}
I hope that these will help.

How to do authentication in UIWebView properly?

I would like to support HTTP Basic Authentication in my UIWebView.
At the moment, I am canceling requests in
webView:shouldStartLoadWithRequest:navigationType: then handle them in my own NSURLConnectionDelegate to check for and provide credentials if needed. I then use loadData:MIMEType:textEncodingName:baseURL: to present HTML in the web view. That works fine for any URLs that are passed to the delegate.
My problem is that the delegate is never called for embedded elements, like images, JavaScript or CSS files. So if I have an HTML page which references an image which is protected with basic authentication, that image cannot be loaded properly. Additionally, webView:didFinishLoad: is never called, because the web view could not fully load the page.
I have checked that case with Terra, a third-party browser available on the App Store, and it can fully cope with that situation. I think it would be possible to solve this by providing my own NSURLProtocol, but that seems too complicated. What am I missing?
Try to use sharedCredentialStorage for all domains you need to authenticate.
Here is working sample for UIWebView it was tested against Windows IIS having only BasicAuthentication enabled
This is how to add your site credentials:
NSString* login = #"MYDOMAIN\\myname";
NSURLCredential *credential = [NSURLCredential credentialWithUser:login
password:#"mypassword"
persistence:NSURLCredentialPersistenceForSession];
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc]
initWithHost:#"myhost"
port:80
protocol:#"http"
realm:#"myhost" // check your web site settigns or log messages of didReceiveAuthenticationChallenge
authenticationMethod:NSURLAuthenticationMethodDefault];
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential
forProtectionSpace:protectionSpace];
[protectionSpace release];
Edit: same code in Swift 4
let login = "MYDOMAIN\\myname"
let credential = URLCredential(user:login, password:"mypassword", persistence:.forSession)
let protectionSpace = URLProtectionSpace(host:"myhost", port:80, protocol:"http", realm:"myhost", authenticationMethod:NSURLAuthenticationMethodDefault)
URLCredentialStorage.shared.setDefaultCredential(credential, for:protectionSpace)
Your webView is supposed to work now, if it does not work use next code to debug, especially check log messages of didReceiveAuthenticationChallenge.
#import "TheSplitAppDelegate.h"
#import "RootViewController.h"
#implementation TheSplitAppDelegate
#synthesize window = _window;
#synthesize splitViewController = _splitViewController;
#synthesize rootViewController = _rootViewController;
#synthesize detailViewController = _detailViewController;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
// Add the split view controller's view to the window and display.
self.window.rootViewController = self.splitViewController;
[self.window makeKeyAndVisible];
NSLog(#"CONNECTION: Add credentials");
NSString* login = #"MYDOMAIN\\myname";
NSURLCredential *credential = [NSURLCredential credentialWithUser:login
password:#"mypassword"
persistence:NSURLCredentialPersistenceForSession];
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc]
initWithHost:#"myhost"
port:80
protocol:#"http"
realm:#"myhost" // check your web site settigns or log messages of didReceiveAuthenticationChallenge
authenticationMethod:NSURLAuthenticationMethodDefault];
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:protectionSpace];
[protectionSpace release];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"http://myhost/index.html"]
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:12
];
NSLog(#"CONNECTION: Run request");
[[NSURLConnection alloc] initWithRequest:request delegate:self];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
}
- (void)applicationWillTerminate:(UIApplication *)application
{
}
- (void)dealloc
{
[_window release];
[_splitViewController release];
[_rootViewController release];
[_detailViewController release];
[super dealloc];
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
NSLog(#"CONNECTION: got auth challange");
NSString* message = [NSString stringWithFormat:#"CONNECTION: cred cout = %i", [[[NSURLCredentialStorage sharedCredentialStorage] allCredentials] count]];
NSLog(message);
NSLog([connection description]);
NSLog([NSString stringWithFormat:#"CONNECTION: host = %#", [[challenge protectionSpace] host]]);
NSLog([NSString stringWithFormat:#"CONNECTION: port = %i", [[challenge protectionSpace] port]]);
NSLog([NSString stringWithFormat:#"CONNECTION: protocol = %#", [[challenge protectionSpace] protocol]]);
NSLog([NSString stringWithFormat:#"CONNECTION: realm = %#", [[challenge protectionSpace] realm]]);
NSLog([NSString stringWithFormat:#"CONNECTION: authenticationMethod = %#", [[challenge protectionSpace] authenticationMethod]]);
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
// release the connection, and the data object
[connection release];
// inform the user
NSLog(#"CONNECTION: failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
{
NSLog(#"CONNECTION: received response via nsurlconnection");
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
{
NSLog(#"CONNECTION: USE!");
return YES;
}
#end
The final solution for WebView authentication was based on custom protocol implementation. All protocols registered as a stack, so if you redefine HTTP protocol it would intercept all requests coming from webView, so you have to check attributes assotiated with incoming request and repack it into new request and send it again via your own connection. Since you are in stack, your request immidiatly comes to you again and you have to ignore it. So it goes down protocol stack to real HTTP protocol implementation, since your request is not athenticated you'll get authenticaiton request. And after authenticaiton you'll get a real response from server, so you repack response and reply to original request received from webView and that's it.
Don;t try to create new requests or responses bodies, you have to just resend them. The final code would be aproximetly 30-40 lines of code and it is quite simple, but requires a lot of debuging and tetsing.
Unfortunatlly I cannot provide code here, since I am assigned to different project already, I just wanted to say that my post is wrong way, it stucks when user changes password.
The secret to HTTP basic authentication using cocoa is knowing NSURL and the related classes.
NSURL
NSURLRequest/NSMutableURLRequest
NSURLConnection
NSURLCredential
NSURLCredentialStorage
NSURLProtectionSpace
UIWebView/WebView/NIWebController etc.
The real magic comes from NSURLConnection. In the words of the devDocs, "An NSURLConnection object provides support to perform the loading of a URL request." If you want to load some a URL in the background without displaying it you would use NSURLConnection. The real power of the NSURLConnection is in the method
+ (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id < NSURLConnectionDelegate >)delegate
The NSURLConnectionDelegate protocol has methods for responding to successful connections, fatal errors, and authentication challenges. If you are trying to access data Protected by HTTP basic authentication this is how Cocoa does it. At this point an example should bring some clarity.
//basic HTTP authentication
NSURL *url = [NSURL URLWithString: urlString];
NSMutableURLRequest *request;
request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:12];
[self.webView openRequest:request];
(void)[NSURLConnection connectionWithRequest:request delegate:self];
This creates a URL. From the URL a URLRequest is created. The URLRequest is then loaded in the web view. The Request is also used to make a URLConnection. We don't really use the connection, but we need to receive notifications about authentication so we set the delegate. There are only two methods we need from the delegate.
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
NSURLCredential * cred = [NSURLCredential credentialWithUser:#"username"
password:#"password"
persistence:NSURLCredentialPersistenceForSession];
[[NSURLCredentialStorage sharedCredentialStorage]setCredential:cred forProtectionSpace:[challenge protectionSpace]];
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
{
return YES;
}
Whenever there is an authentication challenge a credential is added to the credential storage. You also tell the connection to use the credential storage.
I've just implemented this by setting basic auth credentials using an NSMutableURLRequest for the UIWebView. This also avoids the round trip incurred when implementing sharedCredentialStorage (of course there are tradeoffs involved).
Solution:
NSString *url = #"http://www.my-url-which-requires-basic-auth.io"
NSString *authStr = [NSString stringWithFormat:#"%#:%#", username, password];
NSData *authData = [authStr dataUsingEncoding:NSASCIIStringEncoding];
NSString *authValue = [NSString stringWithFormat:#"Basic %#", [authData base64EncodedString]];
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
[mutableRequest setValue:authValue forHTTPHeaderField:#"Authorization"];
NSURLRequest *request = [mutableRequest copy];
NSURLRequest *request = [NSURLRequest basicAuthHTTPURLRequestForUrl:url];
[self.webView loadRequest:request];
You can grab the NSData+Base64 category which implements the base64EncodedString for NSData from Matt Gallagher's page (it was at the bottom of the blog post when I downloaded it)
For TKAURLProtocolPro [http://kadao.dir.bg/cocoa.htm]
For SVWebViewController [https://github.com/samvermette/SVWebViewController]
Make sure to remember that logging out is not so easy with sessions and UIWebView credentials. See answer here: https://stackoverflow.com/a/18143902/2116338.

Resources