DarksideCookie

Come to the dark side...we have cookies!

Securing a NancyFx module with the Azure Access Control Service

In my previous post I gave a semi-quick introduction to NancyFx. This time, I want to take Nancy and combine it with Azure ACS. Not a very complicated thing as such, but still something I want to do as I enjoy working with both technologies.

Just as in the last post, I will self-host Nancy in a console application, and use NuGet to get it going. I will also re-use the “www.nancytesting.org” domain I set up in my hosts file in the last post.

Once I got my console application going with a host, and an empty NancyModule, it is time to start looking at the ACS.

The first thing I need to do is to set up a new ACS relying party. If you have not used the ACS before, I recommend reading up a bit on it to understand how it works. My own introduction might be useful.

In this case, I configure my Relying Party Application to use the Uri “http://www.nancytesting.org/” for both the realm and the return url. As for token format, I went with SAML 2.0 as it was the initially selected… I then select no token encryption, support for both Google and Live ID, and a new default rule group. When it comes to the token signing certificate, I add a certificate I have created on my own, which is also placed in the “Trusted People” container on my local machine… And then as a final thing, I go into the newly created rule group and generate the default rules… Now the ACS should be ready to handle my requests…time to start working on the Nancy end of things…

The first thing I set up is a default route, just to make sure everything works…

public class AcsModule : NancyModule
{
public AcsModule()
{
Get["/"] = parameters => "Hello World";
}
}

And as expected, pointing my browser to http://www.nancytesting.org/ returns “Hello World”…time to move on and secure that route using the ACS…

Lucky for me, Microsoft provides us with a lot of help when it comes to federated security. They do this by giving us the Windows Identity Foundation SDK. Unfortunately, that whole thing is expecting you to use ASP.NET. It seems to prefer you configuring it through a config file, and pretty much expects that authorization is managed through HttpModules. This is somewhat of a problem when it comes to Nancy. And if you think it is just a quick fix, you will soon realize that the way it works is heavily geared towards having an HttpRequest, which once again does not exist in Nancy… It also means that we are back to relying on System.Web, which Nancy does fine without, but that’s ok for this time…

So…the solution for me was to run through the code using .NET Reflector and figure out what is happening internally. Lucky for you, I have already done this, and I won’t run through all the stuff I found. Instead I will just go through how I got it to work.

It might be that it had been easier to read the documentation, but what fun would that be…

As I still want to use WIF, you have to have the SDK installed, and add a reference to Microsoft.IdentityModel to the project. But to keep all of the WIF stuff out of the way, I put it all in a static helper class called AcsHelper.

The interface for the AcsHelper class looks like this

public static class AcsHelper
{



public static void Configure(string audienceUri, string ns, string realm, string thumbPrint);

public static bool IsSignInResponse(dynamic form);
public static bool TryParseSignInResponse(Uri baseUri, Stream response, out SecurityToken token);
public static bool VerifyTokenXml(string tokenXml, out SecurityToken token);
public static string SerializeToken(SecurityToken token);
public static string GetLoginUrl();

}


Remember, this is just a quick spike to see that it works, so the code might not be perfect if we put it like that…but please bear with me as I break it down and have a look at the interesting pieces anyway.

Let’s start by looking at the Configure() method. This is where all the configuration of the WIF parts are made (duh…). It replaces the config file that is normally used by WIF.

private const string ACSLoginUrlFormat = "https://{0}.accesscontrol.windows.net:443/v2/wsfederation?wa=wsignin1.0&wtrealm={1}";
private static string _acsLoginUrl;
private static SecurityTokenHandlerConfiguration _securityTokenHandlerConfiguration;

public static void Configure(string audienceUri, string ns, string realm, string thumbPrint)
{
_acsLoginUrl = string.Format(ACSLoginUrlFormat, ns, HttpUtility.UrlEncode(realm));

_securityTokenHandlerConfiguration = new SecurityTokenHandlerConfiguration();
_securityTokenHandlerConfiguration.AudienceRestriction.AllowedAudienceUris.Add(new Uri(audienceUri));

var issuerNameRegistry = new ConfigurationBasedIssuerNameRegistry();
issuerNameRegistry.AddTrustedIssuer(thumbPrint, string.Format("https://{0}.accesscontrol.windows.net/", ns));
_securityTokenHandlerConfiguration.IssuerNameRegistry = issuerNameRegistry;
}

It starts out by creating and storing the Url to the login page in the ACS. It then creates a new instance of SecurityTokenHandlerConfiguration, which is then configured to accept the passed in audienceUri as an allowed audience.

Next a new IssureNameRegistry is created and added to the SecurityTokenHandlerConfiguration instance. In this case I use the ConfigurationBasedIssuerNameRegistry, which allows me to configure certificate thumbprints and issuer Urls manually. I configure it using the passed in certificate thumbprint and the Url to the ACS namespace.

Once that is done, the config should be done. If you have other requirements such as encrypted tokens or other token formats, then you would have to modify the configuration to suit your needs…

The next method is the IsSignInResponse, which is responsible for looking at a posted form and define whether the request is a sign in response from the ACS. It looks like this

public static bool IsSignInResponse(dynamic form)
{
return form["wa"] == "wsignin1.0";
}

So all it really does, is to look at the posted form and see if there is a posted value with the key “wa”. Simple, but effective…

The next method, TryParseSignInResponse() is a bit more complicated. It takes the response stream and converts it to a string. It then parses that string using the HttpUtility.ParseQueryString() to get to the form data in the form of a NameValueCollection. This can then be parsed by WIF and turned into a SignInResponseMessage, which in turn contains a bunch of XML that that can be turned into a RequestSecurityTokenResponse by using an instance of WSFederationSerializer. That response in turn contains some more XML that we pass to the next method called VerifyTokenXml().

So to make a long story short, we take the XML returned from the ACS and parse that using some classes from WIF to finally end up with some other XML, or rather a subset of the original XML, that we can then pass to another method to create a SecurityToken.

To be honest, I am not 100% sure what the layers of parsing here does (not a security guy that knows a whole lot about tokens and stuff), but the end result seems to just be a subset of the original XML. It might be possible to get to this by doing some XML manipulation on your own, but I thought I would rather do it like the people who wrote the WIF stuff intended me to do…

public static bool TryParseSignInResponse(Uri baseUri, Stream response, out SecurityToken token)
{
var responseString = new StreamReader(response).ReadToEnd();
var form = HttpUtility.ParseQueryString(responseString);
var responseMessage = (SignInResponseMessage)WSFederationMessage.CreateFromNameValueCollection(baseUri, form);
WSFederationSerializer federationSerializer;
using (XmlDictionaryReader r = XmlDictionaryReader.CreateTextReader(Encoding.UTF8.GetBytes(responseMessage.Result), new XmlDictionaryReaderQuotas()))
{
federationSerializer = new WSFederationSerializer(r);
}
var context = new WSTrustSerializationContext();
var tokenXml = federationSerializer.CreateResponse(responseMessage, context).RequestedSecurityToken.SecurityTokenXml.OuterXml;
return VerifyTokenXml(tokenXml, out token);
}

The VerifyTokenXml that gets passed the resulting XML does some more XML work to make sure that the token that is inside the XML can be understood and turned into a SecurityToken.

The SecurityToken is then used to create a new new ClaimsPrinciple, which is used to set the current principle on the Thread. A new type of token, a SessionSecurityToken, is then created and returned.

The reason for the second token is that it is a lot smaller than the original, so when it is set as a cookie, it doesn’t overflow the 4k limit.

public static bool VerifyTokenXml(string tokenXml, out SecurityToken token)
{
token = null;
try
{
using (var reader = XmlReader.Create(new StringReader(tokenXml)))
{
var securityTokenHandlers = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection(_securityTokenHandlerConfiguration);
if (securityTokenHandlers.CanReadToken(reader))
{
token = securityTokenHandlers.ReadToken(reader);
reader.Close();

var claims = securityTokenHandlers.ValidateToken(token);
var principal = ClaimsPrincipal.CreateFromIdentities(claims);
System.Threading.Thread.CurrentPrincipal = principal;

token = new SessionSecurityToken(principal, null, token.ValidFrom, token.ValidTo);
return true;
}
}
}
catch{}
return false;
}

Another little disclaimer here would be regarding setting the principle on the current thread… I assume that this thread comes off a thread pool somewhere. In a real world application, I would suggest resetting the principle at the end of the request so you don’t end up with a bogus principle when the thread is picked up in the future… But on the other hand, that would just be my assumption, and we all know that assumptions are the mother of all !¤% ups…

Ok, almost done with the WIF parts now. The last method worth looking at is the SerializeToken() method. It takes a SecurityToken and serializes it for storage in a cookie…

public static string SerializeToken(SecurityToken token)
{
var tokenString = new StringBuilder();
var xmlWriter = XmlWriter.Create(tokenString);
var securityTokenHandlers = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection(_securityTokenHandlerConfiguration);
securityTokenHandlers.WriteToken(xmlWriter, token);
return tokenString.ToString();
}

Ok, so what does all of this have to do with NancyFx? Didn’t I promise that I would use Nancy as well? Well, I guess it is time to do so…so back to the NanyModule I created earlier…

Currently it has a single route configured, but I do need a few more. First of all, the ACS will actually pass the token information to me using a POST. So I have to make sure that Nancy allows that POST. by adding an empty route for it.

I also need a login page. So I set up a GET-route for that, returning a sshtml-view containing some text saying that you have to login, and a login link. I pass the Url to the ACS login page as a model to the view…

And finally, I need to be able to sign out. I am not putting in a link for that in my “/” reply, but I will make it possible to log out if one knows the Url. So I add a another GET-route that empties out my security cookie and redirects back to the login page.

The actual redirect is done through an sshtml-view that redirects using a JavaScript. The reason for not just emptying the cookie while passing an HTTP 307 straight away is that some browsers have issues with this. Emptying out the cookie while passing a view that will do the redirect for me solves that issue…

Post["/"] = parameters => "";
Get[LoginPath] = parameters => View["LoginView.sshtml", new { Url = AcsHelper.GetLoginUrl() }];
Get["SignOut"] = parameters =>
{
var view = View["RedirectView.sshtml", new { Url = LoginPath }];
view.AddCookie(TokenCookieName,string.Empty);
return view;
};

I’m not too happy about the emptying of the cookie. I would much rather remove it, but I couldn’t find a way to do so using Nancy. So if you know how to do that, please let me know. Right now it creates an empty cookie, which is really annoying…

Ok, so where is the “magic” happening? Well, it is in the Before pipeline, which I have hooked up to point to a method that handles authentication…

The first thing I do there is to check if the request is for the login page. If it is, I just return. I can’t secure the login page, that would cause some issues…

Next, I use the AcsHelper to check if the request is a sign in response. If it is, I once again use the AcsHelper to parse the response. If that works, I set a cookie with the token in it, and redirect to “/” using the afore mentioned method… If it doesn’t, I send a redirect response, redirecting to the login page…

If it isn’t a sign in response, I make sure that the request carries a security token cookie, and that the cookie contains a valid token. If it doesn’t, I redirect the user to the login page… And if it does, I let the request through…

private Response OnBefore(NancyContext ctx)
{
if (ctx.Request.Path == LoginPath)
return null;

SecurityToken token;
if (AcsHelper.IsSignInResponse(ctx.Request.Form))
{
if (AcsHelper.TryParseSignInResponse(new Uri(Context.Request.Url.ToString()), Context.Request.Body, out token))
{
var view = View["RedirectView.sshtml", new { Url = "/" }];
view.AddCookie(TokenCookieName, AcsHelper.SerializeToken(token));
return view;
}
return Response.AsRedirect(LoginPath, RedirectResponse.RedirectType.Temporary);
}

if (!ctx.Request.Cookies.ContainsKey(TokenCookieName) || !AcsHelper.VerifyTokenXml(HttpUtility.UrlDecode(ctx.Request.Cookies[TokenCookieName]), out token))
{
return Response.AsRedirect(LoginPath, RedirectResponse.RedirectType.Temporary);
}

return null;
}

That’s it! An Azure ACS secured NancyModule…

The next step I want is obviously to roll this into a re-usable thing, and maybe integrate it a bit better with Nancy. But as this was, as previously mentioned, a quick spike, that will have to be pushed into the future. Maybe after I have had time to talk to @TheCodeJunkie about how to do that…

And as usual, code can be downloaded here: NancyACS.zip (642.03 kb)

Posted: May 08 2012, 20:50 by ZeroKoll | Comments (0) |
  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5
Filed under: .NET development | Security
Manage post: :)

Add comment




  Country flag
biuquote
Loading