[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