IdPDevCustomExtension
Creating Custom IdP Extensions - General Overview
STOP!
The Shibboleth Identity Provider uses Spring and XML and XML Schema extensively. Before you begin you must have a basic understanding of these technologies.
This document is meant to give developers a basic overview in creating Shibboleth extensions. The general steps are the same for every plugin, only things like the particular classes and schema types that are extended change.
Be aware that the IdP uses the same plugin mechanism as custom extensions do in order to provide default implementations of the extension points. The only difference is that the files for those extensions are included in the IdP's jars instead of a separate one for the custom extension.
Appendix A provides a complete list of the extension points within the IdP. Following the link to the extension-specific description will provide the various namespaces, schema file locations, and classes needed when creating a custom extension.
The SVN repository shib-extension contains some example extensions.
What can be Extended?
The easiest way to tell what can be extended is to look in your IdP's configuration file. Every element with the attribute xsi:type
represents a plugin point.
As mentioned before the IdP uses the same plugin mechanisms for its own implementations. Therefore looking at the existing IdP implementations of the extension point should help provide already working code examples.
What does a Plugin Look Like?
The final result of a plugin is a JAR with the following structure:
/META-INF/spring.handlers /META-INF/spring.schemas /schema/... your schema files ... ... Java class files for extension...
While the schema file(s) for the extension do not have to go in a schema
directory it is recommended for organizational purposes.
Step 1: Set up the Project
The final result of an extension project is a JAR in the form outlined in the previous section. Any build environment may be used to create this JAR but these instructions will use Maven, the build system used by the IdP.
- Download this project template setup and extract it
- Edit the included
pom.xml
- Fill in the
<groupId>
,<artifactId>
,<version>
,<name>
elements (found as children of<project>)
- Fill in the
<version> element
of the shibboleth-identityprovider dependency with the appropriate Shibboleth IdP version, e.g. "2.4.0" (found as a descendant of<project>/<dependencies>)
- Fill in the
- Optionally, within
pom.xml
uncomment some or all of the project metadata section at the bottom of the file and fill it in with the appropriate information
This guide assumes the usage of this project layout. It will refer to the root of the project folder as $PROJ_HOME.
Step 2: Write the Extension
Create a Java class that implements/extends the appropriate interface/class for the extension point being implemented.
Spring works by injecting properties into the plugin by either passing them as constructor arguments or by means of setter methods. That is, it assumes the plugin is a JavaBean. The example in this guide uses both constructor and property based injection.
package org.example.shibboleth.authn; ... imports ... public class IPAddressLoginHandler extends AbstractLoginHandler { public IPAddressLoginHandler(String username){ // method logic } /** {@inheritDoc} */ public void login(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { // method logic } public List<String> getAllowedIPAddressRanges(){ // method logic } public void setAllowedIPAddressRanges(List<String> ranges){ // method logic } }
Step 3: Define a Plugin Schema
The next step is to describe the properties needed by the plugin with an XML schema. This Schema enumerates all the possible configuration options for your extension and allows the IdP to perform basic validation of a user's configuration file.
The contents of the schema file will be one or more <complexType>
elements that extend a type defined either in the standard Shibboleth XML Schema files or another developer's extension.
Here are the basic steps for setting up the schema file:
- Pick a namespace for the plugin. Do NOT use the
urn:mace:shibboleth
namespace. - Create the schema file in
$PROJ_HOME/resources/schema
- Set the
targetNamespace
to the chosen namespace - Declare any Shibboleth namespaces that will be used via the customary
xmlns:PREFIX
attributes. At a minimum the namespace associated with the extension point must be declared. - Set the
elementFormDefault
attribute value to "qualified"
- Set the
- Import the schema file that contains the plugin point being implemented. Shibboleth defines a special URL scheme,
classpath
, which ensures that the schema files are resolved from the classpath. No other resolution mechanism is allowed. - Define the plugin type(s) and ensure they extend the appropriate Shibboleth type.
<schema targetNamespace="urn:mace:example.org:shibboleth:authn" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ph="urn:mace:shibboleth:2.0:idp:profile-handler" elementFormDefault="qualified"> <import namespace="urn:mace:shibboleth:2.0:idp:profile-handler" schemaLocation="classpath:/schema/shibboleth-2.0-idp-profile-handler.xsd" /> <complexType name="IPAddress"> <complexContent> <extension base="ph:LoginHandlerType"> <sequence> <element name="IPEntry" type="string" maxOccurs="unbounded" /> </sequence> <attribute name="username" type="string" /> </extension> </complexContent> </complexType> </schema>
If you are using Eclipse, you might want it to load an URL Handler for the "classpath:" URL scheme, so that Eclipse can find imported schemas when editing a .xsd file. You can download this JAR file and load it in Eclipse like this (assuming you downloaded the file in $HOME/shibboleth):
eclipse -vmargs -Djava.protocol.handler.pkgs=org.my.protocols -Xbootclasspath/p:$HOME/shibboleth/classpath-uri-stream-handler-0.0.1-SNAPSHOT.jar
Step 4: Write the Spring BeanDefinitionParser
The Spring BeanDefinitionParser reads the XML configuration for the extension and sets the appropriate bean properties and/or constructor arguments on the plugin class. In many cases the IdP uses a slightly more complex method for creating its plugins. It uses a BeanDefinitionParser to populate an AbstractFactoryBean. The factory bean provides the JavaBean interface needed by Spring but allows those properties to be mangled or added to a plugin by means of some method other than setters. Within the IdP this is almost always done to avoid having to have setter methods that would replace some Collection field within the plugin (which can lead to various NullPointerExceptions, synchronization issues, etc.).
- Create a class that extends the Bean Parser class for the extension point.
- Define a
public static final QName SCHEMA_TYPE
class field that contains the QName for the extension type. The example uses a static variable from the class to be defined next. This QName isn't required but putting it here helps keep related information together. - Implement the
getBeanClass(Element)
method and return aClass
object for the extension written in step 1. - Implement the
doParse(Element, ParserContext, BeanDefinitionBuilder)
method and populate theBeanDefinitionBuilder
with information from the givenElement
. ThisElement
object is the DOM Element, matching the defined schema, for a particular configuration instance of the extension.
package org.example.shibboleth.authn.config; ... imports ... public class IPAddressLoginHandlerBeanDefinitionParser extends AbstractSimpleBeanDefinitionParser{ /** Schema type. */ public static final QName SCHEMA_TYPE = new QName(AuthnExtensionNamespaceHandler.NAMESPACE, "IPAddress"); /** {@inheritDoc} */ protected Class getBeanClass(Element arg0) { return IPAddressLoginHandler.class; } /** {@inheritDoc} */ protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { // Create a list from the <IPEntry> elements List ipEntries; // code for populating the list omitted builder.addPropertyValue("allowedIPAddressRanges", ipEntries); String username = element.getAttributeNS(null, "username"); builder.addConstructorArg(username); } }
The classes org.opensaml.xml.util.XMLHelper
and org.opensaml.xml.util.DatatypeHelper
provide a lot of helper methods for dealing with XML and various data types. For example, XMLHelper
contains methods for getting attribute values as booleans, Lists, and other things.
Step 5: Define a NamespaceHandler
The last class to implement for Spring is the NamespaceHandler
. This is the class that instructs Spring to execute the BeanDefinitionParser
associated with a given extension type in a configuration file.
- Create a class that extends
edu.internet2.middleware.shibboleth.common.config.BaseSpringNamespaceHandler
- Add a public static final String NAMESPACE field whose value is the Schema namespace for your extension
- Implement the
init()
method and, for each plugin within this namespace, callregisterBeanDefinitionParser(QName, BeanDefinitionParser)
where the first argument is theSCHEMA_TYPE
field on aBeanDefinitionParser
implementation and the second argument is a new'ed instance of thatBeanDefinitionParser
.
package org.example.shibboleth.authn.config; ... imports ... public class AuthnNamespaceHandler extends BaseSpringNamespaceHandler { public static final String NAMESPACE = "urn:mace:example.org:shibboleth:authn"; public void init() { registerBeanDefinitionParser(IPAddressLoginHandlerBeanDefinitionParser.SCHEMA_TYPE, new IPAddressLoginHandlerBeanDefinitionParser()); // other calls to registerBeanDefinitionParser for other extensions in this namespace would go here } }
Step 6: Create Spring Configuration Files
Finally two files; spring.schemas
and spring.handlers
. Both files go in to the META-INF directory of the JAR and the $PROJ_HOME/main/resources/META-INF
directory within the project. Both files may have multiple, line-terminated, entries.
The spring.schemas
file tells Spring where to look for the schema file for a particular namespace. The format of the file is simply the XML namespace URI followed by an "=" followed by the location of the schema file within the JAR (i.e schema/FILE.xsd
). Do not include the a preceding "/", the files are not at the root of the filesystem.
urn\:mace\:example.org\:shibboleth\:authn = schema/authn.xsd
The spring.handlers
file tells Spring which NamespaceHandler
to use for a particular namespace. The format of the file is the XML namespace URI followed by an "=" followed by the fully qualified class name of the NamespaceHandler
implementation.
urn\:mace\:example.org\:shibboleth\:authn = org.example.shibboleth.authn.config.AuthnNamespaceHandler
Note that because Spring uses the java.util.Properties
class to load these files the ':' character must be escaped with a backslash. This character has a special meaning for property files loaded in this manner.
Step 7: Write Installation Instructions
The expected procedure for installing an extension in to the IdP is to have the deployer copy the necessary material in to the expanded IdP distribution archive and then re-run the IdP installation script. This ensures the libraries get bundled in to the WAR file and placed where command line tools may use them. However the exact installation steps will depend on the nature of the extension. For example, an extension that includes not only custom plugins that use the Spring architecture but also new Servlets/JSP pages will require the deployer to edit the web.xml
document and/or copy JSP files before rebuilding the war.
Appendix: Shibboleth IdP Extension Points
Plugin point for creating a new match function used by the Attribute Filter Policy policy requirement, permit value, and deny value rules. | |
Plugin point for creating new attribute resolver attribute definitions. | |
Plugin point for creating a new attribute resolver attribute encoder. | |
Plugin point for creating a new attribute resolver data connector. | |
Plugin point for creating a new attribute resolver principal connector. | |
Plugin point for creating a new profile handler. | |
Plugin point for creating a new login handler. | |
Plugin point for creating a new relying party profile configuration. | |
Plugin point for creating a new relying party metadata provider. | |
Plugin point for creating a new metadata provider filter. | |
Plugin point for creating a new plugin for reading credentials. | |
Plugin point for creating a new trust engine. | |
Plugin point for creating a new security policy. | |
Plugin point for creating a new security policy rule. | |
Plugin point for creating a new resource. | |
Plugin point for creating a new resource filter. | |
Plugin point for creating a new service. |