OpenID Federation 1.0

In a OpenID Federation relying parties and identity providers establish trust between themselves based on one or more common 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 v11.0 of the SDK.

Trust chain resolution

"Can I trust you?" is what a federation 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 trust anchor.

For an OpenID provider it means finding a valid trust chain for the RP.

The example below is taken from the OpenID Federation 1.0 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());

The public JWK set of the trust anchor can alternatively be configured directly, when the trust anchor keys are obtained securely out of band. The trust chain resolution then won't rely on the security of the DNS, the PKIX and the TLS for the federation endpoint of the trust anchor; in case of an incident where those get compromised the trust chain resolution will remain intact.

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

// The configured trust anchor public keys
JWKSet trustAnchorKeys = new JWKSet(...);

// Create a trust chain resolver
TrustChainResolver resolver = new TrustChainResolver(trustAnchor, trustAnchorKeys);

In multilateral OpenID federations an entity can have more than one trust anchor. To cater for such setups and also constrain (filter) the resolved trust chains:

// Configure the trust anchors and their public keys
Map<EntityID,JWKSet> trustAnchors = new HashMap<>();
trustAnchors.put(new EntityID("http://anchor-a.example.com", new JWKSet(...));
trustAnchors.put(new EntityID("http://anchor-b.example.com", new JWKSet(...));

// Configure constraints
int maxPathLength = 3;
TrustChainConstraints constraints = new TrustChainConstraints(maxPathLength);

// Create the trust chain resolver
TrustChainResolver resolver = new TrustChainResolver(
    trustAnchors,
    constraints,
    new DefaultEntityStatementRetriever());

// Continue as usual...

To use a custom HTTP client for the entity statement retrieval:

// Create the custom HTTP client
HTTPRequestSender httpRequestSender = new ApacheHTTPClient();

// Create the trust chain resolver
TrustChainResolver resolver = new TrustChainResolver(
    trustAnchors,
    TrustChainConstraints.NO_CONSTRAINTS,
    new DefaultEntityStatementRetriever(httpRequestSender));

// Continue as usual...

OpenID authentication request with automatic client registration

A relying party that wants to authenticate an end-user with an OpenID provider in a federation can do so without a previous registration step, if the OpenID provider supports the so called automatic client registration.

Automatic client registration requires:

  • The client_id in the authentication request to be set to the entity ID of the relying party, e.g. to https://wiki.ligo.org.

  • The request to be signed with a key found the relying party's entity configuration. This can be done either with a request object (JAR), or a pushed authorisation request (PAR) authenticated with a method utilising a signature, such as private_key_jwt or MTLS.

Example authentication request with a request object (JAR):

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(
    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', '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())
    .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 and 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

How to compose an entity configuration

Every entity in a federation must publish a self-signed statement containing its metadata (configuration), public keys (in a JWK set) and immediate authority in the trust chain (unless the entity is a trust anchor).

If the entity is an OpenID relying party its must included the appropriate metadata. An OpenID provider will use this data to perform the automatic registration of the relying party when processing the OpenID authentication request.

An OpenID provider will similarly include its own metadata in its entity configuration.

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(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());

How to compose a statement about a subordinate

An authority, such as a trust anchor or intermediate, can compose a federation statement about a subordinate in a similar fashion, by setting the iss (issuer) claim to its own entity ID and using its own authority key to sign the it. The metadata fields must not be set, unless the authority needs to assert some information about the subordinate, for example its legal name.

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

How to compose and parse metadata policies

A federation can publish policies that apply to its participants, such as 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 Federation 1.0 specification allows the definition of custom policy operations, to implement one override the default PolicyOperationFactory and the default PolicyOperationCombinationValidator.