Making Ajax play with Passive ADFS 2.1 (and 2.0) – JSONP & Pre-Authentication

The first post, described the issue of using ADFS and Ajax to create SSO between a WebApp and a WebAPI.
This solution looks at using JSONP and pre-authentication to achieve SSO between sites on different domains.

Solution Overview

We add a html page (or handler) to the WebAPI solution.
Whenever we make a call to the WebAPI we first load the html page in an iFrame, this iFrame call handles all the ADFS redirects and sets the session cookies for the WebAPI.
These session cookies are then sent (automatically) with the next JSONP call to the server.

Caveats

  • Like all the solutions, this expects that the user has authenticated with ADFS via the WebAPP. When the iFrame hits the WebAPI pre-auth html page and the request gets redirected to ADFS if the user already has a session (that is compatible with the WebAPI relying party) a token will be issued without further authentication.
//Requires Jquery 1.9+
var hasPreAuthenticated = false;
var webAPIHtmlPage = "http://webapi.somedomain/preauth.html"
function preauthenticate() {
//ADFS breaks Ajax requests, so we pre-authenticate the first call using an iFRAME and "authentication" page to get the cookies set
return $.Deferred(function (d) {
if (hasPreAuthenticated) {
console.log("Already pre-authenticated, skipping");
d.resolve();
return;
}
//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 () {
hasPreAuthenticated = true;
iFrame.remove();
d.resolve();
});
});
};
function makeCall(){
return authenticate().then(function () {
var options = //JSONP ajaxOptions
return $.ajax(options)
});
}
view raw gistfile1.js hosted with ❤ by GitHub

Making Ajax play with Passive ADFS 2.1 (and 2.0) – Piggy-Backing

The first post, described the issue of using ADFS and Ajax to create SSO between a WebApp and a WebAPI.
This solution looks at the easiest solution, Piggy-Backing.

The central idea with Piggy-Backing is that the WebApp authenticates in the usual redirecty ADFS way and has the session cookies set.
The WebAPI then uses the same session cookie, thus not needing to ever authenticate with ADFS directly.

Caveats

  • The two applications must be able to share cookies (same root domain)
  • If the WebAPI attempts to authenticate with ADFS it will error; it will error as the AJAX calls will break as per the problem description, it will also break because the redirect url after authentication will be to the WebApp not the WebAPI.

Setting up this solution is easy. Just configure the WebApp as you normally would for ADFS and then use the same config for the WebApi project i.e. set the realm to be the same as the WebApp realm.
You will also need to set the CookieHandler section of web.config to match.

Sub-Domains

If you are using subdomains webapp.contoso.com and webapi.contoso.com then your cookiehandler will look this in both applications web.config

 <cookieHandler requireSsl="true" domain=".contoso.com" /> 

Different Ports or Virtual Directories

If you are using virtual directories or differing ports such as contoso.com/webapp and contoso.com:8000/webapi then your cookiehandler will look this in both applications web.config

 <cookieHandler requireSsl="true" path="/" /> 

A note on Web Farms and Microsoft Dynamics Crm 2011

If you are using load-balancing and/or MS CRM see this article. You need to implement step #3 as well to enable piggy-backing. The session cookie encryption method is changed to be more farm friendly.

Making Ajax play with Passive ADFS 2.1 (and 2.0) – The Problem

The Problem

ADFS is Microsoft’s Federated Identity Service, but if you are reading this you probably know that.
You may also know that the way it does passive authentication doesn’t work well with Ajax calls.

Below is a fiddle that shows the steps involved in authenticating a request for an ASPX page (that page belongs to the awesome Communica) protected by ADFS. This process happens the first time a request is made to an application, after that the authentication information is stored in a cookie (by default named FedAuth if you are using WIF)

Image

Request 37 responds with Http Status code 200 (ok); the response is a payload of secret information that is submitted to the return url (your application) via javascript that submits the returned form (circled above).

This is fine when we are in a browser as the browser runs the javascript happily and submits the form. Everything is dandy. Unfortunately in an Ajax world the first Http Status of 200 is presumed to be the response and the call is never completed as intended.

This is only a problem if your application is entirely an API. If it is a mixed API/Web Application your user will be authenticated when they load the application. Any subsquent call by your application to API endpoints will already have a session and will not perform the ritual outlined above.

However, if like me, you have a 100% API based application and you are attempting to enable SSO with other applications, then there is some work to be done.

In the next posts I’ll look at 3 possible solutions:

Async Execution Queue in TypeScript

I’m in the process of creating a fully client-side (with server side sync) JavaScript application.
Once thing I needed was the ability to queue and execute commands asynchronously but serially.
With JQuery Promises, this is remarkably easy to achieve.

The Gist below is in typescript.

export class AsyncExecutionQueue{
private tail: JQueryPromise = $.Deferred().resolve();
public enqueue(cmd:()=>any): JQueryPromise {
console.log("Queuing Command");
var next = $.Deferred();
var client = $.Deferred();
this.tail.always(() => {
try {
var result = cmd();
if (result.done && result.fail) {
result
.done(client.resolve, next.resolve)
.fail(client.reject, next.resolve);
}
else {
client.resolve(result);
next.resolve();
}
}
catch (e) {
client.reject(e);
next.resolve();
}
});
this.tail = next;
return client;
}
}
view raw AsyncExecutionQueue hosted with ❤ by GitHub
/// <reference path="jquery-1.8.d.ts" />
/// <chutzpah_reference path="libs.bundle.js" />
/// <reference path="qunit.d.ts" />
/// <chutzpah_reference path="TestLibs.Bundle.js" />
module Tests {
declare var JsMockito: any;
declare var JsHamcrest: any;
JsHamcrest.Integration.QUnit();
JsMockito.Integration.QUnit();
var queue: AsyncExecutionQueue;
QUnit.module("Async Execution Queue", {
setup: () =>{
queue = new AsyncExecutionQueue();
}
});
QUnit.asyncTest("should pass back errors from commandhandlers", () => {
var cmd = () => {
throw new Error("Exception");
};
queue.enqueue(cmd)
.always(start)
.done(() => ok(false, "Error was not returned"))
.fail((error) => ok(true, "Error was returned"));
});
QUnit.asyncTest("should execute next command if current command failed", () => {
var cmd1 = () => {
throw new Error("Exception");
};
var cmd2 = () => {
return "success";
};
queue.enqueue(cmd1);
queue.enqueue(cmd2)
.always(start)
.done((result) => QUnit.equal("success", result, "Second command execeuted"))
.fail((error) => ok(false, "Error was returned"));
});
QUnit.asyncTest("should execute commands serially", () => {
var results = [];
var cmd = (n) => {
var wait = Math.floor((Math.random() * 50) + 1);
return $.Deferred((d) => setTimeout(() => {
console.log("Executing Handler " + n + " took " + wait)
results.push(n);
d.resolve();
}, wait));
};
var result = $.when(
queue.enqueue(() => cmd(1)).done(() => console.log("Done with 1")),
queue.enqueue(() => cmd(2)).done(() => console.log("Done with 2")),
queue.enqueue(() => cmd(3)).done(() => console.log("Done with 3")),
queue.enqueue(() => cmd(4)).done(() => console.log("Done with 4")),
queue.enqueue(() => cmd(5)).done(() => console.log("Done with 5")),
queue.enqueue(() => cmd(6)).done(() => console.log("Done with 6")),
queue.enqueue(() => cmd(7)).done(() => console.log("Done with 7"))
);
result
.always(start)
.done(() => QUnit.deepEqual(<any>[1, 2, 3, 4, 5, 6, 7], <any>results, "Commands Executed Out of Order"))
.fail((e) => ok(false, e))
});
}