IdP-initiated SSO using WIF

| | Comments (0) | TrackBacks (1)
After quite a bit of struggle that stemmed from my improper serialization of the SAML token and its digital signature (every byte matters!), I was able to concoct a SAML message using WIF that I was then able to submit to PingFederate 6.3. Once my whitespace was where it needed to be, PingFederate happily accepted my IdP-initiated SSO message :)

This code isn't rocket science, but it might save you a bit of time. (Though it's not groud breaking, keep in mind that I'm the copyright holder. You're free to use it under the turns of the GPL, which all code I post on my blog is governed by unless stated otherwise). If you have questions, shoot them my way.

Web Form
<%@ Page Language="C#" AutoEventWireup="true" 
CodeFile=
"Default.aspx.cs" Inherits="_Default" %>

<html>
<head><title>IdP-initiated SSO using WIF</title></head>
<body>
<form id="form1" runat="server" action="https://localhost:9031/sp/ACS.saml2">
<input type="text" style="width: 400px" name="RelayState"
value="http://localhost/SpSample/?foo=bar" />
<input type="hidden" name="SAMLResponse" id="SAMLResponse" runat="server" />
<input type="submit"/>
</form>
</body>
</html>

Web Form's Code Behind

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Xml;
using Microsoft.IdentityModel.Claims;
using Microsoft.IdentityModel.Protocols.WSTrust;
using Microsoft.IdentityModel.SecurityTokenService;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Tokens.Saml2;

using SecurityTokenTypes = Microsoft.IdentityModel.Tokens.SecurityTokenTypes;

public partial class _Default : System.Web.UI.Page
{
    #region Configuration Information

    private const int tokenLifetime = 1// In minutes.
    private const string issuer = "localhost:default:idp:entityId";
    private const string appliesTo = "localhost:default:sp:entityId"// Audience restriction
    protected const string assertionConsumerEndpoint = "https://localhost:9031/sp/ACS.saml2";
    private const string signingCertCommonName = "CN=Travis";
    private static readonly Dictionary<stringstring> claimDescriptors = new Dictionary<string,string
    { 
        { "FooUrl""https://localhost/SpSample/?foo=bar" },
        { ClaimTypes.Anonymous, "33" },
        { ClaimTypes.NameIdentifier, "joe" },
    };

    #endregion

    protected void Page_Load(object sender, EventArgs e)
    {
        var samlResponse = CreateSamlResponse();

        SAMLResponse.Value = Convert.ToBase64String(Encoding.UTF8.GetBytes(samlResponse));
    }

    private string CreateSamlResponse()
    {
        var claims = CreateClaims();
        var tokenHandler = new Saml2SecurityTokenHandler(); ;
        var token = CreateToken(claims, tokenHandler);

        return CreateSamlResponseXml(tokenHandler, token);
    }

    private static Saml2SecurityToken CreateToken(IEnumerable<Claim> claims,
        Saml2SecurityTokenHandler tokenHandler)
    {
        var descriptor = CreateTokenDescriptor(claims);
        var token = tokenHandler.CreateToken(descriptor) as Saml2SecurityToken;        

        AddAuthenticationStatement(token);
        AddConfirmationData(token);               

        return token;
    }

    private static void AddConfirmationData(Saml2SecurityToken token)
    {
        var confirmationData = new Saml2SubjectConfirmationData
        {
            Recipient = new Uri(assertionConsumerEndpoint),
            NotOnOrAfter = DateTime.UtcNow.AddMinutes(tokenLifetime),
        };

        token.Assertion.Subject.SubjectConfirmations.Add(new Saml2SubjectConfirmation(
            Saml2Constants.ConfirmationMethods.Bearer, confirmationData));
    }

    private static void AddAuthenticationStatement(Saml2SecurityToken token)
    {
        // Chage to "urn:oasis:names:tc:SAML:2.0:ac:classes:Password" or something.
        var authenticationMethod = "urn:none";

        var authenticationContext = new Saml2AuthenticationContext(new Uri(authenticationMethod));
        var authenticationStatement = new Saml2AuthenticationStatement(authenticationContext);

        token.Assertion.Statements.Add(authenticationStatement);
    }

    private static string CreateSamlResponseXml(Saml2SecurityTokenHandler tokenHandler, Saml2SecurityToken token)
    {
        var buffer = new StringBuilder();

        using (var stringWriter = new StringWriter(buffer))
        using (var xmlWriter = XmlWriter.Create(stringWriter, new XmlWriterSettings()))
        {
            xmlWriter.WriteStartElement("Response""urn:oasis:names:tc:SAML:2.0:protocol");
            xmlWriter.WriteAttributeString("IssueInstant", DateTime.UtcNow.ToString("s"));
            xmlWriter.WriteAttributeString("ID""_" + Guid.NewGuid());
            xmlWriter.WriteAttributeString("Version""2.0");

            xmlWriter.WriteStartElement("Status");
            xmlWriter.WriteStartElement("StatusCode");
            xmlWriter.WriteAttributeString("Value""urn:oasis:names:tc:SAML:2.0:status:Success");
            xmlWriter.WriteEndElement();            
            xmlWriter.WriteEndElement();            

            tokenHandler.WriteToken(xmlWriter, token);

            xmlWriter.WriteEndElement();
        }

        return buffer.ToString();
    }

    private static SecurityTokenDescriptor CreateTokenDescriptor(IEnumerable<Claim> claims)
    {
        var descriptor = new SecurityTokenDescriptor()
        {
            TokenType = SecurityTokenTypes.OasisWssSaml2TokenProfile11,
            Lifetime = new Lifetime(DateTime.UtcNow, DateTime.UtcNow.AddMinutes(1)),
            AppliesToAddress = appliesTo,
            TokenIssuerName = issuer,
            Subject = new ClaimsIdentity(claims),
            SigningCredentials = GetSigningCredentials(),            
        };       

        return descriptor;
    }

    private static SigningCredentials GetSigningCredentials()
    {
        var signingCert = CertificateUtil.GetCertificate(StoreName.My, 
            StoreLocation.CurrentUser, signingCertCommonName);

        return new X509SigningCredentials(signingCert);
    }

    private static IEnumerable<Claim> CreateClaims()
    {
        foreach (var claimDescriptor in claimDescriptors)
        {
            yield return new Claim(claimDescriptor.Key, claimDescriptor.Value);
        }
    }
}