[java-idp-plugin-oidc-op-oidfed] branch main updated: Modify the signed keyset decorator to work with stored auto-registered RPs

Codeberg noreply at shibboleth.net
Thu Oct 1 17:01:58 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch main
in repository java-idp-plugin-oidc-op-oidfed.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-oidc-op-oidfed/commit/c499dccae8fadae93e51d682ca192993a4ab5393

The following commit(s) were added to refs/heads/main by this push:
     new c499dcc  Modify the signed keyset decorator to work with stored auto-registered RPs
c499dcc is described below

commit c499dccae8fadae93e51d682ca192993a4ab5393
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Oct 1 20:01:26 2026 +0300

    Modify the signed keyset decorator to work with stored auto-registered RPs
    
    - Parse the entity configuration from client information, if RelyingPartyTrustChainContext is not available
    - Cover in flow tests
---
 ...ientInformationSignedKeysetJWKSetDecorator.java | 42 +++++++++-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  3 +-
 .../AuthorizeFlowAutomaticRegistrationTest.java    | 90 ++++++++++++++++++++++
 ...shedAuthorizeFlowAutomaticRegistrationTest.java | 49 ++++++++++++
 4 files changed, 181 insertions(+), 3 deletions(-)

diff --git a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/ClientInformationSignedKeysetJWKSetDecorator.java b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/ClientInformationSignedKeysetJWKSetDecorator.java
index c39bdd8..c615ed6 100644
--- a/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/ClientInformationSignedKeysetJWKSetDecorator.java
+++ b/idp-oidfed-op-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oidfed/security/credential/ClientInformationSignedKeysetJWKSetDecorator.java
@@ -17,6 +17,7 @@ package net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.Function;
+import java.util.function.Supplier;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -26,6 +27,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
 import org.slf4j.Logger;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 
@@ -37,7 +39,9 @@ import net.shibboleth.oidfed.metadata.EntityConfiguration;
 import net.shibboleth.oidfed.metadata.cache.SubjectEntityStatementCriterion;
 import net.shibboleth.oidfed.metadata.cache.keyset.SignedKeysetContainer;
 import net.shibboleth.oidfed.metadata.cache.keyset.SubjectSignedKeysetUriCriterion;
+import net.shibboleth.oidfed.metadata.util.EntityStatementHelper;
 import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
+import net.shibboleth.oidfed.support.ClientInformationExtensionSupport;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
 import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
@@ -63,6 +67,9 @@ public class ClientInformationSignedKeysetJWKSetDecorator extends AbstractIdenti
     /** Strategy used to lookup the trust chain context. */
     @Nonnull protected Function<ProfileRequestContext, RelyingPartyTrustChainContext> trustChainContextLookupStrategy;
 
+    /** JSON object mapper used for decoding entity statement payload. */
+    @NonnullAfterInit protected ObjectMapper objectMapper;
+
     /**
      * Constructor.
      */
@@ -96,6 +103,15 @@ public class ClientInformationSignedKeysetJWKSetDecorator extends AbstractIdenti
                 Constraint.isNotNull(strategy, "TrustChainContextLookupStrategy cannot be null");
     }
 
+    /**
+     * Set the JSON {@link ObjectMapper} used for decoding entity statement payload.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
 
     /** {@inheritDoc} */
     protected void doInitialize() throws ComponentInitializationException {
@@ -103,6 +119,9 @@ public class ClientInformationSignedKeysetJWKSetDecorator extends AbstractIdenti
         if (signedKeysetCache == null) {
             throw new ComponentInitializationException("SignedKeysetCache cannot be null");
         }
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("Object mapper cannot be null");
+        }
     }
 
     /** {@inheritDoc} */
@@ -138,9 +157,9 @@ public class ClientInformationSignedKeysetJWKSetDecorator extends AbstractIdenti
                 .map(trustChain -> trustChain.get(0))
                 .filter(EntityConfiguration.class::isInstance)
                 .map(EntityConfiguration.class::cast)
-                .orElse(null);
+                .orElseGet(clientInformationEntityConfigurationResolver(clientInformation));
         if (entityConfiguration == null) {
-            log.error("Could not resolve entity configuration, unable to proceed");
+            log.warn("Could not resolve entity configuration, unable to proceed");
             return;
         }
         log.trace("Resolved entity configuration {}", entityConfiguration.getSubject());
@@ -171,4 +190,23 @@ public class ClientInformationSignedKeysetJWKSetDecorator extends AbstractIdenti
         log.trace("Signed keys {} successfully populated for {}", signedKeys, entityConfiguration.getSubject());
     }
 
+    /**
+     * Create a supplier of entity configuration from the client information extensions.
+     * 
+     * @param clientInformation the information containing extensions
+     * @return supplier of entity configuration, if a trust chain existed in the client information
+     */
+    protected Supplier<EntityConfiguration> clientInformationEntityConfigurationResolver(
+            @Nonnull final OIDCClientInformation clientInformation) {
+        return () -> {
+            return Optional.ofNullable(ClientInformationExtensionSupport.parseValidatedTrustChain(clientInformation))
+                    .map(strings -> {
+                        assert strings != null; assert objectMapper != null;
+                        return EntityStatementHelper.deserializeTrustChain(strings, objectMapper);
+                    })
+                    .map(trustChain -> trustChain.get(0))
+                    .filter(EntityConfiguration.class::isInstance).map(EntityConfiguration.class::cast)
+                    .orElse(null);
+        };
+    }
 }
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index a45f92a..c8a0eb5 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -132,7 +132,8 @@
 
     <bean id="ClientInformationSignedKeysetJWKSetDecorator"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.security.credential.ClientInformationSignedKeysetJWKSetDecorator"
-        p:signedKeysetCache-ref="#{'%{idp.oidfed.SignedKeysetMetadataCache:shibboleth.oidfed.SignedKeysetMetadataCache}'.trim()}" />
+        p:signedKeysetCache-ref="#{'%{idp.oidfed.SignedKeysetMetadataCache:shibboleth.oidfed.SignedKeysetMetadataCache}'.trim()}"
+        p:objectMapper-ref="shibboleth.oidfed.JWTPayloadJSONObjectMapper" />
 
     <bean id="AutomaticRegistrationCondition"
         class="net.shibboleth.idp.plugin.oidc.op.oidfed.profile.logic.DefaultAutomaticRegistrationActivationCondition">
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
index 097a095..b04b3c7 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AuthorizeFlowAutomaticRegistrationTest.java
@@ -31,6 +31,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
+import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.storage.StorageService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -48,13 +49,18 @@ import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.token.AccessToken;
 import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
 import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
 
 import net.shibboleth.idp.plugin.oidc.op.profile.flow.AuthorizeFlowTest;
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.TokenFlowTest;
 import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultPushedAuthorizationRequestUriSerializationFunction;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
 import net.shibboleth.idp.session.SessionException;
 import net.shibboleth.oidfed.testing.FederationJwtSupport;
@@ -622,6 +628,54 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
         Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
     }
 
+    @Test
+    public void testWithPushedValidTrustChain_signedRequestObject_signedJwksUri() throws IOException,
+            UnsupportedOperationException, URISyntaxException, JOSEException, ParseException, DataSealerException {
+        final String clientId = uniqueClientId();
+        final String signedJwksUri = clientId + "/jwks.jwt";
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setSignedJWKSetURI(URI.create(signedJwksUri));
+
+        final List<String> trustChain = List.of(
+                rpEntityConfiguration(clientId, metadata),
+                subordinateStatement(clientId, Map.of("openid_relying_party",
+                        new OIDCClientMetadata().toJSONObject())),
+                trustedAnchorConfiguration());
+        final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType("entity-statement+jwt"))
+                .keyID(rpKey.getKeyID())
+                .customParam("trust_chain", trustChain)
+                .build();
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(clientId).subject(clientId)
+                .issueTime(Date.from(Instant.now()))
+                .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+        final String signedKeyset = FederationJwtSupport.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "jwk-set+jwt", builder.build()).serialize();
+        
+        mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+
+        final FlowExecutionResult result =
+                launchAuthenticationRequest(clientId, "openid profile", signedRequestObject(Map.of(
+                        "iss", clientId,
+                        "client_id", clientId,
+                        "aud", issuer,
+                        "exp", Instant.now().plus(Duration.ofMinutes(5)).getEpochSecond(),
+                        "jti", UUID.randomUUID(),
+                        "response_type", "code",
+                        "scope", "openid profile",
+                        "redirect_uri", redirectUri), header, rpKey));
+        final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+        final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+        Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+        Assert.assertNull(successResponse.getIDToken());
+        Assert.assertNull(successResponse.getAccessToken());
+        final String authorizationCode = successResponse.getAuthorizationCode().getValue();
+        Assert.assertNotNull(authorizationCode);
+        Assert.assertEquals(unwrapTrustChainFromAuthorizeCode(successResponse), List.of(clientId, anchorId));
+        verifyTokenEndpoint(clientId, authorizationCode);
+    }
+
     @Test
     public void testWithPushedValidTrustChain_signedRequestObject_expired()
             throws IOException, UnsupportedOperationException, URISyntaxException {
@@ -968,4 +1022,40 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
         request.setQueryString(query.toString());
     }
 
+    protected void verifyTokenEndpoint(final String clientId, final String code)
+            throws JOSEException, ParseException, DataSealerException {
+        initializeMocks();
+        initializeThreadLocals();
+        request.setMethod("GET");
+
+        final SignedJWT jwt = createPrivateKeyJWT(validJwtAuthenticationClaimsSet(clientId, issuer),
+                rpKey.toRSAKey().toRSAPrivateKey(), JWSAlgorithm.RS512);
+        final Map<String,String> requestParameters = new HashMap<>(Map.of(
+                "redirect_uri", redirectUri,
+                "grant_type", "authorization_code",
+                "code", code,
+                "client_id", clientId));
+        populateClientAssertionParams(requestParameters, jwt);
+        setHttpFormRequest("POST", requestParameters);
+        
+        flowExecutor.launchExecution(TokenFlowTest.FLOW_ID, null, externalContext);
+        final ProfileRequestContext prc =
+                (ProfileRequestContext)request.getAttribute(ProfileRequestContext.BINDING_KEY);
+        assert prc != null;
+        final Object rawResponse = prc.ensureOutboundMessageContext().getMessage();
+        if (rawResponse instanceof OIDCTokenResponse response) {
+            final AccessToken accessToken = response.getTokens().getAccessToken();
+            assert accessToken != null;
+            final String value = accessToken.getValue();
+            assert value != null;
+            final AccessTokenClaimsSet tokenClaims = AccessTokenClaimsSet.parse(value, getDataSealer());
+            final JWTClaimsSet claimsSet = tokenClaims.getClaimsSet();
+            assert claimsSet != null;
+            Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+            Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
+        } else {
+            Assert.fail("Unexpected response message: " + rawResponse);
+        }
+    }
+
 }
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
index aee6fd0..6a02573 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/PushedAuthorizeFlowAutomaticRegistrationTest.java
@@ -23,6 +23,7 @@ import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.Collection;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -67,6 +68,7 @@ import net.shibboleth.idp.session.SessionException;
 import net.shibboleth.oidfed.metadata.payload.claim.Metadata;
 import net.shibboleth.oidfed.profile.context.RelyingPartyTrustChainContext;
 import net.shibboleth.oidfed.profile.context.VerifiedTrustChain;
+import net.shibboleth.oidfed.testing.FederationJwtSupport;
 import net.shibboleth.oidfed.testing.FederationMetadataPolicyTestingSupport;
 import net.shibboleth.shared.collection.Pair;
 import net.shibboleth.shared.component.ComponentInitializationException;
@@ -459,6 +461,53 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString());
     }
 
+    @Test
+    public void testWithPublicClientWithSignedRequestObject_providedTrustChain_signedJwksUri() throws Exception {
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        metadata.setRedirectionURI(new URI(redirectUri));
+        metadata.setResponseTypes(Set.of(ResponseType.CODE));
+        metadata.setTokenEndpointAuthMethod(ClientAuthenticationMethod.NONE);
+        final String clientId = uniqueClientId();
+        final String signedJwksUri = clientId + "/jwks.jwt";
+        metadata.setSignedJWKSetURI(URI.create(signedJwksUri));
+
+        final List<String> trustChain = List.of(
+                rpEntityConfiguration(clientId, metadata),
+                subordinateStatement(clientId, Map.of("openid_relying_party",
+                        new OIDCClientMetadata().toJSONObject())),
+                trustedAnchorConfiguration());
+
+        final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256)
+                .type(new JOSEObjectType("entity-statement+jwt"))
+                .keyID(rpKey.getKeyID())
+                .customParam("trust_chain", trustChain)
+                .build();
+
+        final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder().issuer(clientId).subject(clientId)
+                .issueTime(Date.from(Instant.now()))
+                .claim("keys", new JWKSet(rpKey).toJSONObject(true).get("keys"));
+        final String signedKeyset = FederationJwtSupport.signedJwt(
+                JWSAlgorithm.RS256, leafKey, "jwk-set+jwt", builder.build()).serialize();
+
+        mapResponse(signedJwksUri, mockResponse(200, "application/jwk-set+jwt", signedKeyset));
+
+        setHttpFormRequest("POST", createRequestParameters(clientId, "openid", "code", signedRequestObject(Map.of(
+                "client_id", clientId,
+                "iss", clientId,
+                "aud", issuer,
+                "exp", Instant.now().plus(Duration.ofMinutes(5)).getEpochSecond(),
+                "jti", UUID.randomUUID(),
+                "response_type", "code",
+                "scope", "openid",
+                "redirect_uri", redirectUri), header, rpKey).serialize()));
+        final FlowExecutionResult result =
+                flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
+        assertSuccessResponse(result, clientId);
+        final PushedAuthorizationSuccessResponse response =
+                parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+        verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString());
+    }
+
     @Test
     public void testWithPublicClientWithSignedRequestObject_providedInvalidTrustChain() throws Exception {
         final OIDCClientMetadata metadata = new OIDCClientMetadata();

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list