Introduction
I work as a developer in a small company. A few months ago, I was asked to use ADF2.0 with SAML tokens to authenticate desktop app<->service communication. I didn't know much back then about the technology and started to look up some details.
I discovered that SAML tokens are not the best idea for authentication and they don't really have a good support. So I tried to convince them to use JWT tokens. The main reason for using them is because they have better support in existing products and they weigh less because they are JSON objects while SAML is XML. It didn't work because my company already had an IdentityProvider
that used SAML tokens.
While I had a few examples in my company on how to write and configure services to communicate with browsers with ADFS and SAML tokens, I had no luck finding a full example on how to do it with a desktop application written in C#.
Background
My main questions were:
- How to acquire the tokens on the desktop app in C#?
- How to send the token each time?
- How to verify the token on the service?
While I was searching, the first 2 weren't actually that hard to find on the internet.
The first question led me to these sites and many more (those I found on my GMAIL history):
There is a lot of information about how to request with C# the Identity token for your domain STS, how to send the token to another domain's STS and request a token that will work in the other domain.
For the question of how to send the token to the service, I usually found the answer:
- Put it in the Authorization header like so: "SAML " + tokenXML
I did exactly that but my configured ASP.NET service didn't answer my requests. I thought did something wrong but when I used the same service with Chrome it just worked, I could see the token with Chrome's developer tools and could see the communication with both of the STS on both domains, and the service replied. It worked because the configured service uses redirection and Chrome does all the magic by itself.
So I searched for answers every where but couldn't find any.
So finally I started reading about the service side. I backed up my web.config file, ran the WIF tool to configure my service for secure communication using certificates and tokens and compared the new web.config with the old one. After seeing what was added, I started looking up about all the modules and what their responsibility is, disabling them and watching how the good Chrome will react after each and every model removal.
After a few hours, I found the thing I was looking for, the WSFederationAuthenticationModule
is the module that makes all the magic happen. So I decided to look what it does with the debug tool. I extended the module and used my new module in the service. When extending the module, I have overridden every single method I could and put a breakpoint in every one of them. While doing so, I began to look more and more convinced that I'm on the right track.
So after a few hours of debugging, I made the appropriate changes and made my module look for the token I was sending through the Authorization header. While debugging, I've noticed that while communicating with Chrome, the token wasn't in the header but came with the cookie that the service itself gave to the app after the first correct authentication. So after a few changes, I was able to make the service authenticate with my app and keep the same behaviour with Chrome.
Using the Code
So basically, all you have to do is create the module from down here, replace the WSFederationAuthenticationModule
in the web.config with your module and it will work like a charm.
public class MyWSFederationAuthenticationModule : WSFederationAuthenticationModule
{
#region Consts
private const string SAML_SCHEME = "SAML";
private const string AUTHORIZATION_HEADER = "Authorization";
#endregion
#region Public Methods
public override SignInResponseMessage GetSignInResponseMessage(HttpRequest request)
{
if (IsAuthorizationHeaderContainsSamlToken(request))
{
string authorizationHeader = GetAuthorizationHeader(request);
return new SignInResponseMessage(request.Url, authorizationHeader);
}
}
public override string GetXmlTokenFromMessage(SignInResponseMessage message,
WSFederationSerializer federationSerializer)
{
if (message.Result.StartsWith(SAML_SCHEME))
{
int tokenStartIndex = SAML_SCHEME.Length + 1;
return message.Result.Substring(tokenStartIndex);
}
return base.GetXmlTokenFromMessage(message, federationSerializer);
}
public override bool CanReadSignInResponse(HttpRequest request, bool onPage)
{
if (IsAuthorizationHeaderContainsSamlToken(request))
{
return true;
}
return base.CanReadSignInResponse(request, onPage);
}
#endregion
#region Protected Methods
protected override string GetReturnUrlFromResponse(HttpRequest request)
{
if (IsAuthorizationHeaderContainsSamltoken(request))
{
return request.RawUrl;
}
return base.GetReturnUrlFromResponse(request);
}
#endregion
#region Private Methods
private bool HasAuthorizationHeader(HttpRequest request)
{
return request.Headers.AllKeys.Contains(AUTHORIZATION_HEADER);
}
private string GetAuthorizationHeader(HttpRequest request)
{
return request.Headers[AUTHORIZATION_HEADER];
}
private bool IsAuthorizationHeadercontainsSamlToken(HttpRequest request)
{
if (!HasAuthorizationHeader(request))
{
return false;
}
string authorizationHeader = GetAuthorizationHeader(request);
if (string.IsNullOrEmpty(authorizationHeader))
{
return false;
}
return authorizationHeader.StartsWith(string.Format("{0} ", SAML_SCHEME)) &&
(authorizationHeader.Length > SAML_SCHEME.Length);
}
#endregion
}
I think the code is self explanatory so almost no comments were made. If you don't understand something, you are more than welcome to ask.
Hope I helped at least a few. =]