The functionality of the Identity Server can be extended through plugins.
A plugin is basically a set of plugin JARs containing implementations of the Identity Server’s extension points, along with any accompanying library jars and other resources.
Note
It should be possible to write plugins in most JVM Programming Languages[2]. The server is regularly tested with plugins written in Java, Groovy and Kotlin.
The sections below present a high-level overview of the Identity Server’s Plugin System.
For more details, see the plugin SDK documentation.
The SDK JAR, which your plug-in will depend on, can be found in Maven Central and other public Maven repositories.
Plugins are installed in groups at $IDSVR_HOME/usr/share/plugins/<plugin_group>.
$IDSVR_HOME/usr/share/plugins/<plugin_group>
To install a plugin into the server, simply drop its jars and all of its required resources, including Server-Provided Dependencies, in the <plugin_group> directory. Sub-directories are not taken into consideration.
<plugin_group>
The actual names of the <plugin_group> directories have no meaning for the server. Any number of plugins may be placed together in each directory, allowing several plugins to share the same classpath (see the Classpath considerations section for details). Also, any number of plugin groups may be provided. It is advisable to only split up plugins into separate groups if complete isolation (classpath separation) between them is desired or required.
Each plugin group is given its own Java ClassLoader, meaning that their classpath are almost[1] completely isolated from the server’s and other plugin groups’s classpath.
ClassLoader
This isolation requires all runtime dependencies, except for the Identity Server plugin SDK library and Server-Provided Dependencies, to be included in the plugin group directory alongside the plugin’s own resources.
On server startup, each plugin group directory is scanned for added/removed/modified plugins (notice that this implies that modifications to plugins’s resources require a server re-start). The configuration definition for any added/modified plugins will be compiled at this point; halting server startup on encountering any invalid plugin configuration definition.
Once installed, a Plugin may be configured using the server’s standard configuration mechanisms.
See the Configuration Guide for details.
Warning
Please note that plugin updates that contain modified configuration definitions may break existing server configuration, which will cause a server halt at startup. It is therefore recommended to remove any configured references to a plugin before updating it and restarting the server.
If a plug-in loads Java types at run time, or uses frameworks that do so (e.g. JAXB, JAF, JAX-WS), it may be necessary to modify the Java context ClassLoader so that all types within the plug-in and its dependencies can be found.
To assist with that, the se.curity.identityserver.sdk.ClassLoaderContextUtils type can be used as shown below:
se.curity.identityserver.sdk.ClassLoaderContextUtils
ClassLoaderContextUtils
new SOAPHandler<>() { @Override public boolean handleMessage(SOAPMessageContext context) { return ClassLoaderContextUtils.withPluginClassLoader(() -> { // because this causes Jakarta EE Services to be loaded, this call // must be made inside a `withPluginClassLoader` lambda. SOAPMessage soapMessage = context.getMessage(); ... }); } ... }
A plugin’s entry-point is its PluginDescriptor. That is a class that describes the plugin type (given by which PluginDescriptor sub-type the plugin descriptor implements), what services the plugin provides, as well as its Configuration interface.
PluginDescriptor
The Configuration interface is used to generate the plugin’s configuration at runtime. An instance of the plugin’s Configuration interface is generated by the server based on the server’s current configuration state and provided to the plugin’s code via constructor-injection (see the plugin-example).
The server finds plugin descriptor implementation classes using the standard Java ServiceLoader mechanism, i.e. by looking at files named META-INF/services/<service-interface>, where <service-interface> should be replaced with the fully qualified name of the service interface, within the jars. The contents of these files should be the name(s) of the implementation class(es) for the service interface. If more than one implementation exists, each implementation class should be given on each line.
META-INF/services/<service-interface>
<service-interface>
If this file is missing, the plugin will not be recognized by the server.
In summary, a plugin should normally contain, at least, the following:
Plugin Configuration interface:
package com.example; import se.curity.identityserver.sdk.config.Configuration; import se.curity.identityserver.sdk.service.HttpClient; import java.net.URI; public interface ExampleSmsConfiguration extends Configuration { // plugin configuration primitives URI getServerUri(); int getMaxReconnectAttempts(); // any services required by the plugin HttpClient getHttpClient(); }
SmsSender Service implementation:
SmsSender
package com.example; import se.curity.identityserver.sdk.service.sms.SmsSender; public class ExampleSmsSender implements SmsSender { private final ExampleSmsConfiguration configuration; // obtain the current configuration via constructor-injection public ExampleSmsSender(ExampleSmsConfiguration configuration) { this.configuration = configuration; } @Override public boolean sendSms(String toNumber, String msg) { // add SMS sending logic here return false; } }
PluginDescriptor implementation:
package com.example; import se.curity.identityserver.sdk.plugin.descriptor.SmsPluginDescriptor; import se.curity.identityserver.sdk.service.sms.SmsSender; import se.curity.identityserver.sdk.config.Configuration; public class ExampleSmsPluginDescriptor implements SmsPluginDescriptor<ExampleSmsConfiguration> { @Override public String getPluginImplementationType() { return "example-sms"; } @Override public Class<ExampleSmsConfiguration> getConfigurationType() { return ExampleSmsConfiguration.class; } @Override public Class<? extends SmsSender> getSmsSenderType() { return ExampleSmsSender.class; } }
Service loader file:
com.example.ExampleSmsPluginDescriptor
Plugins that require objects that must have a well-defined lifecycle can use the ManagedObject class.
A ManagedObject is an object that gets instantiated when the plugin gets loaded, and closed as soon as configuration changes or the server starts to shut down.
ManagedObject
They are typically used to handle resources such as connection pools that the plugin may require.
To use a ManagedObject, you should override the createManagedObject() method in the plugin’s PluginDescriptor and return a initialized instance of your managed object.
createManagedObject()
The close() method of the ManagedObject will be called when the server needs to replace configuration or shut down.
close()
Plugin services may obtain an instance of the plugin’s ManagedObject via constructor-injection, in the exact same way as they obtain the plugin’s Configuration instance.
Configuration
The Curity SDK provides several Services that may be used by plugins.
All Services are located within the se.curity.identityserver.sdk.service package and its sub-packages.
se.curity.identityserver.sdk.service
A plugin obtains a Service by adding a getter that returns an instance of the Service to the plugin-provided Configuration interface.
For example, to obtain a SmsSender service, a plugin could declare the following getter in its Configuration interface:
package com.example; import se.curity.identityserver.sdk.config.annotation.Description; import se.curity.identityserver.sdk.config.Configuration; import se.curity.identityserver.sdk.service.sms.SmsSender; public interface ExampleConfiguration extends Configuration { @Description("SMS Sender configured for this plugin") SmsSender getSmsSender(); }
There are a few limitations to what Services may be obtained by a plugin depending on its type, as shown in the following table (services not shown in this Table may be used in plugins of any type):
AuthenticatorInformationProvider
Authenticator
BackchannelAuthenticator
AuthenticationAction
AuthenticationActionInformationProvider
AuthenticatorDescriptorFactory
AuthenticatorDescriptor
AccountDomain
AuthenticatorExceptionFactory
RequestingOAuthClient
AuthenticationRequirements
CredentialVerifier
UserCredentialManager
service.issuer
TokenProcedure
service.introspecter
Managed Objects have limited access to Services. Namely, they may only use Services that are annotated with the @ConfigurationScope annotation, as opposed to other plugin implementation objects in which such limitations do not apply.
@ConfigurationScope
Because a Service’s configuration may be modified independently of a Managed Objects’s, it’s very important to not cache a Service within a ManagedObject (either instance fields or static fields, or anywhere else).
Within a ManagedObject, always access a Service through the Configuration interface getter, never cache Services in fields or global variables.
For more details, consult the Javadocs for ManagedObject and ConfigurationScope.
Some plugin types can expose request handlers (see RequestHandler interface) to process requests originating on the user’s browser. The access from cross-site origins to those handlers is controlled by the Curity Identity Server according to the plugin’s defined policy:
GET
POST
The plugin descriptor’s interface exposes an allowedHandlersForCrossSiteNonSafeRequests method returning a RequestHandlerSet, containing the set of handlers that can be called on cross-site requests. The RequestHandlerSet class has the following static methods to help with instance creation:
allowedHandlersForCrossSiteNonSafeRequests
RequestHandlerSet
RequestHandlerSet.all()
RequestHandlerSet.none()
RequestHandlerSet.of(...)
This feature aims to provide protection against CSRF (Cross Site Request Forgery) attacks, by automatically blocking cross-site requests to handlers that don’t need this type of access. Plugins with handlers that need cross-site requests should have internal protection mechanisms against CSRF attacks.
Note also that this protection feature depends on the user’s browser supporting the SameSite cookie attribute feature. If the browser doesn’t support it, then all requests are classified as same-site and allowed into the handlers.
An HTTP request originating from an iframe hosted on a different origin will be considered cross-origin by a modern browser and any cookies marked with the SameSite attribute set to lax or strict will not be sent. This means that by default this protection mechanism will block plugin handlers from being accessible inside iframes on browsers that support SameSite. To enable framing, the plugin needs to explicitly allow the accessed handlers in the allowedHandlersForCrossSiteNonSafeRequests method. If changing the plugin code is not possible, then see Cross Site Requests on ways to disable this protection mechanism via system properties.
SameSite
lax
strict
The only version of Java which the Curity Identity Server supports is the one that is bundled with it. This is commercial version of Zulu from Azul. Only the JRE is shipped though, not the JDK. So, developers will need to obtain a JDK (including javac and other such tools). The recommended ones are:
javac
The version supported and used by the server can be found in the file $IDSVR_HOME/lib/java/release.
$IDSVR_HOME/lib/java/release
Plugins may have any dependencies, as long as all of the JARs are included with the plugin in the plugin group directory. However, when using one of the dependencies that are provided by the server, the library versions must match exactly those that the server provides. Because the version must exactly match the server’s, it is not recommended to deploy these dependencies with the plug-in, but to instead have compile-time dependencies on the exact versions below. If they are deployed though and if a plugin uses a different version than what the server provides, runtime errors may occur. Including the JARs of these libraries in the plugin directory is optional, but allows the server to check their versions are correct, ensuring that version conflicts will not happen at runtime. If the library JARs are not present in the plugin group directory, this check will not be possible, which can cause errors that only manifest at runtime. This extra check, however, comes at a maintenance cost. As mentioned above, the included dependency’s version will have to be updated to exactly match the server’s or else the server will not start. Consequently, the version of the dependency may have to be changed with new versions of the product.
The following dependencies, besides the Identity Server SDK, are provided by the server. No other dependency is guaranteed to be available, including parts of the Java runtime. Any dependency that is not listed below is subject to change without notice (which may even come with breaking changes).
java.util.logging
jakarta.validation
For security reasons, Java serialization and deserialization are restricted and cannot be used by default. This means that any class implementing the java.io.Serializable interface and using Java’s facilities to turn an object of such a kind to or from a byte stream will fail. Many known and unknown security vulnerabilities are prevented by this restriction, so changing it is ill advised. If it must be, however, Java’s standard system property can be set to include a pattern of allowed packages, classes, etc. that should be serializable and deserializable (Cf. JVM Configuration). Using this property of Java’s, however, needs to also include the internal classes and types that need to be serialized otherwise undefined behavior may result. Because these classes and types are undocumented and subject to change, it is better to use the Curity-specific system property se.curity:identity-server:serialFilter in place of Java’s jdk.serialFilter system property. This will add all internal classes and types plus those defined in the value of this property. The value fo the se.curity:identity-server:serialFilter system property is identical to Java’s jdk.serialFilter system property, so refer to the Java documentation for details.
java.io.Serializable
se.curity:identity-server:serialFilter
jdk.serialFilter
Footnotes
List
String
Map