Plugins

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.

Access to the Curity Release Repository

The SDK JAR, which your plug-in will depend on, is in a repository hosted by Curity. To use this, you will need to update your Maven settings file (usually at $HOME/.m2/settings.xml). A sample is shown in Listing 322:

Listing 322 A sample settings.xml file that adds the Curity repository and credentials necessary to download the SDK
<settings>
    <profiles>
        <profile>
            <id>curity</id>
            <repositories>
                <repository>
                    <id>customer-release-repo</id>
                    <url>https://nexus.curity.se/repository/customer-release-repo/</url>
                    <releases><enabled>true</enabled></releases>
                    <snapshots><enabled>false</enabled></snapshots>
                </repository>
            </repositories>
        </profile>
    </profiles>

    <activeProfiles>
      <!--make the profile active all the time -->
      <activeProfile>curity</activeProfile>
    </activeProfiles>
</settings>

Plugin Installation

Plugins are installed in groups at $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.

Note

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.

Classpath considerations

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.

This isolation requires all runtime dependencies, except for the Identity Server plugin SDK library, 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.

Basic structure of a plugin

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.

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.

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:

  1. An (interface) extension of the Configuration interface.
  2. One or more service implementations, as specified by the plugin descriptor.
  3. An implementation of one or more of the PluginDescriptor sub-types.
  4. A Service loader (META-INF/services/<service-interface>) file.

SmsSender Plugin Example

Plugin Configuration interface:

Listing 323 com/example/ExampleSmsConfiguration.java
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:

Listing 324 com/example/ExampleSmsSender.java
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:

Listing 325 com/example/ExampleSmsPluginDescriptor.java
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:

Listing 326 META-INF/services/se.curity.identityserver.sdk.plugin.descriptor.SmsPluginDescriptor
com.example.ExampleSmsPluginDescriptor

Managed Objects

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.

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.

The close() method of the ManagedObject will be called when the server needs to replace configuration or shut down.

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.

Cross-site Plugin Handlers

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:

  • Same-site access is always allowed.
  • Cross-site access with safe HTTP methods (e.g. GET) is always allowed.
  • Cross-site access with non-safe HTTP methods (e.g. POST) depends on the plugin’s policy. For legacy reasons, by default all handlers allow cross-site access however this can be changed programmatically.

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:

  • RequestHandlerSet.all() - returns a set with all the plugin’s handlers.
  • RequestHandlerSet.none() - returns an empty set.
  • RequestHandlerSet.of(...) - returns a set with the given handlers.

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.

Warning

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.

Java Version

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:

The version supported and used by the server can be found in the file $IDSVR_HOME/lib/java/release.

Server-Provided Dependencies

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, as such dependencies are not loaded by separate class loaders.

The following dependencies, besides the Identity Server SDK, are provided by the server:

Note

If a plugin depends on any of the libraries listed below but 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.

SLF4J Logging API

Description The slf4j API
Bundle-SymbolicName slf4j.api
Version 1.7.22
Required No
Note
Plugins that use the slf4j-api or java.util.logging for logging can be configured via
the server’s log4j2 configuration file.

Bean Validation API

Description Java Validation API
Bundle-SymbolicName javax.validation.api
Version 1.1.0.Final
Required No
Note
Validation annotations defined in this bundle may be used for decorating Request Models handled by a RequestHandler.
Plugins depending on another version of this library bundle than the one specified here will be rejected at service boot; resulting in a controlled service halt.

Hibernate Validator Engine

Description Hibernate Validator Engine
Bundle-SymbolicName org.hibernate.validator
Version 5.1.3.Final
Required No
Note
See note for Bean Validation API dependency.
This dependency should only be added if annotations not present in javax.validation.api are desired.

Kotlin Standard Library

Description Kotlin Standard Library
Implementation-Title kotlin-stdlib-jdk8
Version 1.3.61
Required No
Note
All plugins written in Kotlin should include this dependency.

Footnotes

[1]The server-provided dependencies listed on this page are the only ones that are loaded under a common ClassLoader, namely, the server’s class loader, so the classpath isolation is not complete.
[2]As long as the programming language’s compiler produces valid jars and it does not load classes dynamically or replace common Java types (such as List, String, Map) with their own types, there should be no problems for plugins to interact with the server.