Intuit Data Services + Windows Azure + Identity
This week, we completed a small PoC for brabant court, a customer that is building a Windows Azure application that integrates with Intuit’s Data Services (IDS).
A couple words on mabbled from brabant court.
Mabbled is a Windows Azure app (ASP.NET MVC 3, EF Code First, SQL Azure, AppFabric ACS|Caching, jQuery) that provides complementary services to users of Intuit QuickBooks desktop and QuickBooks Online application. Mabbled achieves this integration with the Windows Azure SDK for Intuit Partner Platform (IPP). An overriding design goal of mabbled is to leverage as much of Microsoft’s platform and services as possible in order to avoid infrastructure development and focus energy on developing compelling business logic. A stumbling block for mabbled’s developers has been identity management and interop between Intuit and the Windows Azure application.
In this PoC we demonstrate how to integrate WIF with an Intuit/Windows Azure ASP.NET app. Intuit uses SAML 2.0 tokens and SAMLP. SAML 2.0 tokens are supported out of the box in WIF, but not the protocol.
I used one of Intuit’s sample apps (OrderManagement) as the base which currently doesn’t use WIF at all.
The goal: to supply to the .NET Windows Azure app, identity information originated in Intuit’s Workplace, using the WIF programming model (e.g. ClaimsPrincipal) and to use and leverage as much standard infrastructure as possible (e.g. ASP.NET authorization, IPrincipal.IsInRole, etc.).
Why? The biggest advantage of this approach is the elimination of any dependency to custom code to deal with identity related concerns (e.g. querying for roles, user information, etc.).
How it works today?
If you’ve seen Intuit’s sample app, you know that they provide a handler for the app that parses a SAML 2.0 token posted back from their portal (https://workplace.intuit.com). This SAML token contains 3 claims: LoginTicket, TargetUrl and RealmId. Of these, LoginTicket is also encrypted.
The sample app includes a couple of helper classes that use the Intuit API to retrieve user information such as roles, profile info such as e-mail, last login date, etc. This API uses the LoginTicket as the handle to get this information (sort of an API key).
Some of this information is then persisted in cookies, or in session, etc. The problem with this approach is identity data is not based on .NET standard interfaces. So the app is :
where RoleHelper.UserisInRole is:
WIF provides a nice integration into standard .NET interfaces, so code like this in a web page, just works: this.User.IsInRole(role);
The app currently includes a ASP.NET Http handler (called "SamlHandler”) whose responsibility is to receive the SAML 2.0 token, parse it, validate it and decrypt the claim. Sounds familiar? if it does, it’s because WIF does the same
I had trouble parsing the token with WIF’s FederationAuthenticationModule (probably because of the encrypted claim which I think it is not supported, but I need to double check).
Inside the original app handler, I’m taking the parsed SAML token (using the existing Intuit’s code) and extracting the claims supplied in it.
Then, I query Intuit Workplace for the user’s general data (e.g. e-mail, name, last name, etc.) and for the roles he is a member of (this requires 2 API calls using the LoginTicket). All this information also goes into the Claims collection in the ClaimsPrincipal.
After that I create a ClaimsPrincipal and I add all this information to the claim set:
The last step is to create a session for this user, and for that I’m (re)using WIF’s SessionAuthenticationModule.
This uses whatever mechanism you configured in WIF. Because this was a quick test, I left all defaults. But since this is a Windows Azure app, I suggest you should follow the specific recommendations for this.
The handler’s original structure is the same (and I think it would need some refactoring, especially with regards to error handling, but that was out of scope for this PoC )
Some highlights of this code:
- Some API calls require a dbid parameter that is passed as a query string from Intuit to the app in a later call. I’m parsing the dbid from the TargetUrl claim to avoid a 2 pass claims generation process and solve everything here. This is not ideal, but not too bad. It would be simpler to get the dbid in the SAML token.
- The sample app uses local mapping mechanism to translate “Workplace roles” into “Application Roles” (it uses a small XML document stored in config to do the mapping). I moved all this here so the ClaimsPrincipal contains everything the application needs right away. I didn’t attempt to optimize any of this code and I just moved the code pieces from the original location to here. This is the “RoleMappingHelper”.
- I removed everything from the session. The “LoginTicket” for instance, was one of the pieces of information stored in session, but I found strange that it is sent as an encrypted claim in the SAML token, but then it is stored in a cookie. I removed all this.
- The WIF SessionAuthenticationModule (SAM) is then used to serialize/encrypt/chunk ClaimsPrincipal. This is all standard WIF behavior as described before.
The web application:
In the web app, I first changed the config to add WIF module and config:
Notice that the usual FederationAutheticationModule is not there. That’s because its responsibilities are now replaced by the handler. The SAM however is there and therefore it will automatically reconstruct the ClaimsPrincipal if it finds the FedAuth cookies created inside the handler. The result is that the application now will receive the complete ClaimsPrincipal on each request.
This is the “CustomerList.aspx” page (post authentication):
The second big change was to refactor all RoleHelper methods to use the standard interfaces:
An interesting case is the IsGuest property that originally checked that the user was a member of any role (the roles a user was a member of were stored in session too, which I’m not a big fan of). This is now resolved with this single query to the Claims collection:
The structure of the app was left more or less intact, but I did delete a lot of code that was not needed anymore.
Again, a big advantage of this approach is that it allows you to plug any existing standard infrastructure into the app (like [Authorize] attribute in an MVC application) and it “just works”.
In this example, the “CustomerList.aspx” page for example has this code at the beginning of PageLoad event:
As mentioned above, the RoleHelper methods are now using the ClaimsPrincipal to resolve the “IsInRole” question (through HttpContext.User.IsInRole). But you could achieve something similar with pure ASP.NET infrastructure. Just as a quick test, I added this to the web.config:
And now when trying to browse “CustomerList.aspx” you get an “Access Denied” because the user is not supplying a claim of type role with value “SuperAdministrator”:
A more elegant approach would probably be to use deeper WIF extensibility to implement the appropriate “protocol”, etc., but that seems to be justified only if you are really implementing a “complete” protocol/handler (SAMLP in this case). That’s much harder work.
This is a more pragmatic approach that works for this case. I think it fulfills the goal of isolating as much “plumbing” as possible from the application code. When WIF evolves to support SAMLP natively for example, you would simply replace infrastructure, leaving your app mostly unchanged.
Finally, one last observation: we are calling the Intuit API a couple times to retrieve user info. This could be completely avoided if the original SAML token sent by Intuit contained the information right away! There might be good reasons why they are not doing it today. Maybe it’s in their roadmap. Once again, with this design, changes in your app would be minimized if that happens.
This was my first experience with Intuit’s platform and I was surprised how easy it was to get going and for their excellent support.
I want to thank Daz Wilkin (brabant court Founder) for spending a whole day with us. Jarred Keneally from Intuit for all his assistance and Federico Boerr & Scott Densmore from my team for helping me polish the implementation.