Ios: Authentication Using Xmlhttprequest - Handling 401 Response

iOS: Authentication using XMLHttpRequest - Handling 401 response

A few things became apparent to me while trying to do this on iOS. One is that iOS has a bug relating to basic auth, so if your password has certain special characters in it you'll never get a response back from your server because your server will never get an authentication challenge. That is, if you're using the username and password field in the "open" method.

My guess is they are doing something stupid like sending it via http://username:password@myorigin.com/etc when they should be using http headers and base64 encoding the creds like so

req.setRequestHeader("Authorization", "Basic " + base64(username) + ':' + base64(password));

The other thing I learned is that Basic Auth isnt very secure and is prone to a million and one problems. One of which that will annoy you is that the client will cache the username and password, which will override any new values you send via "req.open(...)". Good luck getting around that using javascript alone, you'll have to do some magic in ObjC to clear the cache.

If you have control over your server, I would suggest using token authentication. Connect over SSL and then send a POST with JSON data containing the username and password. The server could then send back JSON data with an authentication token (essentially a bunch of random characters long enough that it can't ever be guessed, a UUID works well. this is generated by the server and can only be known to the client and the server). Then store the token and the username in the keychain so the user doesnt need to enter their creds everytime they start your app.

My server will always send back a 200 response but the JSON data will contain the information needed to either retry or to store the auth token. In general... basic auth basically sucks.

try {
var req = new XMLHttpRequest();
req.onload = function(ev) {
var response = JSON.parse(this.responseText);
if (response.success === true) {
// The server will respond with a token that will allow us to login
storeCredentials(userValue, response.token);
// redirect with token
else if (req.status == 401) {
alert("Invalid Username/Password");
document.getElementById('password').focus();
} else {
alert("Some other status");
}
}
req.ontimeout = setTimeout(function(ev) { navigator.notification.alert('Timeout trying to contact the server'); }, 10000);
req.onerror = function(ev) { clearTimeout(this.ontimeout); navigator.notification.alert('Error connecting to the server during authentication.'); };

var uri = myWebOrigin + '/authenticate';
req.open('POST', uri, true);
req.setRequestHeader('Cache-Control', 'no-cache');
req.setRequestHeader('Content-Type', 'application/json');
json_data = {username : Base64.encode(userValue), password : Base64.encode(passValue)};
req.send(JSON.stringify(json_data));
} catch(error) {
navigator.notification.alert('Uh oh, an error occurred trying to login! ' + error);
return;
}

401 prompt on 3G, but not on Wi-Fi - XmlHttpRequest over SSL with Basic Authentication, hard-coded credentials

I'm still not entirely sure why this was happening, but I was able to fix it with the method posted here.

It just uses Base64.js to create a hash from the username and password, and passes that in an authorization header.

xmlhttp.setRequestHeader('Authorization', auth);

I removed the username and password from the .open():

xmlhttp.open('POST', 'https://foo.bar.com:5443/WebServices/Web.svc', true);

The server doesn't send any 401 challenges at all. It results directly in a 200 status, which means the data loads even faster.

XMLHttpRequest fails basic authentication

I found a workaround that worked for me on Android.

Don’t know why but direct authenticated request:

    req.open(method, fullurl, true, user, pass);
req.send(data);

didn’t work for me – it always returned 401. So Instead I tried to set basic authentication via header:

    req.open(method, fullurl, true);
req.setRequestHeader("Authorization", "Basic " + Base64.encode(user + ":" + pass));
req.send(data);

(where Base64 is taken from here: https://stackoverflow.com/a/246813/961695) – and it worked! Perhaps there’s a bug in XmlHttpRequest implementation on android.

How to display the Authentication Challenge in UIWebView?

It's actually super easy... I'm sure you can just show a UIAlertView when the auth challenge delegate is shown (or prior to loading the URL, if you know for sure that the URL you're hitting will prompt for auth login info). Anyways, the trick is to create your own NSURLConnection and I do some logic to save whether the auth delegate has been used.

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
{
NSLog(@"Did start loading: %@ auth:%d", [[request URL] absoluteString], _authed);

if (!_authed) {
_authed = NO;
/* pretty sure i'm leaking here, leave me alone... i just happen to leak sometimes */
[[NSURLConnection alloc] initWithRequest:request delegate:self];
return NO;
}

return YES;
}

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
NSLog(@"got auth challange");

if ([challenge previousFailureCount] == 0) {
_authed = YES;
/* SET YOUR credentials, i'm just hard coding them in, tweak as necessary */
[[challenge sender] useCredential:[NSURLCredential credentialWithUser:@"username" password:@"password" persistence:NSURLCredentialPersistencePermanent] forAuthenticationChallenge:challenge];
} else {
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
{
NSLog(@"received response via nsurlconnection");

/** THIS IS WHERE YOU SET MAKE THE NEW REQUEST TO UIWebView, which will use the new saved auth info **/

NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:]];

[_webView loadRequest:urlRequest];
}

- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
{
return NO;
}

Is there ANY way to suppress the browser's login prompt on 401 response when using XmlHttpRequest

I think I worked around it. Ofcourse relying on internal MS AJAX related errors is not really nice, but this will do the trick for others faces with this problem.

Basically what you do, is you set the X-MicrosoftAjax header to the value Delta=true (case is important here), the ScriptModule will interpret this as a normal redirect, and turn the response into a 200, but will set the pageRedirect data string for any ScriptManager (MS-AJAX PageRequestManager) on the page to consume. The jQuery.ajax function will still see this as an error, so basically you can check for pageRedirect in responseText property of the XHR and hadle it accordingly. Like sending the user to the login page of something.

    $.ajax({
type: "POST",
url: postUrl + "/SomePageMethod",
data: "{searchString:\"" + escape(searchString) + "\"}",
contentType: "application/json; charset=utf-8",
beforeSend: function(xhr) {
xhr.setRequestHeader("X-MicrosoftAjax","Delta=true");
},
dataType: "json",
success: onSuccess,
error: onError
});

function onError(xhr, e, textStatus) {
var isAjaxRedirect = xhr.status == 200 && xhr.responseText.match(/pageRedirect/);
if (isAjaxRedirect == "pageRedirect") {
// forms authentication ticket expired
location.href = "a session timeout page, or a login page or whatever";
}
}

handle 401 unauthorized error on windows Phone with Phonegap

I've encountered the same problem and the only solution I founded is to implement a Phonegap plugin.

Here is the C# code I use :

namespace WPCordovaClassLib.Cordova.Commands
{
[DataContract]
public class PhonegapWindowsPhonePostObject
{
[DataMember(IsRequired = false, Name = "yourParamName1")]
public string yourParam1;

[DataMember(IsRequired = false, Name = "yourParamName2")]
public string yourParam2;
}

public class PhonegapWindowsPhonePost: BaseCommand
{
public void post(string options)
{
PhonegapWindowsPhonePostObject pwppo = JSON.JsonHelper.Deserialize<PhonegapWindowsPhonePostObject>(options);

try
{
System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() =>
{
var data = "YOUR DATA STRING HERE"; //use the pwppo object to retrieve your parameters
var url = "URL OF THE SERVICE HERE";

WebClient wc = new SharpGIS.GZipWebClient();
var URI = new Uri(url);
wc.Encoding = Encoding.UTF8;
wc.Headers[HttpRequestHeader.ContentType] = "application/x-www-form-urlencoded";
wc.UploadStringCompleted += new UploadStringCompletedEventHandler(wc__UploadStringCompleted);
wc.UploadStringAsync(URI, "POST", data);
});

}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
void wc__UploadStringCompleted(object sender, UploadStringCompletedEventArgs e)
{
try
{
if (e.Result != null)
this.DispatchCommandResult(new PluginResult(PluginResult.Status.OK, e.Result));
else
this.DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Error 401"));
}
catch (Exception ex)
{
this.DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, ex.Message));
// no http status code available
}
}
}
}

And here is the Javascript code to call the pluging from your app :

function loginToWebService(yourParam1, yourParam2) {

var options = { "yourParamName1": yourParam1, "yourParamName2": yourParam2};
cordova.exec(success, error,"PhonegapWindowsPhonePost","post", options);
}

I hope it will help you.

Note : the Phonegap 2.8 version does not solve the problem contrary to what said the release note...

Bye !

Override XMLHTTPRequest Calls

Cordova/phonegap redefines XMLHttpRequest object out of the box for some of the platforms providing additional functionality, for example for Windows Phone it does it in the following way

https://github.com/apache/incubator-cordova-js/blob/master/lib/wp7/plugin/wp7/XHRPatch.js

Probably you would like to do something in the same manner or extend existing implementation for your platform.



Related Topics



Leave a reply



Submit