Friday, February 4, 2011

ADFS, SAML and RelayState

I have been working with ADFS for a customer for som time now, and as a part of their pilot we are going to enable trust with a SaaS application that suports SAML WebSSO Post profile. The customer requires deep linking to conten from their intranet. The SaaS solution handles this by reading the RelayState parameter in the SAML POST.
Microsoft did not implement support for this part of the POST profile....
In this forum thread Collins gives an outline of the solution, but no actual code examples. As we needed this feature I had to write this piece of code (not the prettiest code ever writen, but it works in my case).
So, to the solution:
(As I said, it is not going to be pretty)
code is shown in italic

Code to add a cookie:
in "IdpInitiatedSignOn.aspx.cs" add the following line in the using section:

using System.Web;

And at the beginning of the method "Page_Init" add the following lines at the beginngin of the method:

string value = Context.Request.QueryString["RelayState"];
if(String.IsNullOrEmpty(value))
value=" ";
HttpCookie = cookie= new HttpCookie("RelayState", value);
Context.Response.Cookies.Add(cookie);

(It might be a good idea to enable error messages in web.config (a small typo might cause crashes)

Create a class library project in Visual Studio 2008, with a strong name.

add a class called: "RelayStateModule"

public class RelayStateModule:System.Web.IHttpModule
{
#region IHttpModule Members

public void Dispose()
{
_Context = null;
}
private System.Web.HttpApplication _Context;
private RelayStateFilter MyFilter;
public void Init(System.Web.HttpApplication context)
{
_Context = context;
_Context.AuthorizeRequest += new EventHandler(_Context_AuthorizeRequest);

}



void _Context_AuthorizeRequest(object sender, EventArgs e)
{
if (_Context.Request.Cookies.AllKeys.Contains("RelayState"))
{
var value = _Context.Request.Cookies["RelayState"].Value;
MyFilter = new RelayStateFilter(_Context.Response.Filter, value);
_Context.Response.Filter = MyFilter;
}
}

#endregion
}

Add a class called: "RelayStateFilter"

public class RelayStateFilter : Stream
{

private Stream ParentFilter;
private StreamWriter streamWriter;
private string _RelayState;
public RelayStateFilter(Stream filter,string relayState)
{
_RelayState = relayState;
ParentFilter = filter;
streamWriter = new StreamWriter(ParentFilter);
}
public override void Write(byte[] buffer, int offset, int count)
{

MemoryStream ms = new MemoryStream(buffer, offset, count, false);
StreamReader sr = new StreamReader(ms, System.Text.Encoding.UTF8);

string s;
bool isSAMLResponse = false;
StringBuilder sb = new StringBuilder();
int i = 0;
int lineNumberToInsertRS = -1;
while ((s = sr.ReadLine()) != null)
{
i++;
if (s.Contains("SAMLResponse"))
{
isSAMLResponse = true;
}
if (isSAMLResponse && s.Contains("submit"))
{
lineNumberToInsertRS = i;
var rsIncluded = InsertRSIntoResponseString(s, _RelayState);

sb.AppendLine(rsIncluded);
streamWriter.WriteLine(rsIncluded);
}
else
{
sb.AppendLine(s);
streamWriter.WriteLine(s);
}
}
sb.AppendLine("number of lines="+i.ToString());
//For debugging purposes, remove from production code.
if (isSAMLResponse)
{
using (var fs = File.Open(@"c:\temp\lastToken.txt", FileMode.Create))
{
//fs.w
var sr2 = new StreamWriter(fs);

sr2.Write(sb.ToString());
sr2.Close();
}
}
streamWriter.Flush();

}

private string InsertRSIntoResponseString(string s, string _RelayState)
{
//Finding location of Submit action
var loc = s.IndexOf("<noscript>");
return s.Insert(loc,String.Format("<input type=\"hidden\" name=\"RelayState\" value=\>"{0}\" /", _RelayState)); //
}
#region Rest of Stream's function - our stream is write-only

public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}

public override bool CanRead
{ get { return false; } }

public override bool CanSeek
{ get { return false; } }

public override bool CanWrite
{ get { return true; } }

public override long Length
{ get { throw new NotSupportedException(); } }

public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}

public override void Flush()
{
ParentFilter.Flush();
}

public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}

public override void SetLength(long value)
{
throw new NotSupportedException();
}

#endregion
}

Compile and get the full name of your new assembly.

Create a new folder in the adfs\ls directory called "bin" and place your assembly there.
Then you need to register your assembly in web.config:

And at last you register the module in IIS Manager under the ls web site.
There you create a managed module called RelayStateFilter
And under type you have to refer your RelayStateModule
{your namespace}.RelayStateModule

hopefully you will have your RelayState in the SAML message to your RP.