How to implement a Service Provider Interface (SPI) and package a JAR

This guide explains the development and packaging of Java plugins for the Connect2id server.

The server features over 15 plugin interfaces, called Service Provider Interfaces (SPI) in Java. The SPI is a standard Java facility to dynamically load bytecode that implements an interface contract. The plugin bytecode is typically packaged in a JAR file for convenience.

1. Set up your project

Create a new Java project with the appropriate packaging, Java version and dependencies.

Packaging

The project must create a JAR package.

Java version

Should be set to the Java version required by the Connect2id server. At the time of writing this guide this is Java 17.

Dependencies

Import the following dependencies:

<dependencies>
    <dependency>
        <!-- The Connect2id server SDK -->
        <groupId>com.nimbusds</groupId>
        <artifactId>c2id-server-sdk</artifactId>
        <version>[version]</version>
    </dependency>
    <dependency>
        <!-- The OAuth 2.0 / OpenID Connect SDK -->
        <groupId>com.nimbusds</groupId>
        <artifactId>oauth2-oidc-sdk</artifactId>
        <version>[version]</version>
    </dependency>
    <dependency>
        <!-- If the plugin needs to log messages -->
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>[version]</version>
    </dependency>
    <dependency>
        <!-- SPI annotation -->
        <groupId>org.kohsuke.metainf-services</groupId>
        <artifactId>metainf-services</artifactId>
        <version>1.9</version>
        <optional>true</optional>
    </dependency>
</dependencies>

The [version] numbers should match those in the targeted minimal Connect2id server version.

How to find out the dependency versions?

Inspect the content of the c2id.war package (a ZIP file), where a copy of the server's POM is stored under /META-INF/maven/com.nimbusds/c2id-server/pom.xml.

Several transitive dependencies will also be imported, such as those for the Nimbus JOSE+JWT library.

Shading dependencies to prevent conflicts

If the plugin needs to import a dependency that may cause a version conflict or a class naming conflict with the Connect2id server's own dependencies consider shading it. The Maven Shade Plugin is the recommended tool for this purpose.

Here is an example shading of the Gson library, which may have the following import declaration in the plugin's pom.xml:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.10.1</version>
</dependency>

To shade it, import the Maven Shade Plugin. Specify the artifact to shade and the relocation of its package(s). The relocation should typically be to the plugin's own package namespace.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.4.1</version>
    <configuration>
        <artifactSet>
            <includes>
                <include>com.google.code.gson:gson</include>
            </includes>
        </artifactSet>
        <relocations>
            <relocation>
                <pattern>com.google.gson</pattern>
                <shadedPattern>org.myplugin.shaded.gson</shadedPattern>
            </relocation>
        </relocations>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
        </execution>
    </executions>
</plugin>

To shade multiple dependencies add as many artifactSet.includes.include and relocations.relocation elements as necessary.

Dependency injection

Plugins are free to utilise dependency injection internally.

Note that dependency injection may encounter issues with shaded dependencies. Such are typically resolved by configuring the dependency injection framework to use the shaded package names.

2. Development

Implement the plugin SPI, which is defined in the Connect2id server SDK. The SDK JavaDocs are comprehensive and kept up to date.

Git repohttps://bitbucket.org/connect2id/server-sdk

Annotate the implementing class with @MetaInfServices to have the appropriate SPI manifest file automatically generated and included in the JAR. If the SPI manifest is missing the plugin will not be loaded!

Example:

package com.example.my.c2id.plugin;

import org.kohsuke.MetaInfServices;
import com.nimbusds.openid.connect.provider.spi.claims.ClaimsSource;

@MetaInfServices
public class MyClaimsSource implements ClaimsSource {

    // Implementing code
}

For the above example the generated SPI manifest file will be located at

/META-INF/services/com.nimbusds.openid.connect.provider.spi.claims.ClaimsSource

and its content will be the class name of the implementation:

com.example.my.c2id.plugin.MyClaimsSource

3. Deployment

Add the generated plugin JAR to the /WEB-INF/lib/ directory of the Connect2id server. If the server is deployed as a WAR package, e.g. c2id.war, the plugin needs to be added to the package at the given directory.

4. Troubleshooting

Logging of the plugin loading

The Connect2id server logs the loading of every available plugin under a unique code given in the SPI documentation. The makes it easy to locate the loading event in the server logs.

Example:

2017-08-07T13:39:44,634 INFO localhost-startStop-1 MAIN - [OP7101] Loaded claims source [1]: class com.nimbusds.openid.connect.provider.spi.claims.ldap.LDAPClaimsSource
2017-08-07T13:39:44,635 INFO localhost-startStop-1 MAIN - [OP7102] Claims supported by source [1]: sub email_verified address x_employee_number name nickname phone_number_verified phone_number preferred_username given_name family_name email

Single vs multiple enabled plugins for an SPI

Some Connect2id server SPIs allow only a single plugin to be enabled for the SPI, others multiple. This is indicated in the SPI documentation.

If for a given SPI multiple plugins enabled, but the SPI expects at most one enabled plugin, the Connect2id server aborts startup with an error.

Logging

The Connect2id server uses the Log4j framework. The plugin can use it to log events and diagnostic messages.