15

I have a web site that is trying to call an MVC controller action on another web site. These sites are both setup as relying party trusts in AD FS 2.0. Everything authenticates and works fine when opening pages in the browser window between the two sites. However, when trying to call a controller action from JavaScript using the jQuery AJAX method it always fails. Here is a code snippet of what I'm trying to do...

$.ajax({
  url: "relyingPartySite/Controller/Action",
  data: { foobar },
  dataType: "json",
  type: "POST",
  async: false,
  cache: false,
  success: function (data) {
    // do something here
  },
  error: function (data, status) {
    alert(status);
  }
});

The issue is that AD FS uses JavaScript to post a hidden html form to the relying party. When tracing with Fiddler I can see it get to the AD FS site and return this html form which should post and redirect to the controller action authenticated. The problem is this form is coming back as the result of the ajax request and obviously going to fail with a parser error since the ajax request expects json from the controller action. It seems like this would be a common scenario, so what is the proper way to communicate with AD FS from AJAX and handle this redirection?

hexacyanide
  • 88,222
  • 31
  • 159
  • 162
SaaS Developer
  • 9,835
  • 7
  • 34
  • 45
  • 1
    if HTML is being returned by the ajax call, clearly you do not want to parse it with the json parser. change dataType to "html", and post an example of the html returned, so I can show you how to write a handler that will submit the returned form. – ironchefpython Feb 10 '12 at 04:56
  • The issue is I want to get back JSON. AD FS redirects with a new HTML form it wants to post to perform the handshake it needs. This works fine within a browser window but not here. Once the handshake happens there is no redirect with the AJAX request and I get back JSON. I have come up with a workaround for now to handle the html page post in an IFRAME but it's not ideal. – SaaS Developer Feb 16 '12 at 21:34
  • I understand that you want to get back JSON, but you're not going to get back JSON. _However_, if you want to be able to treat the returned data structure as if it _were_ JSON, post an example of the HTML returned, and I'll show you how to write a handler that will submit the returned form **without** using an IFRAME. – ironchefpython Feb 17 '12 at 04:11
  • Why in my case did it give me the error 'No access control Allow-Access-Allow-Origin'? It's a CORS problem which means my browser prevented the redirect to my ADFS server, why your request can be redirected to the ADFS server? – machinarium Jul 17 '14 at 14:30

7 Answers7

4

You have two options. More info here.

The first is to share a session cookie between an entry application (one that is HTML based) and your API solutions. You configure both applications to use the same WIF cookie. This only works if both applications are on the same root domain. See the above post or this stackoverflow question.

The other option is to disable the passiveRedirect for AJAX requests (as Gutek's answer). This will return a http status code of 401 which you can handle in Javascript. When you detect the 401, you load a dummy page (or a "Authenticating" dialog which could double as a login dialog if credentials need to be given again) in an iFrame. When the iFrame has completed you then attempt the call again. This time the session cookie will be present on the call and it should succeed.

//Requires Jquery 1.9+
var webAPIHtmlPage = "http://webapi.somedomain/preauth.html"

function authenticate() {
    return $.Deferred(function (d) {
        //Potentially could make this into a little popup layer 
        //that shows we are authenticating, and allows for re-authentication if needed
        var iFrame = $("<iframe></iframe>");
        iFrame.hide();
        iFrame.appendTo("body");
        iFrame.attr('src', webAPIHtmlPage);
        iFrame.load(function () {
            iFrame.remove();
            d.resolve();
        });
    });
};

function makeCall() {
    return $.getJSON(uri)
                .then(function(data) {
                        return $.Deferred(function(d) { d.resolve(data); });
                    },
                    function(error) {
                        if (error.status == 401) {
                            //Authenticating, 
                            //TODO:should add a check to prevnet infinite loop
                            return authenticate().then(function() {
                                //Making the call again
                                return makeCall();

                            });
                        } else {
                            return $.Deferred(function(d) {
                                d.reject(error);
                            });
                        }
                });
}
yivi
  • 42,438
  • 18
  • 116
  • 138
Adam Mills
  • 7,719
  • 3
  • 31
  • 47
  • The iFrame solution only worked for me by not removing the iframe, but by getting its contents iFrame.load(function () { var content = iFrame.contents(); resolve(); }); – GitteTitter Oct 16 '15 at 08:56
  • The second option is working for me if I do a GET using Ajax. But if I do a POST then the browser is not sending the cookie loaded by the iFrame along with the request. Any idea why? – NLV Dec 30 '15 at 15:01
2

If you do not want to receive HTML with the link you can handle AuthorizationFailed on WSFederationAuthenticationModule and set RedirectToIdentityProvider to false on Ajax calls only.

for example:

FederatedAuthentication.WSFederationAuthenticationModule.AuthorizationFailed += (sender, e) =>
{
    if (Context.Request.RequestContext.HttpContext.Request.IsAjaxRequest())
    {
        e.RedirectToIdentityProvider = false;
    }
};

This with Authorize attribute will return you status code 401 and if you want to have something different, then you can implement own Authorize attribute and write special code on Ajax Request.

Gutek
  • 701
  • 7
  • 24
  • Handle the 403 in JS and load an iframe with a html page inside the secure area, all the crappy adfs redirects will occur and you'll get your cookies... you can then retry the call – Adam Mills Nov 08 '13 at 15:41
  • This should (in theory) work fine in the `Application_Start()` method of `Global.asax.cs` -- but won't. It does, however, work just fine in `Application_BeginRequest()`. – Jeremy McGee Jan 16 '14 at 09:03
  • 1
    check the MSDN article about recommended way to set AuthorizationFailed event in ASP.NET applications https://msdn.microsoft.com/en-us/library/system.identitymodel.services.wsfederationauthenticationmodule.authorizationfailed(v=vs.110).aspx – Kaveh Hadjari Jan 28 '16 at 16:15
2

In the project which I currently work with, we had the same issue with SAML token expiration on the clientside and causing issues with ajax calls. In our particular case we needed all requests to be enqueud after the first 401 is encountered and after successful authentication all of them could be resent. The authentication uses the iframe solution suggested by Adam Mills, but also goes a little further in case user credentials need to be entered, which is done by displaying a dialog informing the user to login on an external view (since ADFS does not allow displaying login page in an iframe atleast not default configuration) during which waiting request are waiting to be finished but the user needs to login on from an external page. The waiting requests can also be rejected if user chooses to Cancel and in those cases jquery error will be called for each request.

Here's a link to a gist with the example code:

https://gist.github.com/kavhad/bb0d8e4a446496a6c05a

Note my code is based on usage of jquery for handling all ajax request. If your ajax request are being handled by vanilla javascript, other libraries or frameworks then you can perhaps find some inspiration in this example. The usage of jquery ui is only because of the dialog and stands for a small portion of the code which could easly be swapped out.

Update Sorry I changed my github account name and that's why link did not work. It should work now.

Kaveh Hadjari
  • 217
  • 1
  • 10
  • can you share your code please again, the link of GitHub does not work, and can you please give more detail, I have the same case as you, am using ADFS, and SAML Token, I enabled Cross-Domain for my sites. When I do ajax request "get " to another site, it show me the iframe, for me I should not do the iframe, i have to get the token and used, any solution to do the get without authentication and iframe, Note : the user is already logged in – Imen Jan 23 '20 at 15:27
  • @kaveh-hadjari can you please share the code link again. – Jay Sep 22 '20 at 05:09
0

First of all you say you are trying to make an ajax call to another website, does your call conforms to same origin policy of web browsers? If it does then you are expecting html as a response from your server, changedatatype of the ajax call to dataType: "html", then insert the form into your DOM.

Juzer Ali
  • 4,109
  • 3
  • 35
  • 62
0

Perhaps the 2 first posts of this serie will help you. They consider ADFS and AJAX requests

What I think I would try to do is to see why the authentication cookies are not transmitted through ajax, and find a mean to send them with my request. Or wrap the ajax call in a function that pre authenticate by retrieving the html form, appending it hidden to the DOM, submitting it (it will hopefully set the good cookies) then send the appropriate request you wanted to send originally

Grooveek
  • 10,046
  • 1
  • 27
  • 37
  • The cookies do get transmitted wants the handshake happens. This problem is the handshake is trying to happen on the AJAX request if it's the first time. – SaaS Developer Feb 16 '12 at 21:36
0

You can do only this type of datatype

"xml": Treat the response as an XML document that can be processed via jQuery. 

"html": Treat the response as HTML (plain text); included script tags are evaluated. 

"script": Evaluates the response as JavaScript and evaluates it. 

"json": Evaluates the response as JSON and sends a JavaScript Object to the success callback. 

If you can see in your fiddler that is returning only html then change your data type to html or if that only a script code then you can use script.

manny
  • 1,878
  • 2
  • 15
  • 31
-1

You should create a file anyname like json.php and then put the connection to the relayparty website this should works $.ajax({ url: "json.php", data: { foobar }, dataType: "json", type: "POST", async: false, cache: false, success: function (data) { // do something here }, error: function (data, status) { alert(status); } });

azarmfa
  • 47
  • 1
  • 1
  • 6