SAML is an extensible protocol. Since it is based on XML, through the use of XML namespaces, custom elements and attributes can be inserted into the SAML messages at the appropriate places. Sometimes third party or custom SAML implementations will require particular custom elements or attributes to function.
In this example, we will suppose an IdP requires a custom <CompanyInfo> element included in the SAML extensions to provide the name of the company issuing the SAML request:
<samlp:Extensions xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> <CompanyInfo xmlns="http://example.com/samlext/1.0" CompanyName="Example Corporation" /> </samlp:Extensions>
(This case is based on a real customer scenario; I’ve changed the XML to simplify it and to respect that customer’s confidentiality.)
In 11.1.2.3 this is possible using OIFMessageProcessingPlugin. Note that only one plugin is allowed in your OAM environment, but since the plugin is passed information on each message (e.g. name of the partner it is for, whether it is incoming or outgoing, etc), you can use conditional logic in your plugin to do different things for different messages.
This tutorial assumes you have set up OAM Federation loopback test (LoopbackIDP/LoopbackSP) per the previous post. This enables us to initially test the plugin without requiring the third party server expecting the custom elements to be available. (Of course, once you believe you have it working without the third party server, you’d want to test it with them.)
DISCLAIMER: This sample code is just a start for your own development process, it is not production quality. This is a sample only, not officially supported by Oracle, use at your own risk.
Build the example plugin
We need our Java code for our plugin. Create a directory anywhere, let us refer to it as $PLUGINDEV. Then create the directory tree $PLUGINDEV/src/oracle/ateam/msgprocplugin and in that directory place the file SampleMsgProcPlugin.java with the following content:
package oracle.ateam.msgprocplugin; import java.io.*; import java.util.*; import javax.xml.parsers.*; import oracle.security.am.plugin.*; import oracle.security.fed.plugins.fed.msgprocessing.*; import org.w3c.dom.*; import org.w3c.dom.ls.*; import org.xml.sax.*; import static java.lang.System.err; public class SampleMsgProcPlugin extends OIFMessageProcessingPlugin { private boolean monitoringStatus; public ExecutionStatus process(MessageContext messageCtx) throws MessageProcessingException { try { String msg = ""; msg += "************************************\n"; msg += "* SAMPLE MESSAGE PROCESSING PLUGIN *\n"; msg += "************************************\n"; msg += "Partner Name: " + messageCtx.getPartnerName() + "\n"; msg += "Message Type: " + messageCtx.getMessageType() + "\n"; msg += "Message Version: " + messageCtx.getMessageVersion() + "\n"; msg += "User DN: " + messageCtx.getUserDN() + "\n"; msg += "User ID: " + messageCtx.getUserID() + "\n"; msg += "User ID Store: " + messageCtx.getUserIDStore() + "\n"; // Determine if this message meets our criteria for modification boolean matches = "LoopbackIDP".equals("" + messageCtx.getPartnerName()) && "SSO_AUTHN_REQUEST_OUTGOING".equals("" + messageCtx.getMessageType()) && "SAML2.0".equals("" + messageCtx.getMessageVersion()); if (!matches) msg += "@@@@@@ CRITERIA NOT MET - SKIPPING THIS MESSAGE @@@@@@\n"; else { msg += "@@@@@@ CRITERIA MET - TRANSFORMING THIS MESSAGE @@@@@@\n"; Element root = parseXML(messageCtx.getMessage()); String pretty = unparseXML(root); msg += "---------- ORIGINAL XML -----------------\n"; msg += "\n" + pretty + "\n"; // Now to modify the XML message Element ext = getExtensionsXML(); root.appendChild(root.getOwnerDocument().importNode(ext,true)); // DOM tree modified - unparse the DOM back into string messageCtx.setModifiedMessage(unparseXML(root)); msg += "---------- MODIFIED XML -----------------\n"; msg += "\n" + messageCtx.getModifiedMessage() + "\n"; } msg += "=================ENDS===================\n"; err.println(msg); return ExecutionStatus.SUCCESS; } catch (Exception e) { e.printStackTrace(); throw handle(e); } } @Override public String getDescription(){ return "Sample Message Processing Plugin"; } @Override public Map<String, MonitoringData> getMonitoringData(){ return null; } @Override public boolean getMonitoringStatus(){ return monitoringStatus; } @Override public String getPluginName(){ return "SampleMsgProcPlugin"; } @Override public int getRevision() { return 123; } @Override public void setMonitoringStatus(boolean status){ this.monitoringStatus = status; } private RuntimeException handle(Exception e) { return e instanceof RuntimeException ? (RuntimeException)e : new RuntimeException(e); } private DocumentBuilderFactory createDBF() { try { Class<?> c = Class.forName("com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl",true,ClassLoader.getSystemClassLoader()); return (DocumentBuilderFactory) c.newInstance(); } catch (Exception e) { throw handle(e); } } private Element parseXML(String xml) { try { DocumentBuilderFactory dbf = createDBF(); dbf.setNamespaceAware(true); DocumentBuilder db = dbf.newDocumentBuilder(); StringReader sr = new StringReader(xml); InputSource is = new InputSource(sr); Document doc = db.parse(is); return doc.getDocumentElement(); } catch (Exception e) { throw handle(e); } } private DOMImplementationSource getDOMImpl() { try { Class<?> c = Class.forName("com.sun.org.apache.xerces.internal.dom.DOMXSImplementationSourceImpl",true,ClassLoader.getSystemClassLoader()); return (DOMImplementationSource) c.newInstance(); } catch (Exception e) { throw handle(e); } } private String unparseXML(Node node) { try { Document doc = node.getOwnerDocument(); DOMImplementationSource reg = getDOMImpl(); DOMImplementationLS ls = (DOMImplementationLS) reg.getDOMImplementation("LS"); LSSerializer w = ls.createLSSerializer(); w.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE); w.getDomConfig().setParameter("xml-declaration", Boolean.TRUE); String ppxml = w.writeToString(doc); return ppxml.replace("<?xml version=\"1.0\" encoding=\"UTF-16\"?>","<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); } catch (Exception e) { throw handle(e); } } private Element getExtensionsXML() { String extensionsXML = ""; extensionsXML += "<samlp:Extensions xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\">\n"; extensionsXML += " <CompanyInfo xmlns=\"http://example.com/samlext/1.0\"\n"; extensionsXML += " CompanyName=\"Example Corporation\" />n"; extensionsXML += "</samlp:Extensions>\n"; return parseXML(extensionsXML); } private Element getChildElem(Node node,String nsuri, String name) { try { NodeList kids = node.getChildNodes(); for (int i = 0; i < kids.getLength(); i++) { Node k = kids.item(i); if (k.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE) continue; if (!Objects.equals(k.getNamespaceURI(),nsuri)) continue; if (!Objects.equals(k.getLocalName(),name)) continue; return (Element)k; } return null; } catch (Exception e) { throw handle(e); } } } |
Question: Why do I get the DOM factory objects directly from the System Class Loader using Class.forName?
Answer: I tried doing it the normal way, but OAM overrides the standard JDK XML classes with some Oracle-specific ones that don’t implement DOM 3 Load and Save. So by doing it this way I make sure I get an XML implementation that has the features I need.
Next we need to create the XML plugin manifest $PLUGINDEV/SampleMsgProcPlugin.xml:
<?xml version="1.0"?> <Plugin type="Message Processing"> <author>Oracle A-Team</author> <email>donotreply@oracle.com</email> <creationDate>2015-04-16 12:53:37</creationDate> <description>Sample Message Processing Plugin</description> <configuration> </configuration> </Plugin> |
Here we could, if we so wished, define some configuration settings for our plugin, that could then be modified using the OAM Console. However, in this simple example, we have not done that.
Next we need to create $PLUGINDEV/MANIFEST.MF file:
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: SampleMsgProcPlugin Bundle-SymbolicName: SampleMsgProcPlugin Bundle-Version: 1 Bundle-Activator: oracle.ateam.msgprocplugin.SampleMsgProcPlugin Import-Package: javax.xml.parsers,oracle.security.am.plugin,oracle.security.fed.plugins.fed.msgprocessing,org.osgi.framework;version="1.3.0",org.w3c.dom,org.w3c.dom.ls,org.xml.sax Bundle-RequiredExecutionEnvironment: JavaSE-1.6 |
This is the OSGi bundle metadata. Notably, it lists the Java packages our plugin requires. (Note that Import-Package is all on one-line.)
Next we need to create $PLUGINDEV/compile.sh file:
#!/bin/bash DOMAIN_HOME=/idmtop/config/domains/IAMAccessDomain SERVER_NAME=wls_oam1 JARS="$(find $DOMAIN_HOME/servers/$SERVER_NAME/tmp/_WL_user/oam_server_11.1.2.0.0/ -name fed.jar -o -name oam-plugin.jar -o -name felix.jar | tr '\n' ':' | sed -e 's/:$//')" SRCS="$(find src -name '*.java')" rm -rf build mkdir build javac -d build -classpath $JARS $SRCS cp SampleMsgProcPlugin.xml build mkdir build/META-INF cp MANIFEST.MF build/META-INF cd build jar cvmf META-INF/MANIFEST.MF ../SampleMsgProcPlugin.jar * |
This shell script compiles the plugin for us. (Of course, one should use something like ANT or Maven instead, but for our simple example a shell script will do.) Note the path DOMAIN_HOME and the SERVER_NAME may need to be changed for your environment. Also note that JARS= is supposed to all be on one line (in case your web browser is wrapping it).
Finally we run compile.sh to create SampleMsgProcPlugin.jar.
Deploy the example plugin
Login to OAM Console
Go to “Application Security” tab
In “Plug-ins” section select “Authentication Plug-ins”:
Note: Even though the label says “Authentication Plug-ins”, the same screen works for non-authentication types of plugins, such as the message processing plugin in this case.
Click “Import Plugin”:
Upload “SampleMsgProcPlugin.jar” you built in earlier step and click “Import”:
Refresh the table and search for the new plugin:
Click “Distribute Selected”, then click Refresh icon to see status has changed to “Distributed”:
Now click “Activate Selected”, then click Refresh icon to see status has changed to “Activated”:
Enabling message processing plugin
The plugin has now been installed. Now we need to tell OAM Federation to use it. Edit the $DOMAIN_HOME/config/fmwconfig/oam-config.xml file. Look for the “fedserverconfig” section:
In that section, look for a setting called “messageprocessingeplugin”:
Change its value to the name of your plugin:
Also, in the same section, look for a setting called “messageprocessingenabled”:
Change that from “false” to “true”.
Finally, near the top of the file, look for a version number:
Increment that number by 1, and save. (It doesn’t matter by how much you increment it, so long as the new version number is higher than the old one.)
Now check the oam-config.ref file in the same directory:
When the version number in that file has increased to the new number in the oam-config.xml, you know the new number has been loaded.
Testing the new plugin
Go to the SP Test Page (http://OAMHOST:OAMPORT/oamfed/user/testspsso). This assumes you have enabled it per my previous post. Test using LoopbackIDP. As you test, watch SAML messages using e.g. SAML Tracer plugin in Firefox [https://addons.mozilla.org/en-us/firefox/addon/saml-tracer/]. You should see the custom XML extension in the SAML AuthnRequest in SAML Tracer:
Also note the custom plugin writes to the wls_oam1 console log every time it runs:
You would probably not want to do this in Production, but it is helpful in development. And you’d want to make it log through java.util.logging not System.err.println. Remember this sample code is just a start for your own development process, it is not meant to be production quality.
All content listed on this page is the property of Oracle Corp. Redistribution not allowed without written permission