Yossi Dahan [BizTalk]

Google
 

Wednesday, July 22, 2009

Integrating DotNetNuke and the Geneva Framework

Was a somewhat painful exercise. sure – it was not helped by the fact that I’m not really familiar with DNN, nor am I really a web developer by any stretch of the imagination, but never mind that – DNN has certain ‘features’ that made making it play nice with the WIF  somewhat ‘challenging’. below are some points we’ve encountered that are worth remembering/considering (in no particular order) -

DNN will redirect to ErrorDisplay.aspx on most errors.
This means that once you’ve configured the web site with FAM and automatic passive redirects, most errors will send you in an endless redirect loop.
In the first few attempts I did to integrate the two, the aliases for my portals in the DNN database were only configured for ‘localhost’; as my STS is on a remote machine (very important for testing federated identity scenarios, obviously) I was now accessing my portal using the machine’s name and/or IP address, for which aliases had to be defined; this error was caught by DNN before the FAM had a chance to execute and so the call to ErrorDisplay.aspx was made when the user was still unauthenticated, but now it no longer carried a security token, which caused the redirect back to the STS and the infinite loop; to avoid that problem, I’ve added a location configuration in the web.config for ErrorDisplay.aspx and set it’s authorisation settings to allow all users – this allowed the error to be displayed despite of the fact the user is not authenticated (something that needs to be considered carefully, of course, but we’re not showing any dangerous details on our error page.)

The membership module performs a lookup for the username in DNN’s membership database.
As we’ve moved the authentication work from DNN to the STS, it is now possible for an authenticated user to not exist in the DNN database; we already have synchronisation process that keeps the two databases (our membership database, used by the STS, and the DNN membership database) aligned as we needed that anyway, but there’s always the chance of things getting out of sync; out of the box, the DNN membership module redirects the user back to the home page, in our case - because we’re using the FAM with auto passive redirects, this will enter an infinite redirect again (as the redirect to the homepage loses the security token), so we’re looking to change the on-error redirect to an allowed page (not an easy task in DNN, it appears)

 

DNN can host several portals.
DNN supports hosting many portals on the one application (/virtual directory) - driven by database configuration; that means that the FAM configuration, driven via web.config, does not fit very well as we’d have a single realm/replyTo address.
There were two optinos to choose from – we could decide that the entire DNN instance is a single RP, which would mean the existing configuration solution could suffice, but is a security risk – a user’s permissions cannot be checked at the STS at a portal level, only at ‘DNN level’; the other option was to treat each portal independatly, for this to work we had to set the realm and replyto values in the request to the STS dynamic as the configuration story was not enough; and so - we’ve extended the FAM by overriding OnRedirectingToIdentityProvider and setting the realm and reply properties of the SignInRequestMessage  dynamically (based on the HttpRequest, in our case)

AudienceURI’s

Setting the realm and replyTo dynamically, as described above, raised another challenge - the Geneva Frameworks would like you to specify the audienceUris you’d be expecting in the tokens received from the STS; generally – this setting exists in the web.config, but as our realm can be one of many things we were faced with two options – either list all possible audienceURIs in the ‘allowed’ list, which has some security implications, or provide a mechanism to dynamically evaluate the request arriving and see if its allowed.


The problem with the first approach, out-of-the-box, is that it means keep updating the web.config of DNN with newly added portals’ Uris; this actually has two implications – one: it means that whoever sets up a new portal (which is a DNN user, generally), must have access to the config file – not quite what we had planned (or can live with), two: whenever the web.config gets edited, if we did allow that, the appdomain is reloaded, which kicks users our of their sessions (or so I’m told) as well as makes the next call really slow as the application is recompiled; the outcome was clear – we must stay away from the web.config

It appears that there are several ways around this:

  • You can have a single AudienceURI in the RP side, and have code your STS to always return the same one (for all portals, despite the realm provided); you will need a way to find the audienceUri to use (as its no longer the realm from the request, which is generally used), but that’s possible through configuration;  you are also introducing a risk as DNN – the RP – will now accept tokens across portals, but that risk can be mitigated by DNN’s own authorisation.
  • You can load the audienceURIs section of the configuration in the RP from somewhere else but the web.config (a database table, for example); to do this you would need to add a handler to the FederatedAuthentication.ServiceConfigurationCreated event in the FAM (best way is through the constructor of your custom FAM, InitializeModule is called after the configuration has been loaded) on the RP side and set the audience uri for each portal alias in the DNN database; in a sense this is the RP version of the previous option as it will allow any token to any allowed portal access, even if the token is issued to one portal and the redirect goes to another; it does solve the need to edit the web.config, but it does not solve the need to restart the app domain when changes have been made as the call to the database will only happen once – when the module is initialised.

Both the options above provide some answer to the first approach mentioned at the beginning of this section – allow access to all possible realms, without having to edit web.configs.

The two options below talk about how you could implement a more dynamic check – moving further away from the existing method of checking against a static list of audienceUris -

  • You can implement your own SamlSecurityTokenHandler (you might want to implement two – one that inherits from Saml2SecurityTokenHandler and another that inherits from Saml11SecurityTokenHandler) in which you would override the ValidateConditions method; in ValidateConditions you would call the base ValidateConditions method, passing in false for the ‘enforceAudienceRestictions’ parameter – this would ensure the configured audienceUris are not checked by the base method; you would then implement your own audienceUri validation, presumably against the DNN database (the conditions parameter passed  to you will contain the audienceUri provided by the STS); you could use either code or configuration to setup your RP to use these tokens instead of the built in ones.
  • A slightly re-factored version of the above is to wrap the validation code required in a custom SamlSecurityTokenRequirement class in which you override the ValidateAudienceRestriction method; both Saml2SecurityTokenHandler and Saml11SecurityTokenHandler classes allow you to provide them with a custom SamlSecurityTokenRequirement class in order to override the built-in logic; this allows you to write the validation logic just once; you will need to replace the default class in the token handlers with yours, which is best done in the same ServiceConfigurationCreated event mentioned above.

DNN’s UrlRewriteModule will redirect any request to the DNN vdirs copying any paths “lower” than the DNN vdir to the query string -

This means that even without dynamically setting the realm and reply address as described above, out-of-the-box you would simply get an infinite redirect situation due to lost cookies -
If you have a portal with the alias www.MyDomain.com/DNN/MyPortal and you set the realm and reply to to this address, you get redirected to the STS and then back to the portal correctly; cookies will be set at the URL above.
However, the UrlRewriteModulre will redirect the request to to www.MyDomain.com/DNN/default.aspx?alias=MyPortal; as the authentication cookies are stored in the original location (one or more virtual directories “lower” than the DNN root directory) they cannot be found and so the user is redirected back to the STS and so on…
The obvious solution is to set the cookie handler path in the configuration to be the DNN’s virtual directory – it would mean that moving between portals would require obtaining a new token and a roundtrip to the STS, but it will solve the circular redirect, which is better (and in any case this is not a likely scenario as far as real users are concerned, as they will normally work on a single portal)

Logout

Ok – possibly the easiest point encountered – we needed to replace the logout functionality (which would logout the user locally from DNN) to the ws-federation single sign out supported by our STS; to do this we simply replace the code in Login.ascx.vb (in our case it existed in [path to root DNN website]\admin\skins) from the out-of-the-box redirect to a redirect to to the STS with ?ws=wsingout1.0 in the query string.

 

Going over most items in this list with Jon Simpson – the chief architect in one of the companies I work with – he raised an interesting point – why does every time the Geneva Framework gets upset the user ends up in an endless redirect loop? (ok, he didn’t put it quite in those words, and also generally his view is, naturally, limited to the bits he cares, or – have to worry – about, but the man has a point)

I did not buy into this initially, but I suspect there’s a lot of sense in expecting the framework to recognise there’s an endless loop in place and display some error instead of keeping it going; we will certainly need to do something on our end, as an endless redirect, even if cause by a configuration error on our part, is not acceptable, but this should be part of the framework, or so Jon says :-).

Labels: ,

Thursday, July 09, 2009

Geneva Framework Query String woes

I bumped into a situation where a query string passed from one RP to another RP was lost as the request went through the STS.

Looking through this carefully I think I figured out why this would happen, and it turns out that the scope of this issue is slightly bigger (and therefore the title of my post is slightly misleading), see the details below - but first – here’s how it goes when everything goes well -

A user tries to access an RP url with some query string parameters; the RP configuration redirects the user to the STS providing realm uri and reply address; as these two values are generally retrieved from the RP configuration, they are unlikely to include the query string parameter.

Good news are, though, that the FAM keeps the original URL requested by the user (excluding the scheme and domain) in a context field passed to the STS with the Sign-in request(the ‘ru’ field).

Once the STS authenticates the user it redirects the request to the reply address provided by the RP; this is NOT the original address the user requested (now in the context) but the reply value provided by the RP through the request, and it will NOT contain the query string parameter.

The FAM, at the RP side, now extracts the URL the user requested from the context and does a second redirect to that URL – the one with the query string.
This time the local cookie is found, the user is authenticated, and the request can be processed by the RP code, where the query string is available to be read.

 

So – Where is it broken? it appears that if, after all of the above had happened, a request to the RP, with some query string, is redirected again to the STS for authentication for some reason, as the request comes back from the STS to the RP, the SignInResponseMessage is being ignored (as there’s already an authentication cookie) and that second redirect, to the URL in the context does not happen.
In this case, the RP code that will get executed in the page defined in the reply URL in the configuration, which is not likely to have the query string the user requested; in fact – and here’s the bigger scope – it may well be a completely different page to the one the user requested in the browser!

 

So – the question is – why would I be redirected to the STS when there’s already an authentication cookie for this realm? (after all – if there is cookie I would not be redirected to the STS, right?!) - it turns out there are some ‘edge' cases where this is possible, here’s one -

Step 1

1. Some link somewhere sends the user to the RP using domain name in the URL, with some query string parameters – http://MyDomain.com/SomeApp/Default.aspx?Something=SomethingElse

2.The RP redirects the request to the STS for authentication, providing the realm and reply from the configuration; let’s assume the reply address was set to http://MyDomain.com/SomeApp/SSO.aspx

3. The STS authenticates the user, and redirects back to the reply URL above

4. At the RP, the FAM processes the SignInResponseMessage , stores the authentication cookies for MyDomain.com/SomeApp and then redirects to the context’s http://MyDomain.com/SomeApp/Default.aspx?Something=SomethingElse (note that SSO.aspx never got executed, and that the query string is now available for Default.aspx)

 

Step 2

5. Now assume that another link somewhere sends the user to the same application, but for whatever reason it uses the server’s IP address instead of the domain – /SomeApp?SomeQueryString">http://<some IP address>/SomeApp/Default.aspx?Something=SomethingElse

6. The RP cannot find any authentication cookies for this URL (as it has the IP address and not the domain name) and so it too, redirects the request to the STS for authentication, however – as its the same configuration, the STS URL, the realm and the reply address are the same as in Step 1, so the STS redirects straight back to http://MyDomain.com/SomeApp/Default.aspx?Something=SomethingElse 

7. At the RP, as the URL is now the same as it was in Step 1, the FAM find the previous cookie set and so it ignores the SignInResponseMessge; this means that the redirect that would have happened to the original URL the user requested does not happen, and so it is the page at the reply address, without any query string, that gets processed

 

There is actually a much simpler way to reproduce the issue, albeit slightly less realistic (post testing) – if you access the RP via it’s IP address, but the reply address in the configuration is set using the domain name;

The cookie will be stored for the domain name, and so any subsequent attempts to access the RP, with query string parameter, will result in them being removed as the redirect at the FAM does not happen.

Labels: ,

Monday, June 22, 2009

Implementing Single Sign Out scenario with the Geneva Framework

One of the items on my to-do list for a while now was to add support for single sign out in our passive scenarios; the idea is that if a user browses to several RPs, and then hits the sign out button on one of them, she would automatically be sign out of all the other RPs visited in this session.

Whilst, as you will see shortly, the framework has great support for this scenario, and it is easily achieved, it is not the out-of-the box behaviour; out of the box – if you’re using the SignInStatus control (with or without the FAM) and the FederatedPassiveTokenService control, when the user hits the sign-out button of the SignInStatus control, she will be signed out of the current RP, as well as the STS itself, but any other RPs the user had visited in this session will keep her logged in.

So – if the user browsed to application A, authenticated at the STS, and then browsed to application B, she is not signed on in both applications as well as on the STS; hitting the sign out button in application B will sign her out of application B as well as of the STS; if she tries to browse to application B now (with no browser caching), she will get redirected to the STS, and would need to re-authenticate there; same would happen if she tried to browse to any application other than application A, which is protected by the STS; within application A, however, the user would still be authenticated and she will be able to keep using this app.

In some cases this may be acceptable, but in our case the users assume that if they hit sign out, they are signed out of the entire “set”, and so we were set on achieving this behaviour.

It turns out that the framework has great support for this scenario, and that only very little code is required to achieve this; in fact – on the RP side – there’s nothing to do.

Both the FAM and the SignInStatus controls handle requests to sign off out of the box, all you have to do is send an HTTP request with “wa=wsignoutcleanup1.0” in the query string and the framework will take care of removing the local cookies; it will even return a nice image to indicate success (you can control which image to show through configuration);

To see this in action – create a standard scenario with two RPs configured to use a single STS; add to your STS an ASPX page,  which would look something like this (you will need to update the urls to point at your RPs) -

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="SignOut.aspx.cs" 
Inherits="HRG.Profile.Identity.STS.Web.Passive.SignOut" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<
html xmlns="http://www.w3.org/1999/xhtml" >
<
body>
<
form id="form1" runat="server">
<
img src="https://localhost/ststests/testwebsite4/default.aspx?wa=wsignoutcleanup1.0" /><br />
<
img src="https://localhost/ststests/testwebsite3/default.aspx?wa=wsignoutcleanup1.0" />
</
form>
</
body>
</
html>



In the code behind add the following -



protected void Page_Load(object sender, EventArgs e)
{
FormsAuthentication.SignOut();
}


Now run through your scenario - login to one application, then browse to another, then browse to this test page; you should see a couple of “green ticks” indicating you have been signed out of both applications.



Now try to browse to either of them (make sure to refresh to pages to avoid browser caching) - you should notice that you’re no longer authenticated in neither (nor the on the STS) and that your’e redirected to the STS’ login page. cool!



So- we’ve proved that there’s really nothing to do on the RP side to achieve single sign out; but what do we need to do on the STS side? well – when the user hits the sign out link button on the SignInStatus control a request goes to the configured url for the STS, so this would be the entry point; what we really need to do is figure out a way to, for example, dynamically generate a page similar to our test page above; to do that we need to be able to a) track the RPs a user had visited and b) control the behaviour of the STS when the user hits sign-out on any RP, to make the required sign out requests to all the other RPs.



Until now I’ve been using the STS control (FederatedPassiveTokenService) in my passive STS, and so – to add behaviour required I would have to extend it, which is not something I felt comfortable doing; the alternative was to get rid of the control altogether and simply write the code required to handle both sign in and sign out from scratch, which is something I wanted to experiment with (in fact – I had to do much of it it in a different area of my solution – bridging single sign out protocols, but that’s for another post), so I though this is a good opportunity to give it a go.



As it turns out, as the framework has all the code to do the heavy lifting, all I needed to do is “control the flow”, and it was all relatively painless - I removed the controls from my page, and started to replace it by placing code in the code behind -



First – I needed to figure out what request I’ve received from the caller; this was as simple as two lines -



WSFederationMessage message = null;
bool messageCreated = WSFederationMessage.TryCreateFromUri(Request.Url, out message);



messageCreated now indicates whether the request to the STS was a valid one, message is expected to be either SignInRequestMessage or SignOutRequestMessage (there are two other possible request types that are not currently supported by the framework, but that’s for another day)



Before I’ll go back to my single sign out scenario, I need to complete the single sign in scenario as I no longer have the control on the page (I could, potentially, leave the control on the page and do nothing if the message was a sign in request – leaving the control to do all the work, and if the message was a single sign out request execute whatever code I needed to achieve that, but I wanted to get rid of the control so I implemented code for both paths)



So – to implement the single sign in my STS needed to call the STS, get a SignInResponse message and write that to the Http response stream, how is this done? well – there may be many favours, but the main code would look something like this (some elements removed for bravity) -



if (message is SignInRequestMessage)
{


   SignInRequestMessage requestMessage = message as SignInRequestMessage;



// Create our STS backend
SecurityTokenService sts = new MySTS(stsConfig);

// Create the WS-Federation serializer to process the request and create the response
WSFederationSerializer federationSerializer = new WSFederationSerializer();
WSTrustSerializationContext serialisationContext = new WSTrustSerializationContext();
// Create RST from the request
RequestSecurityToken request = federationSerializer.CreateRequest(requestMessage, serialisationContext);

// Get RSTR from our STS backend,
//the thread's principal would not be an IClaimsPrincipal, so create one from the contained identity
IClaimsPrincipal principal = ClaimsPrincipal.CreateFromIdentity(Thread.CurrentPrincipal.Identity);
//issue the RSTR
RequestSecurityTokenResponse response = sts.Issue(principal, request);
// Create Response message from the RSTR
SignInResponseMessage response = new SignInResponseMessage(new Uri(response.ReplyTo),
response,
federationSerializer,
serialisationContext);



response.Write(Page.Response.Output);
Response.Flush();
Response.End();



}



I’m creating an instance of the STS for each request, but am re-using the sts configuration class (which I keep as an “Application” variable in the STS’ asp.net application).



I’m then using a federation serialiser to create the RST, run this through the STS (providing the principal, “converted” to a ClaimsPrincipal) and then create a SignInResponseMessage out of the RSTR returned by the STS;



Job done – my STS now supports single sign in without the control;



You can already imagine what I needed to do to complete the sign out support- to start with I needed to add an else-if to handle SignOutRequestMessage (as I’ve mentioned – there are other types of requests theoretically possible, but lets not worry about them at the moment) -



else if (message is SignOutRequestMessage)
{


}



The first thing I would do there, is sign out the user from the STS itslef -



FormsAuthentication.SignOut();



All I needed to do now is add bog standard ASP.net code to generate the required Http Get requests to all my RPs; but to do this I needed to keep track of which RPs a user had visited within the session, so I know which RP’s to sign her out of; to help me achieve that I’ve created the following class to track the user’s visited realms -



public class VisitedRealmsTracker
{
private Dictionary<string, string> visitedRealms = new Dictionary<string, string>();

public void Add(string sessionId, string realm)
{
string key = sessionId + "|" + realm;
lock (visitedRealms)
{
if (!visitedRealms.ContainsKey(key))
visitedRealms.Add(key, sessionId);
}
}

public IEnumerable<string> GetAllRealmsForSession(string sessionId)
{
//find all visited realms for this session and return the second part of they key (after the '|') which would be the realm
return from visitedRealm
in visitedRealms
where visitedRealm.Value == sessionId
select visitedRealm.Key.Split('|')[1];
}

public void ClearUserEntries(string sessionId)
{
lock (visitedRealms)
{
List<string> keys =
visitedRealms.Where(realm => realm.Value == sessionId)
.Select(realm=>realm.Key).ToList();
foreach (string key in keys)
visitedRealms.Remove(key);
}
}
}


I’ve added a member of this type to my STS Configuration class, which – you would remember – I now keep as an application variable, and so I could add a call to Add in the STS code handling the sign in request I showed earlier, which would ensure I’m tracking all the visited realms; the sign out logic could now iterate over the results of the GetAllRealmsForSession and handle the logout requests, simple code to achieve this could be something like -



foreach(string realm in stsConfig.Tracker.GetAllRealmsForSession(Session.SessionID))
{
//get realm configuration
ReliantPartyConfigurationElement rpConfig = Configuration.ReliantParties[realm];
//create an image pointing the at realm's signout url appending the signout and cleanup action
Image img = new Image();
img.ImageUrl = rpConfig.SignOutUrl.Trim()+"?wa=wsignoutcleanup1.0";
Repeater1.Controls.Add(img);
//add a line break after each realm
LiteralControl br = new LiteralControl("<br />");
Repeater1.Controls.Add(br);
}



This sample code simply creates the same images I’ve previously had hard coded in the test page dynamically.



With all the code in place – sign in requests are being processed by code instead of the control, with the code now customised to keep track of visited realms, sign-out requests use this tracked information to dynamically build a page that issues the required Http get request to all the visited RPs to sign the use out of all of them; single sign out achieved. easily.

Labels: ,

Friday, June 12, 2009

Geneva Framework and Url case sensitivity- solved?

I’ve blogged before (somewhat briefly, for a change) about my surprise when I learnt that URLs are [largely theoretically, in my view] case sensitive and the problem that this causes for a Geneva Framework based passive STS implementation.

In that post I mentioned  a solution suggested by Peter Korn at the time – setting the path of the cookie to the domain root (‘/’) instead of the application path (including virtual directories), as, unlike the rest of the path, the domain name in a URL is not case sensitive, this works well, and I though it was “case closed”; until recently, when I’ve realised this solution has a very significant drawback - as the cookie, containing the authorisation token from the STS, is stored at the root of the domain, it will be served to every application under that domain, which is taking single-sign-on slightly too far :-)

Following this approach it is not possible allow access to one application and deny it from another (on the same domain) other than through claims processing in the applications themselves, which is a less secure approach from an architecture perspective); clearly not a good solution then…

So – I needed to go back to storing the cookie in the correct path, which would ensure that the STS is re-visited when trying to access a second application (even in the same domain), which – in turn – would mean that the user’s permissions are re-evaluated, before a second, application-specific, token is provided; with that - came back the problem of the URLs being case sensitive.

Thankfully, we’re now on the TAP program for the Geneva Framework, and we’re getting great support by the guys at Redmond (can’t thank them enough!), and after bringing up this issue in a discussion, Shiung Yong suggested another approach to solving this - overriding the GetReturnUrlFromResponse method in the WSFAM.

(Side track: The more I work with the Geneva Framework the more impressed I am with the extensibility options it provides, sure – it’s hard to figure those out on your own if you don’t know about them, and yeah – the resulting solution is often somewhat fragmented, with bits of code in several places, but that’s not much different from many other solutions in this space I suspect – you can see this with many WCF implementations – on the upside, however, if you’re willing to put the sweat, you can do pretty much everything (but yes – the continuum moves from adding a couple lines of code to re-writing the framework :-) )

To understand why and how Shiung’s solution works, consider the following scenario, describing the problem (and here’s where my description is bound to get somewhat confusing) -

Out of the box, the flow of circular redirects, when the URL in the browser is entered in the “wrong” casing, is as follows -

  1. The user types in the RP’s URL, let’s say - all in uppercase, into the browser
  2. As the http request to the RP does not contain an authentication token at this point, the FAM at the RP redirects it to the STS, providing the RP’s ‘realm’ to the STS (the ‘realm’ is configured at the RP and is intended to provide a unique URI to the STS, which it can use to identify the RP, and, for example, be used to load the relevant configuration such as which certificates to use when creating the token); the original URI the user had typed in is also provided through the query string (the ru property in wctx); optionally, and crucially, the RP may also provide the wreply query string parameter, based on its configuration; it is expected that the STS will forward the request, after authentication the user, to this address (but this is not mandated), this will become a key point shortly.
  3. Still at the STS the user authenticates (generally using a login screen), and the STS redirects the request, with a ‘sign in response’ message containing an authentication token back to the RP; as mentioned before it is expected that this would be the address provided by the wreply (and this would be the default behaviour provided by the framework, but this can be easily overridden in the STS’ implementation); for this example, lets assume that the configured value, echoed in the wreply property is set to be in lowercase (remember – the user typed in the URL in the browser in uppercase).
  4. The redirect request contains the set-cookie instructions with the token from the STS and so the browser would set the required cookies in the address the STS redirected to - the lowercase address.
  5. In the step that would follow, the FAM does its sign-in ‘magic’, which concludes by redirecting the request to the URL set in the STS’ response message through the ru field in the query string - this is the URL the user typed into the browser initially, kept by the RP and then the STS - which is all uppercase
  6. At this point, FAM is called again for the new request, attempting to extract the authentication cookie, but as the cookie was stored on the URL the STS redirected to – which was lowercase - and the browser is now using the URL the user typed initially – which is all uppercase - the cookie is not served by the browser, and thus not found in the server code, and the user is being redirected back to the STS as if this was the first call;
  7. As the request arrives to the STS with the uppercase url again, the above would happen again and again in an endless cycle.

Confused? hopefully not too much…but to summarise - out of the box, if the two (the URL configured as the reply to address in the RP, or any other URL the STS uses to redirect back to the RP)  and the URL typed into the browser by the user) are not [case-sensitive] identical, the cookie will be set, but subsequently not found when attempted to be read and thus authentication at the RP would continuously fail.

In comes Shiung’s solution -

As long as there’s a convention in the implementation as to the correct form of the URLs (or if drowning in more configuration is acceptable) the FAM could be extended to over come this -

Step 5 above mentions the FAM has some ‘magic’ authentication work with a redirect in the end; the built in implementation uses the ru field to obtain the address to redirect to, but there’s a good extension point there in the form of the GetUrlFromResponse method of the FAM which is called to obtain the url; by overriding this function you can provide whatever logic you wish to control the URL the FAM would redirect the request to after authenticating the request.

Lets say we can agree (as we have) that all reply to addresses will always be configured in lowercase (whilst we can’t control user behaviour, we can control our own configuration), with that agreed we can override the GetUrlFromResponse to always convert the ru value to lowercase before returning it to the bulit in functionality – here’s my version of the method, as suggested by Shiung -

   public class CaseInsensitiveFAM : Microsoft.IdentityModel.Web.WSFederationAuthenticationModule
    {
        protected override string GetReturnUrlFromResponse(System.Web.HttpRequest request)
        {
            string returnUrl = base.GetReturnUrlFromResponse(request);
            return returnUrl.ToLower();
        }
    }

(it is important, of course, to remember to configure the RP to use your custom FAM and not the build-in one -

        <!--<add name="WSFederationAuthenticationModule"
type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule,
Microsoft.IdentityModel, Version=0.6.1.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35"/>-->

<add name="CaseInsensitiveFAM" type="CaseInsensitiveFAM, Utilities"/>


)



What had just happened?




  1. By convention, we ensured the RP provided a lowercase reply to address to the STS.


  2. The STS uses this (lowercase) address to forward the request containing the authentication token, and this is where the cookies will be set.


  3. The FAM uses GetUrlFromResponse to retrieve the URL to redirect to, my customised version ensures this would always be lowercase, aligned with the RP configuration


  4. The browser is redirected, again to the lowercase address, this time to receive the cookies set in step 2 which means the request is now authenticated and the user is let in; no more circular redirects!



Of course I’ve implemented a hardcoded rule (always lowercase), but you could use configuration, investigate the http request message or any other logic you’d like…



Some issues remain with that approach (that I can think of) -



If, at this point, the user goes and types the URL in a different casing, as the cookie already exists and the FAM code will not execute again, the user will get redirected to the STS for authentication, but that’s fair enough – I don’t know of any user that would do that in real life..and the result (requiring re-authenticaiton) is quite acceptable



The other thing is that this solution would break should an application be case sensitive (for query string parameters, for example), but we don’t have that problem, and it could be handled by more sophisticated code in the custom FAM, so that’s ok as well.



 



I suspect this is not the clearest post I’ve ever published (but, unfortunately, probably not the worst), so I can only hope someone will manage to make sense of it and find it useful; I’m pretty sure I’ll need it for future reference; there’s no chance I’m remembering all of this!



Labels: ,

Wednesday, April 01, 2009

RoleClaimsMapper for the Geneva Framework

Briefly back on my STS work -

Our STS implementation can already replace the authentication implementation of most of our applications; naturally we can’t do that just yet, given that the Geneva-framework has not been released yet, but all of my tests are quite positive so we’re just waiting for the opportunity to start using it.

However, so far, we  were not in a position to replace the authorisation mechanism, not easily anyway, and that’s something that was on my list for some time now.

The STS provides a list of claims, which the applications can relatively easily access via code, as many samples show, and this proves very useful; application can investigate various claims about a user and drive their functionality from that.

It does mean, though, that the applications need to change to support this new claims based mode for authorisation, which is not something we can just assume we would be able to do; as a start, we just want to achieve an in-place replacement for our current authorisation logic.

Most of our web apps currently use ASP.net membership and roles and so they extensively use ‘IsInRole’ checks to figure out user authorisation and drive the application behaviour, to start with, we had to hook to that mechanism.

Luckily the Geveva framework has a relatively good support for exactly this need - , out of the box, it would convert any claims of the Microsoft role namespace (‘http://schemas.microsoft.com/ws/2008/06/identity/claims/role’) to roles; so – if a token included a claim of this type with a value of ‘Manager’, a call to HttpContext.Current.User.IsInRole(“Manager”) would return true.

And so I made sure my STS adds any roles with the correct claim type, very easy.

However – this is very Microsoft centric. what about all those claims that come from systems that don’t follow Microsoft’s approach? (how dare they!) ? and what about us wanting to have our own claims, using our own types, some matching roles (while others may not…) -

Well – we needed a way to map any claims to ms-role claims before the Geneva framework does its bit.

As is often the case - Dominick Baier was most helpful in posting on exactly that, and so, following his example, I created my RoleClaimsMapper -

    public class RoleClaimMapper : ClaimsAuthenticationManager
{
public override IClaimsPrincipal Authenticate(string endpointUri, IClaimsPrincipal incomingPrincipal)
{
//load configuration section for component
RoleClaimsMapperConfigurationSection config =
(RoleClaimsMapperConfigurationSection)ConfigurationManager.GetSection("RoleClaimsMapper");
//create a collection of claim types and populate from configuratoin
List<string> claimsToMap = new List<string>(config.RoleClaims.Count);
foreach (RoleClaimConfigurationElement claimElement in config.RoleClaims)
claimsToMap.Add(claimElement.ClaimType);

//loop on all identities, we really only expect one, but can easily support multiple.
foreach (IClaimsIdentity identity in incomingPrincipal.Identities)
{
//extract the claims that we need to map (matching the configured list of claims)
IEnumerable<Claim> roleClaims =
identity.Claims.Where<Claim>(c => claimsToMap.Contains(c.ClaimType));
//now create a role claim (using the MS role claim type) for each claim found;
//need to keep this outside claim loop so we don't modify the collection while iterating
List<Claim> claimsToAdd = new List<Claim>(roleClaims.Count());
foreach (Claim claim in roleClaims)
claimsToAdd.Add(new Claim(Microsoft.IdentityModel.Claims.ClaimTypes.Role, claim.Value,claim.ValueType,"local",claim.Issuer));
//add new claims to current identity
identity.Claims.AddRange(claimsToAdd);
}

return incomingPrincipal;
}
}



I then configured my authentication manager with the framework -



<microsoft.identityModel>
<claimsAuthenticationManager type="RoleClaimMapper,Identity.Utilities" />



and added my bit of custom configuration



  <RoleClaimsMapper>
<RoleClaims>
<add Type="http://someDomain.com/identity/claims/SomeRole"/>
<add Type="http://someDomain.com/identity/claims/AnotherRole"/>
</RoleClaims>
</RoleClaimsMapper>



As you can see this code would take a list of claim types from configuration, and map all claims of these types to roles, adding them to the identity’s claims collection using the required claim type (leaving the original claim intact), and voila – when the app executes it can check the roles, corresponding to the values supplied in my custom claims using -



HttpContext.Current.User.IsInRole(“[custom claim value"]”);


Very nice indeed!



 



UPDATE: Dominic’s comment on this post (on http://geekswithblogs.net/Connected/archive/2009/04/01/roleclaimsmapper-for-the-geneva-framework.aspx#453535) had indirectly suggested an even cleaner solution; instead of duplicating the claims, I can add the claim types I have as roles to each identity’s RoleClaimTypes collection; this achieves the same result in a much cleaner way, here is the updated function -



        public override Microsoft.IdentityModel.Claims.IClaimsPrincipal Authenticate(string endpointUri, Microsoft.IdentityModel.Claims.IClaimsPrincipal incomingPrincipal)
{
//load configuration section for component
RoleClaimsMapperConfigurationSection config =
(RoleClaimsMapperConfigurationSection)ConfigurationManager.GetSection("RoleClaimsMapper");

//loop on all identities, we really only expect one, but can easily support multiple.
foreach (IClaimsIdentity identity in incomingPrincipal.Identities)
//for each identity, add all the claim types that are role claim types.
foreach (RoleClaimConfigurationElement claimElement in config.RoleClaims)
identity.RoleClaimTypes.Add(claimElement.ClaimType);

return incomingPrincipal;
}

Labels: ,

Sunday, February 15, 2009

Supporting both active and passive scenarios in my STS

In a comment on a previous blog post Travis Spencer asked

Can you explain more about how you implemented an STS that supports both active and passive scenarios?

 

So here’s how -

 

To start with – I’ve implemented my STS class with all the logic I needed; this was done as a class library with several classes – my STS implementation, my STS-Configuration class, an STS service factory, my custom WindowsUserNameSecurityTokenHandler implementation and all the classes I needed to support my custom configuration section.

Then, in order to support an active scenario, I’ve created a WCF service and, through the SVC file, I’ve configured it to use my STS service factory class -

 

<%@ ServiceHost Language="C#" Debug="true" Service="<My STS configuration class>"  factory="<My STS Factory class>"%>

 

I’ve then configured the web.config of the wcf service to support my scenario – that included all the relevant binding configuration I needed,  the Geneva framework related configuration (microsoft.IdentityModel) as well as any custom configuration my STS uses.

 

The passive scenario can seem a little bit more confusing -

Obviously I’ve started by creating an asp.net web application; this application basically  has two web pages (admittedly I’m simplifying things a little bit for clarity) – default.aspx and login.aspx

 

Using standard asp.net forms authentication the web site is configured to redirect all unauthenticated users to the Login.aspx page, which in turns has a pretty standard login implementation using my custom username validator logic and the framework’s RedirectFromLoginPage function to set the local forms authentication cookie.

All my web-based reliant parties redirect the user to the default.aspx page; forms-auth then redirects again to login.aspx for authentication and then, once authenticated, the user is redirected back to default.aspx; on this page I’ve simply put the FederatedPassiveTokenService control provided with the geneva framework configured to use my STS configuration class as the service; this takes care of calling the STS and posting the token back to the RP

 

I hope that makes sense…do let me know if it does not!

Labels: ,

Tuesday, December 16, 2008

Never thought Url would be case sensitive

 

Working on my Geneva Framework based STS scenario I’ve stumbled into a very weird and annoying case where by if the user typed a Url in the wrong case (compared to the case of the V-Dir) the browser would enter a circular redirect between the STS and the RP.

 

I’ve started a forum thread, which you can find here, that got an answered by Peter Kron from MS through which I’ve learnt that the path portion of a cookie is case sensitive; you can find this in this RFC spec as well (read 3.3.3) -

…the old  and new Domain attribute values compare equal, using a case-insensitive string-compare; and, the old and new Path attribute values string-compare equal (case-sensitive). …

I don’t know if that’s just me, but I find this really surprising as, as a web user, I was never “trained” to tread urls as case sensitive, but it appears that, according to the spec, any personalisation stored for a particular path might be lost if I enter the wrong url?

In the STS scenario case this would mean potentially me having to login again, although I have already logged in on the STS.

 

Peter suggest to store the cookie against the domain, which is not case sensitive, and is good enough for me (for now?), but I don’t know if that’s realistic for all scenarios…..

Labels: , ,

Thursday, December 04, 2008

Geneva-based passive STS and .net 2.0 web applications

Over the last few weeks I’ve been working on implementing a Geneva Framework based STS that supports both active and passive scenarios.

This is progressing very well and I already have a fairly solid PoC running for both scenarios.

Generally, to make any web site participate in the federated identity “dance” all it takes is some configuration on the application’s web.config (separate post coming shortly), but up until today I have only done so for web applications developed as a .net 3.5 project.

Today I have created a bog standard .net 2.0 web application project (on VS 2008, though, not that it should matter) and applied exactly the same configuration as I did on the other web sites.

when I ran this, not so surprisingly, everything just worked. isn’t it great?!

 

Of course – considering that everything is implemented as HttpModules – this should not be very surprising, but still.

It is important to remember, though, that .net 3.5, as well as the Geneva Framework, must be installed on the machine running the web site for this to work of course.

Labels: ,

Tuesday, November 25, 2008

Configuring the Geneva Framework based STS to work with custom UserNamePasswordValidator

It took me a little while (and quite a bit of help from others on this thread) to get to a relatively simple implementation, so I thought I’d summarise the steps I’ve taken –

At the risk of sounding the obvious I would definitely recommend making sure the overall STS scenario works well using windows authentication before changing it to support custom authentication.

 

Once that’s done change the STS’ bindings’ clientCredentialType to UserName and the establishSecurityContext to false.

      <ws2007HttpBinding>

        <binding name="UserNameAuthentication">

          <security mode="Message">

            <message establishSecurityContext="false" clientCredentialType="UserName"/>

          </security>

        </binding>

      </ws2007HttpBinding>

The equivalent changes need to be made on the clinet’s binding going to the STS. these may not be obvious at first glance – on the client you will have the endpoint representing the RP and using ws2007FederationHttpBinding; inside this binding’s configuration you will find the issuer element, which is somewhat similar to an endpoint; this represents the STS’ endpoint and as such has a binding (ws2007HttpBinding) and a binding configuration; it is in that binding’s configuration that you need to change the credential type. setting it on the wrong bindings, as I did initially would send you back a couple of hours :-)

Next, as we’re using username authentication for the STS, a service certificate must be used so that the credentials can be encrypted. This is done through configuration of a service behaviour on the STS service as such:

    <behaviors>

      <serviceBehaviors>

        <behavior name="STSBehaviour">

          <serviceCredentials>

            <serviceCertificate findValue="STS" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName"/>

          </serviceCredentials>

          <serviceMetadata httpGetEnabled="true"/>

          <serviceDebug includeExceptionDetailInFaults="true"/> <!—use for debug only -->

        </behavior>

      </serviceBehaviors>

(don’t forget to wire the behaviour to the service...)

As the test certificate I’m using are not valid, I needed to disable validation on the client side; I could not find a way to do this through configuration, as at the client there isn’t an endpoint as such for the STS service, just the issuer element in the ws2007FederationHttpBinding, so I’ve done this in the client code (this is a temporary measure for development only!) –

            proxy.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None;

            proxy.ClientCredentials.ServiceCertificate.Authentication.RevocationMode = X509RevocationMode.NoCheck;

 

The current version of the Geneva Framework, unlike Zermatt before it, does not support the userNameAuthentication element in the serviceCredetial service behaviour. (to be accurate you can kind of force it to do so, but that’s planned to be blocked in the near future, so for all intents and purposes you should not include this element, see more information in the thread mentioned above)

In order to implement authentication a customer SecurityTokenHandler needs to be added; to do so created a class that inherits from WindowsUserNameSecurityTokenHandler and overridden the ValidateToken method; again – several samples of such implementation exist on the thread but the idea is to validate the username and password (made available through the SecurityToken parameter to the method) in whatever way you wish and then, ideally, add some claims to the ClaimsIdentityCollection output; this should generally include the identity, authentication method and authentication instant, but you can add whatever you wish.

To wire the custom handler to the STS service a bit more configuration is required on the STS side -

  <microsoft.identityModel>

    <securityTokenHandlers>

      <remove type="Microsoft.IdentityModel.Tokens.WindowsUserNameSecurityTokenHandler, Microsoft.IdentityModel,Version=0.5.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

      <add type="MyUserNameSecurityTokenHandler, MyUserNameSecurityTokenHandlerAsembly"/>

    </securityTokenHandlers>

  </microsoft.identityModel>

This replaces the built-in WindowsUserNameSecurityTokenHandelr with my class that inherits from WindowsUserNameSecurityTokenHandler and adds custom implementation.

Note: I needed to add the definition of this section as such –

<section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel,Version=0.5.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

I hope that makes sense….

Labels: ,

Thursday, November 06, 2008

From "Zermatt" to the "Geneva Framework" part II

A couple of days ago I've posted about the changes I've had to make to allow my custom STS to work with the updated Geneva framework. there's one more, quite crucial, change that I had to make, which I will try to describe next -

If my understanding is correct (and unfortunately there's all the chances in the world that it is not, so if you know otherwise please do comment) the October Geneva SDK has tightened security a little bit around token validation.

I believe that the previous version of SDK, the RP simply made sure that a token was included with the request, and that this token was signed by a party whose certificate exists on the server (and is accessible); the RP did not check which certificate was used to sign the token.

 

As far as I can tell the Geneva Framework SDK now behaves differently - if you execute the same code and configuration you had before (baring necessary changes to allow the code to compile on the new version, but these are mostly name changes) you will get the following error from the RP:

 

"An unsecured or incorrectly secured fault was received from the other party. See
the inner FaultException for the fault code and detail."

 

Basically the Client gets a token from the STS and attaches it to the request but the RP does not recognise the issuer of the token; in order to instruct the RP to accept tokens signed by a particular STS you need to provide it with a list of issuers you accetps, this can be done using the following configuration for example -

 

<microsoft.identityModel>
  <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=0.5.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
    <trustedIssuers>
      <add name="STS" thumbprint="7a0671d475673c1ab131ca1c0c804e4fbd385140"/>
    </trustedIssuers>
  </issuerNameRegistry>
</microsoft.identityModel>

 

This bit of configuration lists all the certificates that are acceptable as STS token signing.

 

It is interesting to note that this model is completely extensible - you can define your own registry IssuerNameRegistry type that would look and behave differently if you have other means of listing those; the same can also be done via code, which is the example provided with the SDK - you define a custom IssuerNameRegistryClass -

 

namespace ClaimsAwareWebService
{
    public class TrustedIssuerNameRegistry : IssuerNameRegistry
    {
        /// <summary>
        ///  Returns the issuer Name from the security token.
        /// </summary>
        /// <param name="securityToken">The security token that contains the STS's certificates.</param>
        /// <returns>The name of the issuer who signed the security token.</returns>
        public override string GetIssuerName( SecurityToken securityToken )
        {
            X509SecurityToken x509Token = securityToken as X509SecurityToken;
            if ( x509Token != null )
            {
                //Note: This piece of code is for illustrative purposes only. Validating certificates based on
                //subject name is not a good practice.  This code should not be used as is in production.
                if ( String.Equals( x509Token.Certificate.SubjectName.Name, "CN=STS" ) )
                {
                    return x509Token.Certificate.SubjectName.Name;
                }
            }

            throw new SecurityTokenException( "Untrusted issuer." );
        }
    }
}

 

and then when configuring the host for the RP service you provide this as a parameter -

 

FederatedServiceCredentials.ConfigureServiceHost(host, new TrustedIssuerNameRegistry());

 

And while I'm on the subject - as this has sent me going in circles - it appears that the framework is not happy with claim-less tokens, so if you're dumb enough (as I was) and end up not adding any claims (I was adding them base on the requested claims in the incoming request, which, at some point, was empty in my configuration) you will get a  error, which, after setting the ServiceDebugBehavior would read "A SamlAssertion requires at least one statement.  Ensure that you have added at least one SamlStatement to the SamlAssertion you are creating."

 

I can't decide about this one - does it not make sense to have a scenario in which you just want to get a signed token to indicate that an STS has authenticated the caller, but don't actually need any claims? not that it's a problem to find at least one claim to add (identity, authentication method are two easy examples), but speaking in principal I'm not yet convinced not having any specific claim should be an error.

Labels: ,

Tuesday, November 04, 2008

From "Zermatt" to the "Geneva Framework"

I have already mentioned that Zermatt has been renamed as the "Geneva Framework", which makes total sense.

At PDC Microsoft have released a new download for the "Geneva Framework", which I have downloaded today to check some of my code against;

While not at all an extensive list, here are the changes I had to do to my code to get it to work with the updated framework -

On the STS:

  • The SecureTokenService class, which is the base class for any STS implementation has moved to the main Microsoft.IdentityModel namespace (it formerly existed under it's own namespace - Microsoft.IdentityModel.Service)
  • The GetScope method of the SecureTokenService is now marked as abstract and so has to be implemented (I believe it previously was not abstract so a base implementation could have been used, either directly or indirectly through an overriding method;
  • ClaimsPrincipal no longer has a 'Current' property, you can get the claims principal from an IClaimsPrincipal instance using the CreateFromPrincipal method or from an IIdentity instance using the CreateFromIdentity method.
  • GetOutputSubjects renamed to GetOutputClaimsIdentity, the order of the parameters has changed a bit (but otherwise remained the same) and the return value is now IClaimsIdentity and not ClaimsIdentityCollection (which, again, makes perfect sense)
  • In the STS service configurationI have changed the bindings from wsHttpBinding to ws2007HttpBinding and the STS contract from IWSTrustFeb2005SyncContract to IWSTrust13SyncContract.

On the RP:

  • ExtensibleServiceCredentials, which is used to configure the RP's host to use the Geneva Framework is now called FederatedServiceCredentials
  • To get the list of Claims in the RP you no longer use something like "(IClaimsIdentity)ClaimsPrincipal.Current.Identity;" but instead check the CurrentPrincipal of the current thread - "IClaimsIdentity identity = Thread.CurrentPrincipal as IClaimsIdentity;"

Labels: ,

Sunday, November 02, 2008

Non-Optional Claims in the Geneva Framework

I'm currently doing some work with the Geneva Framework (formerly known as "Zermatt"), which I am very excited about;
With the SOA wave and now the coming Cloud wave, federated identity becomes a crucial component in the enterprise and it is great to see such a good story for it from Microsoft.

Using the "Zermatt" SDK (I now need to download the updated framework and align with it) I have succesfully, and quite simply, managed to create both an active STS scenario and a passive STS scenario, both sharing the same underlying STS code; this was a great experience and I hope to post some more details over the next few days.

I was, however, a little bit surprised by the behaviour of the framework around non-optional claims -

 

In my scenario the RP (=relaying party, the service the client actually want to call) indicates through its configuration that it requires a specific (custom) claims, which is not optional -

<security mode="Message">
  <message>
    <claimTypeRequirements>
      <add claimType="http://myCompany/claims/someClaim" isOptional="false"/>
      <add claimType=http://myCompany/claims/someOtherClaim isOptional="false"/>
    </claimTypeRequirements>
    <issuer address="http://localhost:6000/STS"/>
    <issuerMetadata address="http://localhost:6000/STS/mex"/>
  </message>
</security>

 

When the client adds a web reference to this service, it is correctly configured with the STS details and the required claims (not posted here, I will try and describe my scenario in detail in a separate post) and so when it calls the service, WCF ensures it first hits the STS requesting the claims as indicated in the config.

You would all probably know that when thinking about any aspect of security in WCF the story is very “tight”, in the sense that you could set up pretty much all the requirements in configuration should you wish to and you could trust that the service’s code will never get executed if these are not met; I believe this is a key design point for WCF - the implementer of the method should not need to worry about how authentication is implemented, nor should you need to change the code if you decide to change your authentication method.

Considering this I expected the STS to try and provide all the claims it can based on the request message and/or configuration for the RP, and then I would expect the channel on the RP side (using the "Geneva" Framework to reject any requests that arrived without all the non-optional claims BEFORE calling the service’s code.

When testing my scenario I deliberately set the STS code so that it does not provide the required and was surprised to find out this was not the case.
My service's method was called whether both claims existed or not; I did have, of course, full access to the claims in code and so it was fairly easy to validate the existence of the claims required, but this seemd a little misaligned with the WCF approach to all the other security aspects and quite wrong frankly.

I could not find much help online (this is still early days for the framework), and checking with a couple of people they all confirmed both my observation and my expectations; luckily for me, though, I was able to attend PDC and so I made sure to give a visit to the Identity folks' booth.

I'm happy to say that they as well have confirmed that the expectation is quite valid and indeed, they expect this behaviour to change before RTM; hopefully this will happen which would keep things nice and tidy.

Labels: , ,