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 (SPI) for collecting claims from one or more data sources, such as

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.

  • Provides access to the subject session where the claims sourcing was authorised.

  • Enables implementations to release resources on Connect2id server shutdown.

  • Is executed synchronously, in a Java Servlet.

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 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 its configuration manual for details.

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

4. How to develop your own claims source connector

First, read our general guide for developing, annotating and packaging an SPI-based plugin.

The connector must implement the ClaimsSource or AdvancedClaimsSource SPI defined in the Connect2id server SDK:

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

The AdvancedClaimsSource interface 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 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.

    Support for a pattern of claims can be indicated with the * wildcard character, for example as https://idp.example.com/* for a set of claims having a common URI prefix. Returning a single claim set to * indicates support for all claims supported by the OpenID provider without explicitly listing them.

  • 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 method.

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

If the Connect2id server detects an SPI implementation at startup it will log its loading under OP7101 or OP7104:

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 Accessing the subject session

Starting with Connect2id server 14.0 the AdvancedClaimsSource provides lazy access to the subject session where the claims sourcing was authorised.

The session can be used to source selected claims, such as:

  • Claims related to or derived from the user authentication;

  • Claims saved into the claims session field during the user authentication / session creation.

The session is supplied in the following cases:

  • Claims sourcing for the UserInfo endpoint where the subject session where the claims consent occurred is still present (not expired or closed);

  • Claims sourcing for ID token issue for an OAuth 2.0 authorisation code, implicit (including OpenID Connect hybrid response type) and refresh token grants;

  • Claims sourcing for a direct authorisation request where a valid subject session ID was supplied, or a new subject session was created.

The claims source can access the session via the ClaimsSourceRequestContext.getSubjectSession method.

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.provider.spi.internal.sessionstore.SubjectSession;
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 {


        // Returns the associated subject (end-user) session where the claims
        // sourcing was authorised.
        SubjectSession session = requestContext.setSubjectSession();

        if (session != null) {
            // The sessions is available / present, do something with it
            // ...
        }
    }
}

5.8 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.9 Logging

At startup the Connect2id server logs each claims source that it detects and then loads. Search for lines with the OP7101, OP7102 and OP7104 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.10 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.11 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.12 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.13 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.