September 2010 Archives

I've read the XACML spec a handful of times, discussed it w/ colleagues, attended workshops and presentations about it, and investigated various product that support it. After all my research, my conclusion is that the XACML specification and some entitlement management products built on top of it currently suffer from three major drawbacks that are impeding mass adoption:
  1. The wire is not defined.
  2. The attributes describing the subject presented to the PDP are not cryptographically bound to a trusted identity provider (IdP).
  3. The policy authoring story is way too technical.
By the first, I mean that the transport mechanism used to communicate with a PDP is not standardized. There is the SAML profile for XACML (PDF), but that's by no means enough. IMO, there needs to be many different profiles created before the protocol will reach critical mass -- a simple SOAP interface, one for JSON, OData, WS-Trust, etc., etc. Only after this happens will it become commonplace to find PEPs and PDPs from different companies communicating because custom integration work won't be required to do so. Each vendor will ship messages through a standardized and well-defined pipe.

Another problem is that the attributes that describe a subject are not cryptographically bound to a trusted IdP. According to the XACML spec, the PDP is presented with XML containing attributes that describe who the subject is. How is it supposed to know that this information is correct? Because it and the PEP are within a trusted subsystem? That's not going to cut it in many cases. Lots of times the PEP will present the PDP with information that it was given from an upstream entity, and the PDP will have to decide if access should be granted based on who asserted it. How can it do this unless the PEP provides more than strings? It can't. Crypto is needed.

The last problem with XACML is that the authoring experience of all the products on the market that I've looked at require the user to have a computer science degree and five years of software engineering experience to use them. (I'm exaggerating, but not much.) Policy authors in most organizations, I believe, are not engineers; they are business analysts and other non-technical folks.

Solutions to these Problems

First, the wire must be defined. Period. Gerry Gebel of Axiomatics said at CIS that it was his impression that the XACML technical committee (TC) has no interest in defining transport mechanisms. I really can't understand this. I would argue that this lack of definition will cause the market to view the spec as incomplete, immature, and unusable. The solution to this problem is to be at the table w/ the TC and persuade them.

The solution to the second problem is to include a digital signature computed by the IdP in the environment element of the request sent to the PDP. This way, it will be able to recompute the signature using the attributes presented by the PEP. If the PEP or any other entity between the PDP and the IdP has altered the attributes, the signature will not match, and the PDP won't allow access to the resource. How would this work in practice? I haven't thought about it enough to say, but I'm told that that's what IBM does in their XACML product.

The third problem can be solved with better authoring tools. As Anil Saldhana of Red Hat wrote last month, editors are needed that allow non-technical professionals to specify policy in the domain they are in. Using domain specific authoring tools, the policy creator won't know or care that XACML is the underlying technology. To them, it is a helpful tool that allows them to define rules that govern access to their organization's data using the nomenclature of their company and industry.

Conclusion

The XACML protocol is a good spec. It's easy to read and includes helpful examples and diagrams; however, it lacks definition of a key aspect -- the wire. With this and some good software engineering, IMHO, the spec and entitlements management products that are built on it will be more likely to receive mass adoption.
At the moment, I spend most of my time setting up federation connections for big banks. These financial institutions are super concerned about their image and brand, as any large organization is. One thing that they all worry about is what happens when an error occurs while a user is SSOing from them to their service provider (SP). It's not good enough that the SP brands their service with custom URLs to make it look like they are in the bank's domain. That's a given. If an error page is shown, it must be branded w/ their graphics, logos, colors, and text. Regardless of the minuscule chance of such an error occurring, the image of the bank must not be tarnished if it actually does.

To be specific, these are the cases I'm talking about:
  • The XML of the SAML message is malformed or invalid.
  • The digital signature on the SAML message or assertion is invalid.
  • The digital signature is missing.
  • The assertion or SAML message is expired.
  • The assertion has been submitted before.
  • The assertion can't be decrypted.
  • The audience restriction of the assertion is missing or incorrect.
  • The assertion is a holder-of-key (HoK) rather than bearer type.
To complicate things, some banks want to know exactly what went wrong. They want a different error code for each of these conditions printed on the page together with their toll-free support number. Others just want a generic SSO error code. Even more challenging, some banks want the user redirected back to their Web site while others are OK with the user being left on the SP's server (as long as the page is branded). The requirements are all over the map, and it's very difficult for an SP to accommodate all of these needs. Coming up w/ a solutions for this that works for all partners is really, really hard. Here's the best I've been able to do using PingFederate for just the SAML IdP-initiated SSO scenario.

PingFederate uses the Apache Velocity templating framework to render HTML pages, including those displayed when SSO errors occur. It also allows you to provide information that should be displayed when errors happen during SSO (both IdP- and SP-initiated though I'm only talking about the former). This info can be configured on a per federation connection basis. As shown in the following screenshot, it is usually just text.

pf_error_text.gif

The trick though is to use text and HTML, and to alter the Velocity template to output it as such.

The other trick is not really a trick; it's a dirty hack, but it's needed to meet the requirement that some IdPs have to display or pass back an error code indicating what exactly went wrong. (Please don't rail me for doing this if you're an enterprise architect.) To accomplish this, you have to parse the error message that is made available to you in the Velocity template. Based on that, you can set a variable and use it in the HTML configured for a connection. Specifically, you need to change the template named sp.sso.error.page.template.html which is located in $PINGFEDERATE_HOME/server/default/config/template. It needs to be something like this:

#set($m = $escape.escape($exception.getMessage()))

#if ($m.startsWith("Missing or invalid signature (INVALID") || $m == "Invalid signature")
    #set($errorCode = 0)
#elseif ($m.startsWith("Missing or invalid signature (NOT_PRESENT"))
    #set($errorCode = 1)
#elseif ($m.matches("(?sm).*Assertion with ID of.*has previously been processed.*"))
    #set($errorCode = 2)
#elseif ($m.matches("(?sm).*Time condition: now.*is on/after NotOnOrAfter.*"))
    #set($errorCode = 3)
#elseif ($m.matches("(?sm).*Assertion audience condition validation failed.*"))
    #set($errorCode = 4)
#else
    #set($errorCode = -1)
#end

#evaluate($errorText)

Nasty, I know, but stay w/ me. What this is doing is looking through the error message passed from PingFederate to the Velocity template used for SSO errors that happen on the SP side. The variable $errorText contains whatever is in the error message text box shown in the screenshot above. So, rather than the default text, you need to use some HTML like this:

pf_error_text_only.gif

Notice how the variable $errorCode is used in the configured HTML. Federation partner with config like this are those wanting to display the error code on the SP's server. Others that don't want to show the error code would be configured with HTML that doesn't use the variable at all. To render the value of the variable that is set in the previously shown template code, the Velocity function, #evaluate, is called. Using #evaluate requires the Velocity JAR to be upgraded unless you're using PingFederate 6.3 or higher. (If you do upgrade, use the JAR that contains all its dependencies.) For other partners that want the end user to be redirect back to their server, the HTML could contain a meta refresh tag or JavaScript to ship them over. If desired, the error code could be provided as a query string argument.

Not that this won't catch errors where the SAML message is malformed or invalid. PingFederate doesn't make the configured error message available to the template that's used to render errors in that case (to the best of my knowledge). It also doesn't handle incorrect HoK assertion types. (I didn't think of that case till writing this post.)

So, what we have here is a really ugly solution to an archaic use case that will have your enterprise architect hopping mad. Oh, well, at least the bank's image will be as pristine as possible when problems arise.
In my last post, I wrote about how to do IdP-initiated SSO using WIF. It was pretty easy stuff (once I got my digital signature's bytes where they belonged). From that post, you can see that it's not hard to generate a SAML assertion, put it in a form, and post it to a SAML service provider (SP). This can be done using ComponentSoft's SAML lib as well. It's equally as simple w/ one difference: The toolkit is all about SAML, so it exposes the nitty gritty parts of the protocol, making it easy to support more exotic use cases.

In my last example, the SAML response was more or less an envelope containing the SAML assertion. In some cases, you might want to do more w/ the response. For example, you might want to sign it, add a destination, an issuer, etc. You can absolutely do this w/ just WIF and .NET, but doing it w/ ComponentSoft's toolkit makes it really simple. To see how this might work, the previous example can be modified to use the following code to sign the SAML response rather than the assertion, serialize the message, etc.

using ComponentSoft.Saml2;
...

public partial class _Default2 : System.Web.UI.Page
{
    ...

    private string CreateSamlResponse()
    {
        var samlResponse = new ComponentSoft.Saml2.Response();
        var assertion = CreateAssertion();

        samlResponse.Assertions.Add(assertion);
        samlResponse.Status = new Status(SamlPrimaryStatusCode.Success, null);
        samlResponse.Issuer = new Issuer(issuer);
        samlResponse.Destination = assertionConsumerEndpoint;
        samlResponse.Sign(CertificateUtil.GetCertificate(StoreName.My,
            StoreLocation.CurrentUser, signingCertCommonName));

        return samlResponse.ToBase64String();
    }

    private Assertion CreateAssertion()
    {
        var userName = claimDescriptors[ClaimTypes.NameIdentifier];
        var subject = new Subject(new NameId(userName));
        var assertion = new Assertion
        {
            Issuer = new Issuer(issuer),
            Subject = subject,
        };

        AddConfirmationData(assertion);
        AddAuthenticationStatement(assertion);
        AddAttributeStatement(assertion);

        return assertion;
    }

    private void AddAttributeStatement(Assertion assertion)
    {
        var attributes = new AttributeStatement();

        foreach (var claim in claimDescriptors)
        {
            attributes.Attributes.Add(new Attribute(claim.Key, claim.Key, claim.Value, claim.Value));
        }

        assertion.Statements.Add(attributes);
    }

    private void AddAuthenticationStatement(Assertion assertion)
    {
        var authenticationMethod = "url:none";
        var authetnicationStatement = new AuthnStatement
        {
            AuthnContext = new AuthnContext
            {
                AuthnContextClassRef = new AuthnContextClassRef(authenticationMethod),
            },
        };
        
        assertion.Statements.Add(authetnicationStatement);
    }

    private static void AddConfirmationData(Assertion assertion)
    {        
        var subjectConfirmationData = new SubjectConfirmationData
        {
            Recipient = assertionConsumerEndpoint,
            NotOnOrAfter = System.DateTime.UtcNow.AddMinutes(tokenLifetime),
        };
        var subjectConfirmation = new SubjectConfirmation
        {
            Method = SamlSubjectConfirmationMethod.Bearer,
            SubjectConfirmationData = subjectConfirmationData,
        };
        var audienceRestriction = new AudienceRestriction();

        audienceRestriction.Audiences.Add(new Audience(appliesTo));
        assertion.Conditions = new Conditions();
        assertion.Conditions.ConditionsList.Add(audienceRestriction);

        assertion.Subject.SubjectConfirmations.Add(subjectConfirmation);
    }
}

So, there you have another way work w/ SAML in .NET. One additional thing about using ComponentSoft's toolkit that's nice is that it provides support for not only IdP-initiated SSO like I've shown here but also other use cases such as SP-initiated SSO, SLO, etc.
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);
        }
    }
}