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, can be found in Maven Central and other public Maven repositories.

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 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:

Listing 266 Example usage of 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();
            ...
        });
    }
...
}

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 267 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 268 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 269 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 270 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.

Note

Back-channel Authenticators that have a link to a front-channel authenticator plugin may also inject the
front-channel plugin’s ManagedObject (and Configuration), if it has one, in addition to its own.

Plugin Services

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.

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:

Listing 271 com/example/ExampleConfiguration.java
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();
}

Service Restrictions by Plugin Type

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):

Service Allowed Plugin Types
AuthenticatorInformationProvider Authenticator, BackchannelAuthenticator, AuthenticationAction
AuthenticationActionInformationProvider AuthenticationAction
AuthenticatorDescriptorFactory AuthenticationAction
AuthenticatorDescriptor AuthenticationAction
AccountDomain AuthenticationAction
AuthenticatorExceptionFactory Authenticator
RequestingOAuthClient Authenticator, BackchannelAuthenticator, AuthenticationAction
AuthenticationRequirements Authenticator, AuthenticationAction
CredentialVerifier Authenticator
UserCredentialManager Authenticator, AuthenticationAction
all services under the SDK’s service.issuer package TokenProcedure
all services under the SDK’s service.introspecter package TokenProcedure

Service Restrictions in ManagedObject

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.

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).

Warning

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.

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. 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).

SLF4J Logging API

Description The slf4j API
Bundle-SymbolicName slf4j.api
Version 2.0.12
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 Jakarta Bean Validation API
Bundle-SymbolicName jakarta.validation.jakarta.validation-api
Version 3.0.0
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 7.0.3.Final
Required No
Note
See note for Bean Validation API dependency.
This dependency should only be added if annotations not present in jakarta.validation are desired.

Kotlin Standard Library

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

Serialization

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.

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.