OpenID Connect Federation 1.0

In a OpenID Connect Federation relying parties and identity providers establish trust between themselves based on one or more existing trust anchors.

The entities participating in a federation can be OpenID providers and relying parties, plain OAuth 2.0 clients, servers and protected resources, as well as organisations having authority over them.

The examples below assume v8.13 of the SDK.

Trust chain resolution

"Can I trust you?" is what an entity needs to find out before interacting with a peer.

For a relying party trying to sign-in a user with an OpenID provider it means finding out if the OpenID provider has a valid trust chain leading up to a recognised trust anchor.

For an OpenID provider before allowing a relying party to register with it means similarly finding a valid trust chain for the RP.

The example below is taken from OpenID Connect Federation spec and shows how a relying party finds out if an OpenID provider can be trusted.

import com.nimbusds.jose.jwk.*;
import com.nimbusds.openid.connect.sdk.federation.entities.*;
import com.nimbusds.openid.connect.sdk.federation.policy.*;
import com.nimbusds.openid.connect.sdk.federation.trust.*;
import com.nimbusds.openid.connect.sdk.op.*;

// The configured federation trust anchor URL
EntityID trustAnchor = new EntityID("https://edugain.geant.org");

// The entity ID (URL) of the OpenID provider to resolve
EntityID openIDProviderEntity = new EntityID("https://op.umu.se");

// Find out if there is a valid trust chain leading from the OpenID provider
// up to the configured trust anchor
TrustChainResolver resolver = new TrustChainResolver(trustAnchor);

TrustChainSet resolvedChains;
try {
    resolvedChains = resolver.resolveTrustChains(openIDProviderEntity);
} catch (ResolveException e) {
    // Couldn't resolve a valid trust chain
    System.err.println(e.getMessage());
    return;
}

// The process can theoretically resolve multiple chains if multiple achors
// are configured, choose the shortest
TrustChain chain = resolvedChains.getShortest();

// Get the policy for registering a relying party with the OpenID provider
MetadataPolicy metadataPolicy = chain.resolveCombinedMetadataPolicy();
System.out.println(metadataPolicy.toJSONObject());

OpenID authentication request with automatic client registration

OpenID providers that support automatic client registration make it possible for relying parties to skip the explicit registration step, and instead simply make an OpenID authentication request signed with a key found in the relying party's entity statement.

import java.net.*;
import com.nimbusds.jose.*;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.util.*;
import com.nimbusds.oauth2.sdk.auth.*;
import com.nimbusds.oauth2.sdk.id.*;
import com.nimbusds.openid.connect.sdk.*;

// The OpenID provider authorisation endpoint
URI endpoint = new URI("https://op.umu.se/authorize");

// The client_id must be set to the entity ID of the relying party (RP)
ClientID clientID = new ClientID("https://wiki.ligo.org");

// Key pair belonging to the RP entity
RSAKey rsaJWK = ...;

// Build the OpenID authentication request
AuthenticationRequest request = new AuthenticationRequest.Builder(
    new ResponseType("code"),
    new Scope("openid", "profile", "email"),
    clientID,
    new URI("https://wiki.ligo.org/openid/callback"))
    .state(new State("em9Yah2eevathieh"))
    .nonce(new Nonce("the5Sha1Aeraete1"))
    .endpointURI(endpoint)
    .build();

// Convert the OpenID authentication request to a JWT claims set
// and append the required 'iss', 'aud', 'sub', 'jti' and 'exp'
// claims
Date now = new Date();
Date exp = DateUtils.fromSecondsSinceEpoch(
    DateUtils.toSecondsSinceEpoch(now) + 60);
JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder(request.toJWTClaimsSet())
    .issuer(clientID.getValue())
    .audience(endpoint.toString())
    .subject(clientID.getValue())
    .jwtID(new JWTID().getValue())
    .expirationTime(exp)
    .build();

// Sign the request object JWT with the RP entity private key
SignedJWT jwt = new SignedJWT(new JWSHeader.Builder(
    (JWSAlgorithm)rsaJWK.getAlgorithm())
    .keyID(rsaJWK.getKeyID())
    .build(),
    jwtClaimsSet);
jwt.sign(new RSASSASigner(rsaJWK));

// Compose the final OpenID authentication request with the
// request object JWT the minimal other required top-level
// parameters
request = new AuthenticationRequest.Builder(jwt, clientID)
    .responseType(request.getResponseType())
    .scope(request.getScope())
    .endpointURI(endpoint)
    .build();

System.out.println(request.toURI());

// https://op.umu.se/authorize?
// response_type=code
// &client_id=https%3A%2F%2Fwiki.ligo.org
// &scope=openid+profile+email
// &request=eyJraWQiOiJLLVFLM0JTY0NWYTZXY2wzRHFDZXg3amQ0VFBMV1dhRkFXbnNiQnNUOGF
// nIiwiYWxnIjoiUlMyNTYifQ.eyJhdWQiOiJodHRwczpcL1wvb3AudW11LnNlXC9hdXRob3JpemUi
// LCJzdWIiOiJodHRwczpcL1wvd2lraS5saWdvLm9yZyIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUg
// ZW1haWwiLCJpc3MiOiJodHRwczpcL1wvd2lraS5saWdvLm9yZyIsInJlc3BvbnNlX3R5cGUiOiJj
// b2RlIiwicmVkaXJlY3RfdXJpIjoiaHR0cHM6XC9cL3dpa2kubGlnby5vcmdcL29wZW5pZFwvY2Fs
// bGJhY2siLCJzdGF0ZSI6ImVtOVlhaDJlZXZhdGhpZWgiLCJleHAiOjE1OTM1OTkxNzMsIm5vbmNl
// IjoidGhlNVNoYTFBZXJhZXRlMSIsImNsaWVudF9pZCI6Imh0dHBzOlwvXC93aWtpLmxpZ28ub3Jn
// IiwianRpIjoiM2pSTmdYRjB4RlZ3bjN2YXA4azhIa1RZWm5vVVRqR08yeTNjekxyb0xrTSJ9.Q69
// ncdGF8k8u0XxxMaUFu0Vr-9fEf7m66_C7-hQoXGSgXRnrJ-7kJ5A5FGv5pmdcJaIywD5c5bQYnuN
// bsTrUmPdZf2pIifUcr5-QftBQ7AqlCoIzypUD9fiqGsktqd6KsPVXKVtRWe_BIV_srb8QgBjBG3G
// ve24Tr58CPmMjgUgS_xarYP-RxNUwaeneaAkKjyq24bQypvcqM-mywF5UguNhWvc-HiJnkHJdGDF
// JeGvU2eVgMg_AWZ0XThpFwG6M4Vekvruiu1cv5xCos1cIlB8mrJgBRZ-7O3B3CxdVd9Lf6EBvNbe
// I1KY_6_VWk2JZz_mv2ysHGUbvT2tzxR539g

Composing an entity self-signed statement

Every entity in a OpenID Connect federation must publish a self-signed statement detailing its public keys and its immediate authority in the trust chain (unless the entity is a trust anchor).

If the entity is an OpenID relying party is must also include its client metadata in the statement. An OpenID provider will use this data to perform the automatic registration of the relying party during processing of the OpenID authentication request.

OpenID providers must similarly include their metadata in the statement.

The statement is published as a signed JWT at a /.well-known/openid-federation location relative to the identifying entity URL.

The code below reproduces the wiki.ligo.org relying party statement example from the spec:

import java.net.*;
import java.util.*;
import com.nimbusds.jose.*;
import com.nimbusds.jose.jwk.*;
import com.nimbusds.jose.jwk.gen.*;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.auth.*;
import com.nimbusds.oauth2.sdk.id.*;
import com.nimbusds.openid.connect.sdk.federation.entities.*;
import com.nimbusds.openid.connect.sdk.*;
import com.nimbusds.openid.connect.sdk.rp.*;

// The required entity statement parameters
EntityID iss = new EntityID("https://wiki.ligo.org");
EntityID sub = new EntityID(iss.getValue());

Date iat = new Date();
Date exp = ...;

RSAKey jwk = new RSAKeyGenerator(2048)
    .algorithm(JWSAlgorithm.RS256)
    .keyID("1")
    .keyUse(KeyUse.SIGNATURE)
    .generate();
JWKSet jwkSet = new JWKSet(jwk);

List<EntityID> authorities = Collections.singletonList(
    new EntityID("https://incommon.org"));

EntityStatementClaimsSet claims = new EntityStatementClaimsSet(
    iss,
    sub,
    iat,
    exp,
    jwkSet.toPublicJWKSet());
claims.setAuthorityHints(authorities);

// The relying party metadata
OIDCClientMetadata rpMetadata = new OIDCClientMetadata();
rpMetadata.setName("LIGO Wiki");
rpMetadata.setEmailContacts(Collections.singletonList("[email protected]"));
rpMetadata.setJWKSetURI(new URI("https://wiki.ligo.org/jwks.json"));
rpMetadata.setApplicationType(ApplicationType.WEB);
rpMetadata.setGrantTypes(new HashSet<>(Arrays.asList(GrantType.AUTHORIZATION_CODE, GrantType.REFRESH_TOKEN)));
rpMetadata.setResponseTypes(Collections.singleton(new ResponseType("code")));
rpMetadata.setRedirectionURI(new URI("https://wiki.ligo.org/callback"));
rpMetadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT);

stmt.setRPMetadata(rpMetadata);

// Sign the entity statement
EntityStatement entityStatement = EntityStatement.sign(
    claims,
    jwkSet.getKeyByID("1"));

// Output as signed JWT
System.out.println(entityStatement.getSignedStatement().serialize());

Composing a statement about a subordinate

A trust anchor or intermediate can compose a statement about a subordinate statement in a similar fashion, setting the iss (issuer) to the authority's entity ID and using an authority's key to sign the statement. The metadata fields must not be set however, it is obtained from the self-signed statement only.

EntityID iss = new EntityID("https://incommon.org");
EntityID sub = new EntityID("https://wiki.ligo.org");

EntityStatementClaimsSet claims = new EntityStatementClaimsSet(
    iss,
    sub,
    iat,
    exp,
    jwkSet.toPublicJWKSet());

EntityStatement entityStatement = EntityStatement.sign(claims, authorityJWK);

Composing and parsing metadata policies

Federations can publish common policies to apply to participating OpenID providers and relying parties.

Composing a metadata policy programmatically:

import java.util.*;
import com.nimbusds.jose.*;
import com.nimbusds.openid.connect.sdk.federation.policy.*;
import com.nimbusds.openid.connect.sdk.federation.policy.language.*;
import com.nimbusds.openid.connect.sdk.federation.policy.operations.*;
import com.nimbusds.openid.connect.sdk.rp.*;

MetadataPolicy policy = new MetadataPolicy();

// Policy for the scope values
SubsetOfOperation subsetOf = new SubsetOfOperation();
subsetOf.configure(Arrays.asList("openid", "eduperson", "phone"));
SupersetOfOperation supersetOf = new SupersetOfOperation();
supersetOf.configure(Collections.singletonList("openid"));
DefaultOperation defaultOp = new DefaultOperation();
defaultOp.configure(Arrays.asList("openid", "eduperson"));

List<PolicyOperation> ops = new LinkedList<>();
ops.add(subsetOf);
ops.add(supersetOf);
ops.add(defaultOp);

policy.put("scopes", ops);

// Policy for the ID token JWS algs
OneOfOperation oneOf = new OneOfOperation();
oneOf.configure(Arrays.asList(
    JWSAlgorithm.ES256.getName(),
    JWSAlgorithm.ES384.getName(),
    JWSAlgorithm.ES512.getName()));

policy.put("id_token_signed_response_alg", oneOf);

// Policy for the contacts
AddOperation addOp = new AddOperation();
addOp.configure("[email protected]");

policy.put("contacts", addOp);

// Policy for the application type
ValueOperation valueOp = new ValueOperation();
valueOp.configure(ApplicationType.WEB.toString());

policy.put("application_type", valueOp);

// To print the metadata policy
String json = policy.toJSONString();

To parse a metadata policy:

// To parse a metadata policy
policy = MetadataPolicy.parse(json);

To apply a policy to some metadata:

try {
    JSONObject out = policy.apply(metadata);
} catch (PolicyViolationException e) {
    System.err.println(e.getMessage());
}

The OpenID Connect Federation specification allows for custom policy operations, to implement one override the default PolicyOperationFactory and the default PolicyOperationCombinationValidator.