877-277-2477
info@capstonecourseware.com

OpenSAML Examples

Will Provost

August 14, 2009

In this article I present a few simple examples of managing SAML 2.0 content using the OpenSAML library, version 2.2.3. I have found OpenSAML to be an excellent tool, but lacking somewhat for documentation and especially for code examples. I hope that the code here will be helpful: it is beginner-level stuff, and the aim is that it will clarify some of the fundamental practices for those just getting familiar with the library.

All code here is drawn from my course, Securing Java Web Services, in which OpenSAML -- and SAML generally -- are part of a bigger picture that includes XML cryptography, WS-Security, WS-SecurityPolicy, and other standards, as well as other tools including Metro/WSIT and OpenSSO.

Having explained what it is, I want also to be clear about what this article is not. It's not a tutorial, either on SAML or OpenSAML. I also am not claiming any sort of blessing from the OpenSAML project or its authors; for all I know they would develop splitting headaches upon seeing the code here.

And, to that point: not all the code here takes full advantage of all the tools available within OpenSAML. One item that comes to mind is that I use classic JAXP code to conjure a DOM document builder; OpenSAML implements a parser pool, and for most purposes that will be a better choice.

You get the idea. Mostly, as I've been craving examples along these lines and had to build them myself, I thought they might prove useful to others. It's no more or less than that.

Getting the Code

Okay, let's get to it: to see the source files, build them, and test them, download one of the following ZIP files, and unpack it anywhere you like:

  • A small archive (~100k) with just the source files, make files, and javadocs -- choose this only if you already have OpenSAML 2.2.3, and the necessary SLF JAR(s) that are required at runtime, and are prepared to copy JAR files into place to get the build working; or if you just want a look at the code and don't need to build it
  • A much larger archive (~10meg) that includes the required OpenSAML JARs in the right places for the build -- I recommend this in spite of the more significant download time, as it builds and runs right out of the box

You'll see a simple tree /OpenSAML that forms a small Java code project, including an Ant build.xml. Source files are found in the src directory, and javadocs in doc. If you chose the larger bundle to download, then the required OpenSAML JARs are already in place in lib, and the Xerces parser and supporting JARs (on which OpenSAML insists at runtime) is in endorsed.

If you got the little archive, copy the following files from your OpenSAML distribution:

  • Make a subdirectory lib and copy in:
    • bcprov-ext-jdk15-1.40.jar
    • commons-codec-1.3.jar
    • commons-collections-3.1.jar
    • commons-httpclient-3.1.jar
    • commons-lang-2.1.jar
    • jargs-1.0.jar
    • jcip-annotations-1.0.jar
    • jcl-over-slf4j-1.5.5.jar
    • joda-time-1.5.2.jar
    • log4j-over-slf4j-1.5.5.jar
    • not-yet-commons-ssl-0.3.9.jar
    • opensaml-2.2.3.jar
    • openws-1.2.2.jar
    • slf4j-api-1.5.6.jar
    • slf4j-jdk14-1.5.6.jar
    • slf4j-nop-1.5.6.jar
    • velocity-1.5.jar
    • xmlsec-1.4.2.jar
    • xmltooling-1.2.0.jar
  • Make a subdirectory endorsed and copy in:
    • resolver-2.9.1.jar
    • serializer-2.9.1.jar
    • xalan-2.7.1.jar
    • xercesImpl-2.9.1.jar
    • xml-apis-2.9.1.jar

In any case, you'll also see several subdirectories with example SAML content (which was produced using these Java applications).

Also, to build and run, the example code requires two supporting tools:

Setup

Be sure that your executable path includes the Java bin directory and the Ant bin directory.

You can then build the code project by running ant from the OpenSAML directory. You'll see a new build directory with class files.

Code Design

UML diagramA handful of Java classes in package cc.saml are organized in an inheritance hierarchy, and each of these has a main method for testing out some part of the code:

  • SAML.java is a general utility for working with OpenSAML. Its main method doesn't do much that's all that interesting: it can read and re-write a file with SAML content. But methods here are useful for many common tasks, such as building a <Subject> or converting between XMLObject and DOM representations.
  • SAMLAssertion.java lets you invoke methods each of which either reads or writes a certain kind of SAML assertion: authentication, attribute, or authorization-decision. This and the remaining subclasses are less generally useful, but serve as decent coding examples and might even be reusable via copy-and-paste.
  • SAMLProtocol.java follows the pattern of SAMLAssertion.java, but writes queries and responses of those same three types.
  • SAMLBinding.java does pretty much the same thing but wraps its output in a SOAP envelope and body according to the SAML 2.0 SOAP binding.

There is also a SAMLSignature.java, which we don't discuss further in this article because it isn't really an OpenSAML example. It's just DOM code that signs a SAML assertion, request, or response, and can verify a signature and check a pre-set trust store as well. It uses the SAML.java utility, but only for testing purposes via its own main method.

Packages cc.security and cc.xml provide support for, respectively: reading keystores and trust stores, and pretty-printing XML output.

We've not bundled the javadoc, but you can create it by running makejavadoc.bat (or run similar commands on non-Windows systems). This will put the docs in a subdirectory doc.

General Utilities

cc.saml.SAML can serve a few possible purposes, and we'll look at the code in sections accordingly. Skip over the main method for now, and we'll come back to it when we're ready to test the code. First, it wraps the default bootstrapping of OpenSAML such that any load of this class assures that OpenSAML is ready to roll. See the static initializer block for this, and also notice that it initializes an ID generator.

DefaultBootstrap.bootstrap ();
generator = new SecureRandomIdentifierGenerator ();
    

Instances of this class can be used to build new SAML content, and such instances can be created with an issuer URL that will automatically be used in many of the utility methods. See the constructors for this; they also initialize a private document builder. (See the earlier note about the option of using an OpenSAML parser pool.)

The next method is one of the most generally useful: it's a shortcut to create XML objects using the XML toolkit embedded in OpenSAML. This system is robust, flexible, and extensible; it is also a bit of work to use for most purposes, and so the create method simplifies the programming model in a small way, by assuming one QName for the schema type and resulting element, and by getting the builder and creating the object, all in one swoop:

@SuppressWarnings ("unchecked")
public <T> T create (Class<T> cls, QName qname)
{
  return (T) ((XMLObjectBuilder)
  Configuration.getBuilderFactory ().getBuilder (qname)).buildObject (qname);
}
    

We'll see usage examples very soon.

The next set of methods wraps uses of the toolkit's marshaller and unmarshaller system, again wrapping the behavior of finding the right marshaller/unmarshaller for a given object or element, and doing the requested marshalling/unmarshalling. In the order found in the source code these are addToElement, asDOMDocument, printToFile, fromElement, and readFromFile. Each is useful for a slightly different situation.

Next are a set of factory methods for progressively larger chunks of SAML content, starting with a method to create an <Issuer> element from the issuer URL with which the utility object was created. Here too is our first example of creating new XMLObjects from scratch, using the create method:

    
result = create (Issuer.class, Issuer.DEFAULT_ELEMENT_NAME);
result.setValue (issuerURL);
    

The next method creates a complete subject structure, based on a name, name format, and confirmation method. Only the name is required; the confirmation method can be null, sender-vouches, or bearer, while HOK would be a bad idea since this method isn't built to take the necessary key information and pack it into the XML structure. In terms of technique, createSubject doesn't do anything we haven't already seen: it's mostly creating new XMLObjects of specific types and then calling mutators on those objects to assemble the tree. (OpenSAML's design is really nice and intuitive here, and if you know the SAML structure you want to assemble, it's usually quick work to find the right methods and arguments to put it together.)

public Subject createSubject
  (String username, String format, String confirmationMethod)
{
  NameID nameID = create (NameID.class, NameID.DEFAULT_ELEMENT_NAME);
  nameID.setValue (username);
  if (format != null)
    nameID.setFormat (format);

  Subject subject = create (Subject.class, Subject.DEFAULT_ELEMENT_NAME);
  subject.setNameID (nameID);

  if (confirmationMethod != null)
  {
    SubjectConfirmation confirmation = create 
      (SubjectConfirmation.class,  SubjectConfirmation.DEFAULT_ELEMENT_NAME);
    confirmation.setMethod (CM_PREFIX + confirmationMethod);

    subject.getSubjectConfirmations ().add (confirmation);
  }

  return subject;    
}
    

The next set of methods create assertions and responses, given various pieces and parts. More of the same techniques here, really, though the number of overloads of createResponse might be a surprise. Some of these, keep in mind, are for producing error responses, and then some will work with an inResponseTo parameter and bake that into the XML structure.

Finally there's a set of methods that create specific assertion types. These are narrower in their purpose (and are aimed at specific lab exercises in the training course whence this code comes). createAttributeAssertion can either build a list of attributes from a simple name-value map, or can leave this list empty to be built using the addAttribute helper method. Note the code comments in this latter method that explain the use of the XSAny type in lieu of AttributeValue.

We can test out some of these methods from this class' main method. Try the following, some of which uses prepared files in the Assertion subdirectory:

    
run SAML
Usage: java cc.saml.SAML <inputFile> [<outputFile>]

run SAML Assertion/Authn.xml Rewritten.xml
type Rewritten.xml
<?xml version="1.0" encoding="UTF-8" ?>

<saml:Assertion
  xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
  ID="Assertion12345789"
  IssueInstant="2009-07-15T15:42:36.750Z"
  Version="2.0"
>
  <saml:Issuer>http://mycom.com/MyJavaAuthnService</saml:Issuer>
  <saml:Subject>
    <saml:NameID>harold_dt</saml:NameID>
  </saml:Subject>
  <saml:AuthnStatement>
    <saml:AuthnContext>
    <saml:AuthnContextClassRef>
      urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
    </saml:AuthnContextClassRef>
    </saml:AuthnContext>
  </saml:AuthnStatement>
</saml:Assertion>

run SAML Assertion/Authn.xml
<?xml version="1.0" encoding="UTF-8" ?>
<saml:Assertion
  xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
  ID="Assertion12345789"
  IssueInstant="2009-07-15T15:42:36.750Z"
  Version="2.0"
>
  <saml:Issuer>http://mycom.com/MyJavaAuthnService</saml:Issuer>
  <saml:Subject>
    <saml:NameID>harold_dt</saml:NameID>
  </saml:Subject>
  <saml:AuthnStatement>
    <saml:AuthnContext>
    <saml:AuthnContextClassRef>
      urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
    </saml:AuthnContextClassRef>
    </saml:AuthnContext>
  </saml:AuthnStatement>
</saml:Assertion>
    

Two final tests drive the createAuthnAssertion and createAttributeAssertion methods; they are not shown in the usage statement. Each involves some hard-coded values and generates a pre-defined assertion directly to the console:

run SAML generate authn
<?xml version="1.0" encoding="UTF-8" ?>

<saml:Assertion
  xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
  ID="_76e026126bc35c0f825eced51d6bd43b"
  IssueInstant="2009-08-13T19:54:55.562Z"
  Version="2.0"
>
  <saml:Issuer>http://saml.r.us/AssertingParty</saml:Issuer>
  <saml:Subject>
    <saml:NameID>harold_dt</saml:NameID>
    <saml:SubjectConfirmation
      Method="urn:oasis:names:tc:SAML:2.0:cm:sender-vouches"
    />
  </saml:Subject>
  <saml:Conditions
    NotBefore="2009-08-13T19:54:45.562Z"
    NotOnOrAfter="2009-08-13T20:24:55.562Z"
  />
  <saml:AuthnStatement>
    <saml:AuthnContext>
      <saml:AuthnContextClassRef>
        urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
      </saml:AuthnContextClassRef>
    </saml:AuthnContext>
  </saml:AuthnStatement>
</saml:Assertion>

run SAML generate attr
<?xml version="1.0" encoding="UTF-8" ?>

<saml:Assertion
  xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
  ID="_706381eabe57febe9a041d0119850f41"
  IssueInstant="2009-08-13T19:57:39.171Z"
  Version="2.0"
>
  <saml:Issuer>http://saml.r.us/AssertingParty</saml:Issuer>
  <saml:Subject>
    <saml:NameID
      Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
    >louisdraper@abc.gov</saml:NameID>
  </saml:Subject>
  <saml:Conditions
    NotBefore="2009-08-13T19:57:29.171Z"
    NotOnOrAfter="2009-08-13T20:27:39.171Z"
  />
  <saml:AttributeStatement>
    <saml:Attribute
      Name="securityClearance"
    >
      <saml:AttributeValue>C2</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute
      Name="roles"
    >
      <saml:AttributeValue>editor,reviewer</saml:AttributeValue>
    </saml:Attribute>
  </saml:AttributeStatement>
</saml:Assertion>
    

Reading and Writing Assertions

cc.saml.SAMLAssertion extends cc.saml.SAML, but in practice it doesn't use much of the base class code, instead offering individual methods for reading and writing assertions that do the same sorts of things, but that are more self-contained, and possibly more readable as start-to-finish examples of building or parsing SAML structures. They do rely on the base class for readFromFile and printToFile methods.

The createXXXAssertion methods don't do anything we haven't already seen. But the reading methods are new, if not all that surprising. Consider readAuthenticationAssertion, which expects the contents of a given file to be an authentication assertion and simply reads out the name, name format, and authentication context classes that it finds there:

public void readAuthnAssertion (String filename)
  throws Exception
{
  Assertion assertion = (Assertion) readFromFile (filename);
  NameID nameID = assertion.getSubject ().getNameID ();

  System.out.println ("Assertion issued by " +
    assertion.getIssuer ().getValue ());
  System.out.println ("Subject name: " + nameID.getValue ());
  System.out.println ("  (Format " + nameID.getFormat () + ")");

  System.out.println ("Authentication context classes found:");
  for (Statement statement : assertion.getStatements ())
    if (statement instanceof AuthnStatement)
      System.out.println ("  " + ((AuthnStatement) statement)
        .getAuthnContext ().getAuthnContextClassRef ()
        .getAuthnContextClassRef ());
}
    

Note that we get the character content of an Issuer by calling getValue, which is actually a method on the base type NameIDType. Also, while most assertions will have but one statement, it's legal to have several, and so OpenSAML follows the assertions schema by exposing a List<Statement>, and we loop over that, looking for an instance of the type that we're actually prepared to parse.

A few tests:

run SAMLAssertion
Usage: java cc.saml.SAMLAssertion
  <write|read> <authn|attr|authz> <filename>

run SAMLAssertion read authn Assertion/Authn.xml
Assertion issued by http://mycom.com/MyJavaAuthnService
Subject name: harold_dt
  (Format null)
Authentication context classes found:
  urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport

run SAMLAssertion read attr Assertion/Attribute.xml
Assertion issued by http://mycom.com/MyJavaAttributeService
Subject name: ga489Slge8+0nio9=
  (Format urn:oasis:names:tc:SAML:2.0:nameid-format:transient)
Attributes found:
  FullName: William Whitford Provost
  JobTitle: Grand Poobah

run SAMLAssertion write attr
<?xml version="1.0" encoding="UTF-8" ?>

<saml:Assertion
  xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
  ID="Assertion12345789"
  IssueInstant="2009-08-14T15:06:17.062Z"
  Version="2.0"
>
  <saml:Issuer>http://mycom.com/MyJavaAttributeService</saml:Issuer>
  <saml:Subject>
    <saml:NameID
      Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
    >ga489Slge8+0nio9=</saml:NameID>
  </saml:Subject>
  <saml:AttributeStatement>
    <saml:Attribute
      Name="FullName"
    >
      <saml:AttributeValue>William Whitford Provost</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute
      Name="JobTitle"
    >
      <saml:AttributeValue>Grand Poobah</saml:AttributeValue>
    </saml:Attribute>
  </saml:AttributeStatement>
</saml:Assertion>
    

Writing Queries and Responses

cc.saml.SAMLProtocol extends cc.saml.SAMLAssertion, and has its own methods for producing canned queries and responses. The SAML query model being distinct from the assertions model, this class produces its own queries by more or less the same approach as the createXXX methods in the base class.

For responses, we do a couple new things. First, we have a single method printResponse, which just wraps a given assertion; this is in keeping with the SAML response model, which unlike the query model doesn't try to invent much new content but just serves as a vehicle for assertions traveling from an asserting to a relying party. Second, the method allows for a filename to be given, which it will parse as representing a prior query. It will loosely simulate the actual process of responding to that query, by grabbing the query ID and making it the InResponseTo value.

Response response = createResponse (assertion);

Issuer issuer = create (Issuer.class, Issuer.DEFAULT_ELEMENT_NAME);
issuer.setValue ("http://somecom.com/SomeJavaAssertingParty");
response.setIssuer (issuer);

if (filename != null)
  try
  {
    RequestAbstractType query = (RequestAbstractType) 
      readFromFile (filename + QUERY_SUFFIX);
    response.setInResponseTo (query.getID ());
  }
  ...
    

Tests:

run SAMLProtocol
Usage: java cc.saml.SAMLProtocol
  <query|response> <authn|attr|authz> <simple-name>

run SAMLProtocol query authz AuthzTest
type AuthzTestQuery.xml
<?xml version="1.0" encoding="UTF-8" ?>

<samlp:AuthzDecisionQuery
  xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
  ID="AuthzQuery12345789"
  IssueInstant="2009-08-14T15:22:03.250Z"
  Resource="http://mycom.com/Repository/Private"
  Version="2.0"
>
  <saml:Issuer
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
  >http://somecom.com/SomeJavaRelyingParty</saml:Issuer>
  <saml:Subject
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
  >
    <saml:NameID>harold_dt</saml:NameID>
  </saml:Subject>
  <saml:Action
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    Namespace="urn:oasis:names:tc:SAML:1.0:action:rwedc"
  >read</saml:Action>
</samlp:AuthzDecisionQuery>

run SAMLProtocol response authz AuthzTest
type AuthzTestResponse.xml
<?xml version="1.0" encoding="UTF-8" ?>

<samlp:Response
  xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
  ID="_11ff951ee156e8ad0d04e000013cac68"
  InResponseTo="AuthzQuery12345789"
  IssueInstant="2009-08-14T15:22:13.531Z"
  Version="2.0"
>
  <saml:Issuer
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
  >http://somecom.com/SomeJavaAssertingParty</saml:Issuer>
  <samlp:Status>
    <samlp:StatusCode
      Value="urn:oasis:names:tc:SAML:2.0:status:Success"
    />
  </samlp:Status>
  <saml:Assertion
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="Assertion12345789"
    IssueInstant="2009-08-14T15:22:13.312Z"
    Version="2.0"
  >
    <saml:Issuer>http://mycom.com/MyJavaAuthorizationService</saml:Issuer>
    <saml:Subject>
      <saml:NameID
        Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
      >ga489Slge8+0nio9=</saml:NameID>
    </saml:Subject>
    <saml:AuthzDecisionStatement
      Decision="Permit"
      Resource="http://mycom.com/Repository/Private"
    >
      <saml:Action
        Namespace="urn:oasis:names:tc:SAML:1.0:action:rwedc"
      >read</saml:Action>
    </saml:AuthzDecisionStatement>
  </saml:Assertion>
</samlp:Response>
    

The SOAP Binding

cc.saml.SAMLBinding then extends SAMLProtocol in one simple way: it wraps the queries and responses in SOAP envelopes and bodies. Not much here is all that surprising; we even do the same response simulation, now expecting to encounter a SOAP envelope when reading into the given file for a query ID.

run SAMLBinding
Usage: java cc.saml.SAMLBinding
  <request|response> <authn|attr|authz> [<simple-name>]

run SAMLBinding request authn AuthnTest
run SAMLBinding response authn AuthnTest
type AuthnTestResponse.xml
<?xml version="1.0" encoding="UTF-8" ?>

<soap11:Envelope
  xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/"
>
  <soap11:Body>
    <samlp:Response
      xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
      ID="_b70e75ba90b00a7b8789087e6eb6c236"
      InResponseTo="AuthnQuery12345789"
      IssueInstant="2009-08-14T18:11:26.968Z"
      Version="2.0"
    >
      <saml:Issuer
        xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
      >http://somecom.com/SomeJavaAssertingParty</saml:Issuer>
      <samlp:Status>
        <samlp:StatusCode
          Value="urn:oasis:names:tc:SAML:2.0:status:Success"
        />
      </samlp:Status>
      <saml:Assertion
        xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
        ID="Assertion12345789"
        IssueInstant="2009-08-14T18:11:26.609Z"
        Version="2.0"
      >
        <saml:Issuer>http://mycom.com/MyJavaAuthnService</saml:Issuer>
        <saml:Subject>
          <saml:NameID>harold_dt</saml:NameID>
        </saml:Subject>
        <saml:AuthnStatement>
          <saml:AuthnContext>
            <saml:AuthnContextClassRef>
              urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
            </saml:AuthnContextClassRef>
          </saml:AuthnContext>
        </saml:AuthnStatement>
      </saml:Assertion>
    </samlp:Response>
  </soap11:Body>
</soap11:Envelope>
    

Contact

I hope this has been helpful. With any questions, suggestions, criticism, or other feedback, please write to me at provost@capcourse.com.