Claims source SPI

1. OpenID Connect claims

OpenID Connect enables client applications to retrieve claims (attributes), about the end-user after successful login and consent. The application has a choice of two methods to receive the requested claims:

  • From the protected UserInfo endpoint (the default method, requires an access token);
  • Included in the issued ID token.

Example claims about a logged-in user, typically delivered in JSON format:

{
  "sub"            : "248289761001",
  "name"           : "Jane Doe",
  "given_name"     : "Jane",
  "family_name"    : "Doe",
  "email"          : "[email protected]",
  "email_verified" : true,
  "picture"        : "https://example.com/janedoe/me.jpg"
}

2. Claims source SPI

The Connect2id server comes with a plugin interface (Java Service Provider Interface, or SPI) for collecting claims from one or more data sources, such as

Below is a step-by-step guide how to create a connector implementing the ClaimsSource or AdvancedClaimsSource SPIs defined in the Connect2id server SDK:

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

Features of the claims source SPI:

  • Provides an initialisation method, can be used to configure the source or establish database connections.

  • Method to let the Connect2id server query the names of the claims supported by the connector.

  • Can handle language tags (RFC 5646).

  • Supports all OpenID claim types:

    • normal -- Claims directly asserted by the provider.

    • aggregated -- Claims sourced from an external provider, made available as a signed JWT.

    • distributed -- Claims which can be obtained from the endpoint of an external provider using a supplied bearer access token.

    • verified -- Claims for eKYC / Identity Assurance.

  • Optional "claims_data" set during consent or by an OAuth 2.0 grant handler to assist construction of the claims set.

  • Enables implementations to release resources on Connect2id server shutdown.

3. Available claims source connectors

Connect2id provides two ready connectors for sourcing claims. Their code is open (Apache 2.0 license), so you can use them as a starting point to devise your own claims sourcing.

3.1 LDAP

An LDAP / Active Directory connector is included in Connect2id server package. Check out its configuration manual for details.

Git repohttps://bitbucket.org/connect2id/openid-connect-ldap-claims-source

3.2 HTTP endpoint

This connector retrieves claims from a protected HTTP endpoint. Check out the configuration manual for details.

Git repohttps://bitbucket.org/connect2id/openid-connect-http-claims-source

4. How to develop your own claims source connector

How to implement a claims source connector from scratch:

  1. Get the Connect2id server SDK. Its Maven artifact is com.nimbusds:c2id-server-sdk and is publicly available from the Maven Central repo. The SDK version should match the dependency for the particular Connect2id server that you have. You can find out the expected SDK version in the pom.xml shipped with the c2id.war under /META-INF/maven/com.nimbusds/c2id/pom.xml.

    For Connect2id server 9.3:

    <dependency>
       <groupId>com.nimbusds</groupId>
       <artifactId>c2id-server-sdk</artifactId>
       <version>4.20</version>
    </dependency>
    

    If you're unsure which SDK version you need write to our support.

  2. Implement the ClaimsSource interface in your connector or the AdvancedClaimsSource interface which exposes additional features, such as access to optional "claims_data" set during the consent.

    Explanation of the methods:

    • init(InitContext) -- This method is called by the Connect2id server at startup to initialise the connector. The connector can use this method to read a configuration file with some database details, create a connection pool, etc. If there is a need to implement a claims caching strategy the context object provides access to the Infinispan instance embedded in the Connect2id server. If there is no need to initialise the connector leave it at the default.

    • isEnabled -- For the Connect2id server to check whether the connector is enabled and can be used.

    • supportedClaims -- Returns the names of the claims provided by the connector. The Connect2id server uses this information to find out which connector to call when multiple connectors are present.

    • getClaims(Subject, Set<String>, List<LangTag>) -- Performs the actual claims retrieval for a subject (end-user). Inside the method the connector is supposed to make a database request (SQL query, RESTful call, etc) to retrieve the requested claims. Return null if the end-user wasn't found. If some claims are not available or withheld for some reason they need not be returned.

    • shutdown -- Called by the Connect2id server on shutdown. Intended to clean up resources, such as database connection pools created by the connector. If there is no need to clean up anything leave it at the default.

    Consult the Connect2id server SDK JavaDocs for more information, in particular the com.nimbusds.openid.connect.provider.spi.claims package.

  3. Package your classes (including third-party dependencies) in a JAR file with a manifest for the claims source SPI. The manifest is required to let the Connect2id server know that a connector is available and can be loaded. This happens at server startup.

    How to create the manifest:

    1. Create a folder named /META-INF/services/.

    2. Create a new text file com.nimbusds.openid.connect.provider.spi.claims.ClaimsSource in this folder. The name of this file is the fully qualified name of the ClaimsSource interface.

    3. Inside the text file put the fully qualified name of the connector class implementing the ClaimsSource interface. For example:

      com.myapp.MyClaimsSourceConnector
      
  4. Put the JAR (classes plus manifest) in the WEB-INF/lib directory of the Connect2id server (or WAR if packaged).

  5. Restart the Connect2id server.

  6. The Connect2id server will automatically load the new claims source and add it to the existing ones (if any). Check the logs for any issues.

5. Tips

5.1 Aggregated claims

How to return aggregated claims in a ClaimsSource implementation:

import java.net.*;
import java.util.*;
import com.nimbusds.jwt.*;
import com.nimbusds.oauth2.sdk.id.*;
import com.nimbusds.oauth2.sdk.token.*;
import com.nimbusds.openid.connect.provider.spi.*;
import com.nimbusds.openid.connect.provider.spi.claims.*;
import com.nimbusds.openid.connect.sdk.claims*;

UserInfo userInfo = new UserInfo(subject);

AggregatedClaims ac = new AggregatedClaims(
    // the claim names
    Set.of("credit_score", "credit_plan"),
    // the claim JWT, signed by the claim provider
    SignedJWT.parse(jwt)
);

userInfo.addAggregatedClaims(ac);

5.2 Distributed claims

How to return distributed claims in a ClaimsSource implementation:

import java.net.*;
import java.util.*;
import com.nimbusds.oauth2.sdk.id.*;
import com.nimbusds.oauth2.sdk.token.*;
import com.nimbusds.openid.connect.provider.spi.*;
import com.nimbusds.openid.connect.provider.spi.claims.*;
import com.nimbusds.openid.connect.sdk.claims*;

UserInfo userInfo = new UserInfo(subject);

DistributedClaims dc = new DistributedClaims(
    // the claim names
    Set.of("credit_score", "credit_plan"),
    // the claim endpoint
    URI.create("https://credit-score-agency.com/claims"),
    // the token to retrieve the claims
    new BearerAccessToken("Eethahb8ci6Eidii")
);

userInfo.addDistributedClaims(dc);

5.3 Passing the UserInfo access token to upstream claims providers

The claims source may reuse the UserInfo access token to retrieve aggregated and distributed claims from upstream claims providers, if there is an agreement in place to recognise the token upstream. Requires implementation of AdvancedClaimsSource to receive the ClaimsSourceRequestContext:

import java.util.List;
import java.util.Set;
import com.nimbusds.oauth2.sdk.id.Subject;
import com.nimbusds.openid.connect.provider.spi.claims.AdvancedClaimsSource;
import com.nimbusds.openid.connect.provider.spi.claims.ClaimsSourceRequestContext;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;

public class MyClaimsSource implements AdvancedClaimsSource {


    @Override
    public UserInfo getClaims(final Subject subject,
                              final Set<String> claims,
                              final List<LangTag> claimsLocales,
                              final ClaimsSourceRequestContext requestContext)
        throws Exception {


        // If the claims request was triggered by a UserInfo request the 
        // submitted and successfully validated token can be retrieved
        AccessToken userInfoToken = requestContext.getUserInfoAccessToken();

        // Pass the token to upstream providers...
    }
}

5.4 Verified claims for eKYC / Identity Assurance

Check the guide for implementing verified claims provision.

5.5 Handling claims requests with language tags

Client applications can request the claims to be returned in a given language or script. This is facilitated by means of language tags (RFC 5646).

For a getClaims request:

  1. Check if the Set<String> claims argument contains tagged claim names, e.g. name#de-DE. Try to fulfill those claims in the requested language / script and return them with the same language tag set.

    UserInfo userinfo = new UserInfo(sub);
    userinfo.setName("Alice Adams", langTag);
    
  2. Check if the List<LangTag> claimsLocales argument is set and contains language tags. For each language tag in the list try to fulfill the plain (untagged) claims in Set<String> claim in this language / script. Again, return the claims with the appropriate language tag set.

The Nimbus LangTag library included in the Connect2id server runtime contains helpful methods for dealing with language tags. In particular, check the LangTagUtils. In Connect2id server v10.0+ it adds a split method that splits an optionally language tagged string into a string and language tag pair.

5.6 Accessing optional claims request data

A "claims_data" JSON object set during consent can be accessed like this:

import java.util.List;
import java.util.Set;
import org.minidev.json.JSONObject;
import com.nimbusds.oauth2.sdk.id.Subject;
import com.nimbusds.openid.connect.provider.spi.claims.AdvancedClaimsSource;
import com.nimbusds.openid.connect.provider.spi.claims.ClaimsSourceRequestContext;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;

public class MyClaimsSource implements AdvancedClaimsSource {


    @Override
    public UserInfo getClaims(final Subject subject,
                              final Set<String> claims,
                              final List<LangTag> claimsLocales,
                              final ClaimsSourceRequestContext requestContext)
        throws Exception {


        // The claims data can be obtained from the context object.
        JSONObject claimsData = requestContext.getClaimsData();
    }
}

5.7 How to return an HTTP error status when processing a claims request at the UserInfo endpoint

An exception thrown in the getClaims method will cause the UserInfo endpoint to return a 500 Internal Server Error HTTP status.

To cause the Connect2id server to return a different HTTP status code throw a com.nimbusds.oauth2.sdk.GeneralException from the OAuth 2.0 SDK, with an ErrorObject having the desired status code.

Example:

throw new GeneralException(new ErrorObject(
    "my_error_code",
    "My error message",
    444));

This will result in a HTTP response like this:

HTTP/1.1 444
Content-Type: application/json;charset=UTF-8

{
  "error"             : "my_error_code",
  "error_description" : "My error message"
}

5.8 Logging

At startup the Connect2id server logs each claims source that it detects and then loads. Search for lines with the OP7101 and OP7102 codes:

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

To aid debugging and monitoring consider adding logging to your claims source, using the Log4j2 library which is included in the Connect2id server WAR package.

5.9 The Connect2id server crashes when loading my claims source connector

If the Connect2id server fails to initialise the claims source for some reason it will abort startup with an exception. If you use Tomcat check the following log files for a record of the exception stack trace:

  • /tomcat/logs/c2id-server.log
  • /tomcat/logs/catalina.out
  • /tomcat/logs/[host]_access_log.YYYY-MM-DD.txt

Does your claims source connector have all dependency requirements satisfied? There are two approaches to that: merge the dependency classes into the connector JAR, or adding their JARs to the /WEB-INF/lib of the c2id web application. If a dependency is missing Tomcat will throw a java.lang.NoClassDefFoundError.

It's also good practice to test your connector before deployment, using a framework for automated tests.

5.10 How to monitor your connectors

The aggregate performance of the claims source connectors can be monitored via the claimsSource.retrievalTimer metric. This can help you identify latency and other problems that may in turn affect the overall performance of the IdP service.

Example claimsSource.retrievalTimer metric:

{
  "count"          : 6,
  "max"            : 0.01028953,
  "mean"           : 6.647419832286938E-4,
  "min"            : 6.56503E-4,
  "p50"            : 6.647430000000001E-4,
  "p75"            : 6.647430000000001E-4,
  "p95"            : 6.647430000000001E-4,
  "p98"            : 6.647430000000001E-4,
  "p99"            : 6.647430000000001E-4,
  "p999"           : 6.647430000000001E-4,
  "stddev"         : 9.152683616619665E-8,
  "m15_rate"       : 0.0016652947966130432,
  "m1_rate"        : 1.9309212342695557E-4,
  "m5_rate"        : 0.0015804743529140488,
  "mean_rate"      : 0.0015484305097827385,
  "duration_units" : "seconds",
  "rate_units"     : "calls/second"
}

Claims retrieval should be relatively swift, particularly if ID tokens are used to transport claims to the client apps (serving claims at the UserInfo endpoint with longer delays is less of an issue). If your claims sources incur long latencies you should consider caching their data. Let us know if you need assistance with that.

5.11 How to disable the LDAP connector

There are two possible ways to do that.

By removing the LDAP connector:

  1. Stop the Connect2id server.

  2. Remove oidc-claims-source-ldap-[version].jar from the WEB-INF/lib/ directory.

  3. Start the Connect2id server.

By disabling the LDAP connector from its settings:

  1. Open WEB-INF/ldapClaimsSource.properties

  2. Set op.ldapClaimsSource.enable = false

  3. Restart the Connect2id server.

  4. The Connect2id server will still load the LDAP claims source, but it will not be used.

See the LDAP connector config docs for details.

5.12 When does the claims source get invoked?

The claims source gets called in the following cases:

  • When the UserInfo endpoint processes a request.

  • To feed OpenID claims into an ID token:

    • For OpenID authentication requests with response_type=id_token when OpenID claims are requested via the scope parameter, for example with scope=email profile.
    • For OpenID authentication requests with the claims parameter set and in it having one or more OpenID claims requested to be included in the issued ID token.
  • To feed OpenID claims into an access token. This is triggered by prefixing the name of the consented claim with access_token: in consent.

  • By SPI plugins which for some internal reason need to call the claims source. Custom token codecs and custom token introspection are able to do this.

The above listed cases apply to Connect2id server v9.0. Newer server versions may invoke the claims source in additional situations. To receive an update for the most recent Connect2id server version contact support.

6. Support

Our Connect2id support team is available if you need help with integrating a particular claims source.