[java-idp-plugin-oidc-op-oidfed] 02/02: Wired new properties to control automatic registration to token and userinfo

Henri Mikkonen henri.mikkonen at iki.fi
Thu Oct 9 10:36:43 UTC 2025


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

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

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-oidc-op-oidfed.git;a=commit;h=b6ebb6dedc66183e7e62beafb036e2073582c6a3

commit b6ebb6dedc66183e7e62beafb036e2073582c6a3
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Oct 9 13:36:21 2025 +0300

    Wired new properties to control automatic registration to token and userinfo
    
    - idp.oidfed.token.automaticRegistrationCondition and idp.oidfed.userinfo.automaticRegistrationCondition
      - Both defaults to shibboleth.Conditions.FALSE
    - If condition returns true, the unwrapped claims set (authorization code or access token) must contain the trustchain-claim
      - ClaimsSetExtensionSupport.KEY_AUTO_REGISTERED_TRUST_CHAIN
    - Improved testing
---
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  42 ++++
 .../oidfed/metadata-lookup-ext-oidfed-flow.xml     |   2 +-
 .../flow/oidfed/AbstractFederationFlowTest.java    |  18 ++
 .../AuthorizeFlowAutomaticRegistrationTest.java    |   6 +-
 ...shedAuthorizeFlowAutomaticRegistrationTest.java |  26 +--
 .../oidfed/TokenFlowAutomaticRegistrationTest.java | 211 +++++++++++++++++++++
 .../UserInfoFlowAutomaticRegistrationTest.java     | 168 ++++++++++++++++
 .../net/shibboleth/idp/module/conf/oidc.properties |   2 +
 .../shibboleth/idp/module/conf/relying-party.xml   |   1 +
 9 files changed, 450 insertions(+), 26 deletions(-)

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 7291558..2bae86d 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
@@ -457,6 +457,12 @@
                 <entry key="#{T(net.shibboleth.oidc.profile.oauth2.config.OAuth2PushedAuthorizationRequestConfiguration).PROFILE_ID}">
                     <ref bean="shibboleth.oidfed.par.DefaultAutomaticRegistrationCondition"/>
                 </entry>
+                <entry key="#{T(net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenConfiguration).PROFILE_ID}">
+                    <ref bean="shibboleth.oidfed.token.DefaultAutomaticRegistrationCondition"/>
+                </entry>
+                <entry key="#{T(net.shibboleth.oidc.profile.config.OIDCUserInfoConfiguration).PROFILE_ID}">
+                    <ref bean="shibboleth.oidfed.userinfo.DefaultAutomaticRegistrationCondition"/>
+                </entry>
             </util:map>
         </property>
     </bean>
@@ -483,6 +489,24 @@
         </constructor-arg>
     </bean>
 
+    <bean id="shibboleth.oidfed.token.DefaultAutomaticRegistrationCondition" parent="shibboleth.Conditions.AND">
+        <constructor-arg>
+            <list>
+                <ref bean="%{idp.oidfed.token.automaticRegistrationCondition:shibboleth.Conditions.FALSE}"/>
+                <bean parent="shibboleth.Conditions.Expression"
+                    c:expression="#input.ensureInboundMessageContext().getMessage() instanceof T(com.nimbusds.oauth2.sdk.TokenRequest)" />
+                <bean parent="shibboleth.Conditions.Expression"
+                    c:expression="#input.getOutboundMessageContext() != null"/>
+                <bean parent="shibboleth.Conditions.Expression"
+                    c:expression="#input.ensureOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)) != null"/>
+                <bean parent="shibboleth.Conditions.Expression"
+                    c:expression="#input.ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAuthorizationGrantClaimsSet() != null"/>
+                <bean parent="shibboleth.Conditions.Expression"
+                    c:expression="#input.ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAuthorizationGrantClaimsSet().getClaimsSet().getClaim(T(net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClaimsSetExtensionSupport).KEY_AUTO_REGISTERED_TRUST_CHAIN) != null"/>
+            </list>
+        </constructor-arg>
+    </bean>
+
     <bean id="shibboleth.oidfed.par.DefaultAutomaticRegistrationCondition" parent="shibboleth.Conditions.AND">
         <constructor-arg>
             <list>
@@ -515,6 +539,24 @@
         </constructor-arg>
     </bean>
 
+    <bean id="shibboleth.oidfed.userinfo.DefaultAutomaticRegistrationCondition" parent="shibboleth.Conditions.AND">
+        <constructor-arg>
+            <list>
+                <ref bean="%{idp.oidfed.userinfo.automaticRegistrationCondition:shibboleth.Conditions.FALSE}"/>
+                <bean parent="shibboleth.Conditions.Expression"
+                    c:expression="#input.ensureInboundMessageContext().getMessage() instanceof T(com.nimbusds.openid.connect.sdk.UserInfoRequest)" />
+                <bean parent="shibboleth.Conditions.Expression"
+                    c:expression="#input.getOutboundMessageContext() != null"/>
+                <bean parent="shibboleth.Conditions.Expression"
+                    c:expression="#input.ensureOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)) != null"/>
+                <bean parent="shibboleth.Conditions.Expression"
+                    c:expression="#input.ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAuthorizationGrantClaimsSet() != null"/>
+                <bean parent="shibboleth.Conditions.Expression"
+                    c:expression="#input.ensureOutboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAuthorizationGrantClaimsSet().getClaimsSet().getClaim(T(net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClaimsSetExtensionSupport).KEY_AUTO_REGISTERED_TRUST_CHAIN) != null"/>
+            </list>
+        </constructor-arg>
+    </bean>
+
     <bean class="net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.RequestObjectClaimsValidator">
         <constructor-arg>
             <bean id="shibboleth.oidfed.DefaultRequestObjectClaimsValidation"
diff --git a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
index bbe8d1a..8c5cb3e 100644
--- a/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
+++ b/idp-oidfed-op-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/metadata-lookup-ext/oidfed/metadata-lookup-ext-oidfed-flow.xml
@@ -26,7 +26,7 @@
 
     <decision-state id="CheckIfFallbackToLocalResolution">
         <if test="FallbackToLocalResolutionCondition.test(opensamlProfileRequestContext)"
-            then="ResolveTrustChains" else="proceed" />
+            then="ResolveTrustChains" else="NoTrustChainsResolved" />
     </decision-state>
 
     <action-state id="ResolveTrustChains">
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
index baab2b2..fe61a3d 100644
--- a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/AbstractFederationFlowTest.java
@@ -105,6 +105,8 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
     JWK trustedAnchorKey;
     JWK intermediateKey;
 
+    String subject = "jdoe";
+
     @Autowired
     @Qualifier("shibboleth.oidfed.HttpClient")
     HttpClient federationHttpClient;
@@ -614,6 +616,22 @@ public class AbstractFederationFlowTest extends AbstractOidcFlowTest {
         Assert.assertTrue(errorResponse.getErrorObject().getDescription().contains(errorDescription), message);
     }
 
+    protected void populateClientAssertionParams(final Map<String, String> requestParameters, 
+            final JWT jwt) {
+        requestParameters.put("client_assertion", jwt.serialize());
+        requestParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
+    }
+
+    protected JWTClaimsSet validJwtAuthenticationClaimsSet(final String clientId, final String audience) {
+        return new JWTClaimsSet.Builder()
+                .subject(clientId)
+                .issuer(clientId)
+                .audience(audience)
+                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
+                .jwtID(idGenerator.generateIdentifier())
+                .build();
+    }
+
     protected class RequestUriMatcher implements ArgumentMatcher<ClassicHttpRequest> {
 
         @Nonnull private final String uri;
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 12d8aae..8e7de38 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
@@ -543,7 +543,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
         setQueryParameters(request, requestParameters);
         storeMetadata(storageService, clientId, clientSecret, Scope.parse("openid profile"), redirectUri);
 
-        setBasicAuth("jdoe", "changeit");
+        setBasicAuth(subject, "changeit");
         initializeThreadLocals();
 
         final FlowExecutionResult result =
@@ -590,7 +590,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
                 new Pair<>("request", requestObject == null ? "" : requestObject.serialize())));
         request.setMethod("GET");
 
-        setBasicAuth("jdoe", "changeit");
+        setBasicAuth(subject, "changeit");
         initializeThreadLocals();
         
         return flowExecutor.launchExecution(AuthorizeFlowTest.FLOW_ID, null, externalContext);
@@ -604,7 +604,7 @@ public class AuthorizeFlowAutomaticRegistrationTest extends AbstractFederationFl
                 new Pair<>("request_uri", "" + requestUri)));
         request.setMethod("GET");
 
-        setBasicAuth("jdoe", "changeit");
+        setBasicAuth(subject, "changeit");
         initializeThreadLocals();
         
         return flowExecutor.launchExecution(AuthorizeFlowTest.FLOW_ID, null, externalContext);
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 94b6016..2bea9e1 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
@@ -21,7 +21,6 @@ import java.security.PublicKey;
 import java.text.ParseException;
 import java.time.Duration;
 import java.time.Instant;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -40,7 +39,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.jwk.JWKSet;
 import com.nimbusds.jwt.JWT;
-import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.OAuth2Error;
 import com.nimbusds.oauth2.sdk.PushedAuthorizationSuccessResponse;
@@ -81,7 +79,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
     public void testSuccess() throws Exception {
         final String clientId = uniqueClientId();
         rpConfigureMockHttpClient(clientId);
-        final SignedJWT jwt = createPrivateKeyJWT(validClaimsSet(clientId, issuer),
+        final SignedJWT jwt = createPrivateKeyJWT(validJwtAuthenticationClaimsSet(clientId, issuer),
                 rpKey.toRSAKey().toRSAPrivateKey(), JWSAlgorithm.RS512);
         final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
                 ClientAuthenticationMethod.PRIVATE_KEY_JWT, rpKey.toRSAKey().toPublicKey());
@@ -98,7 +96,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         metadata.setRedirectionURI(new URI(redirectUri));
         metadata.setJWKSet(new JWKSet(rpKey.toPublicJWK()));
         rpConfigureMockHttpClient(clientId, rpEntityConfigurationUnmatchingKey(clientId, metadata));
-        final SignedJWT jwt = createPrivateKeyJWT(validClaimsSet(clientId, issuer),
+        final SignedJWT jwt = createPrivateKeyJWT(validJwtAuthenticationClaimsSet(clientId, issuer),
                 rpKey.toRSAKey().toRSAPrivateKey(), JWSAlgorithm.RS512);
         final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
                 ClientAuthenticationMethod.PRIVATE_KEY_JWT, rpKey.toRSAKey().toPublicKey());
@@ -358,7 +356,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
             initializeThreadLocals();
             final String clientId = uniqueClientId();
             rpConfigureMockHttpClient(clientId, vector);
-            final SignedJWT jwt = createPrivateKeyJWT(validClaimsSet(clientId, issuer),
+            final SignedJWT jwt = createPrivateKeyJWT(validJwtAuthenticationClaimsSet(clientId, issuer),
                     rpKey.toRSAKey().toRSAPrivateKey(), JWSAlgorithm.RS256);
             final FlowExecutionResult result = launchWithJwtAuthentication(jwt, null,
                     ClientAuthenticationMethod.PRIVATE_KEY_JWT, rpKey.toRSAKey().toPublicKey());
@@ -384,7 +382,7 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
     protected void verifyAuthorizeEndpoint(final String clientId, final String requestUri, final String jkt) {
         initializeMocks();
         initializeThreadLocals();
-        setBasicAuth("jdoe", "changeit");
+        setBasicAuth(subject, "changeit");
         request.setMethod("GET");
 
         setRequestParameters(request, List.of(new Pair<>("client_id", clientId),
@@ -417,22 +415,6 @@ public class PushedAuthorizeFlowAutomaticRegistrationTest extends AbstractFedera
         return flowExecutor.launchExecution(PushedAuthorizeFlowTest.FLOW_ID, null, externalContext);
     }
 
-    protected void populateClientAssertionParams(final Map<String, String> requestParameters, 
-            final JWT jwt) {
-        requestParameters.put("client_assertion", jwt.serialize());
-        requestParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
-    }
-
-    protected JWTClaimsSet validClaimsSet(final String clientId, final String audience) {
-        return new JWTClaimsSet.Builder()
-                .subject(clientId)
-                .issuer(clientId)
-                .audience(audience)
-                .expirationTime(Date.from(Instant.now().plusSeconds(600)))
-                .jwtID(idGenerator.generateIdentifier())
-                .build();
-    }
-
     protected Map<String,String> createRequestParameters(final String id) {
         return createRequestParameters(id, "openid", "code", null);
     }
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/TokenFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/TokenFlowAutomaticRegistrationTest.java
new file mode 100644
index 0000000..0c5928b
--- /dev/null
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/TokenFlowAutomaticRegistrationTest.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed;
+
+import java.net.URI;
+import java.security.PublicKey;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.opensaml.storage.StorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.OAuth2Error;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClaimsSetExtensionSupport;
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.TokenFlowTest;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
+import net.shibboleth.shared.security.impl.SecureRandomIdentifierGenerationStrategy;
+
+public class TokenFlowAutomaticRegistrationTest extends AbstractFederationFlowTest {
+
+    @Autowired
+    @Qualifier("shibboleth.StorageService")
+    StorageService storageService;
+
+    public TokenFlowAutomaticRegistrationTest() {
+        super(TokenFlowTest.FLOW_ID);
+    }
+
+    @Test
+    public void testSuccess() throws Exception {
+        final String clientId = uniqueClientId();
+        rpConfigureMockHttpClient(clientId);
+        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", buildAuthorizationCode(clientId, List.of(clientId, anchorId)),
+                "client_id", clientId));
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS512,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT, null, requestParameters);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+        Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+        Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
+    }
+
+    @Test
+    public void testSuccess_resolveApi() throws Exception {
+        final String clientId = uniqueClientId();
+        rpResolveEntityConfigureMockHttpClient(clientId);
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        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", buildAuthorizationCode(clientId, List.of(clientId, anchorId)),
+                "client_id", clientId));
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS512,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT, null, requestParameters);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+        Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+        Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
+    }
+
+    @Test
+    public void testFails_resolveApiFails_noFallback() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        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", buildAuthorizationCode(clientId, List.of(clientId, anchorId)),
+                "client_id", clientId));
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS512,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT, null, requestParameters);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+        assertErrorDescriptionContains(result, "NoTrustChainsResolved");
+    }
+
+    @Test
+    public void testFails_resolveApiFails_fallbackFails() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        request.addHeader(USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION, "true");
+        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", buildAuthorizationCode(clientId, List.of(clientId, anchorId)),
+                "client_id", clientId));
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS512,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT, null, requestParameters);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+        assertErrorDescriptionContains(result, "NoTrustChainsResolved");
+    }
+
+    @Test
+    public void testSuccess_resolveApiFails_fallbackSuccess() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        request.addHeader(USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION, "true");
+        rpConfigureMockHttpClient(clientId);
+        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", buildAuthorizationCode(clientId, List.of(clientId, anchorId)),
+                "client_id", clientId));
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS512,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT, null, requestParameters);
+        final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+        Assert.assertNotNull(response.getTokens().getAccessToken());
+        Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+        Assert.assertNotNull(response.getOIDCTokens().getIDToken().getJWTClaimsSet().getClaim("at_hash"));
+    }
+
+    @Test
+    public void testFails_NonMatchingTrustChain() throws Exception {
+        final String clientId = uniqueClientId();
+        rpConfigureMockHttpClient(clientId);
+        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", buildAuthorizationCode(clientId, List.of(clientId, uniqueIntermediateId())),
+                "client_id", clientId));
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS512,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT, null, requestParameters);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+        assertErrorDescriptionContains(result, "NoTrustChainsResolved");
+    }
+
+    @Test
+    public void testFails_resolveApi_NonMatchingTrustChain() throws Exception {
+        final String clientId = uniqueClientId();
+        rpResolveEntityConfigureMockHttpClient(clientId);
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        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", buildAuthorizationCode(clientId, List.of(clientId, uniqueIntermediateId())),
+                "client_id", clientId));
+        final FlowExecutionResult result = launchWithJwtAuthentication(jwt, JWSAlgorithm.RS512,
+                ClientAuthenticationMethod.PRIVATE_KEY_JWT, null, requestParameters);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+        assertErrorDescriptionContains(result, "NoTrustChainsResolved");
+    }
+
+    protected FlowExecutionResult launchWithJwtAuthentication(final JWT jwt, final JWSAlgorithm algorithm,
+            final ClientAuthenticationMethod method, final PublicKey publicKey,
+            final Map<String, String> requestParameters) throws Exception {
+        // use 'iss' claim from JWT as clientId if set, 'sub' otherwise
+        populateClientAssertionParams(requestParameters, jwt);
+        setHttpFormRequest("POST", requestParameters);
+        return flowExecutor.launchExecution(TokenFlowTest.FLOW_ID, null, externalContext);
+    }
+
+    protected String buildAuthorizationCode(final String clientId, final Object trustChain) throws Exception {
+        final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+        builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+            .setClientID(new ClientID(clientId))
+            .setIssuer("https://op.example.org")
+            .setPrincipal(subject)
+            .setSubject("mock")
+            .setIssuedAt(Instant.now())
+            .setExpiresAt(Instant.now().plusSeconds(100))
+            .setAuthenticationTime(Instant.now())
+            .setRedirectURI(new URI(redirectUri))
+            .setScope(Scope.parse("openid profile"))
+            .addCustomClaim(ClaimsSetExtensionSupport.KEY_AUTO_REGISTERED_TRUST_CHAIN, trustChain);
+
+        return builder.build().serialize(getDataSealer());
+    }
+    
+}
diff --git a/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/UserInfoFlowAutomaticRegistrationTest.java b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/UserInfoFlowAutomaticRegistrationTest.java
new file mode 100644
index 0000000..c692231
--- /dev/null
+++ b/idp-oidfed-op-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/oidfed/UserInfoFlowAutomaticRegistrationTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.flow.oidfed;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.NoSuchAlgorithmException;
+import java.time.Instant;
+import java.util.List;
+
+import org.opensaml.storage.StorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.OAuth2Error;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.BearerTokenError;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
+
+import net.shibboleth.idp.plugin.oidc.op.oidfed.support.ClaimsSetExtensionSupport;
+import net.shibboleth.idp.plugin.oidc.op.profile.flow.UserInfoTest;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.security.DataSealerException;
+
+public class UserInfoFlowAutomaticRegistrationTest extends AbstractFederationFlowTest {
+
+    @Autowired
+    @Qualifier("shibboleth.StorageService")
+    StorageService storageService;
+    
+    public UserInfoFlowAutomaticRegistrationTest() {
+        super(UserInfoTest.FLOW_ID);
+    }
+
+    @BeforeMethod
+    public void removeAuthorizationHeader() throws IOException {
+        request.setMethod("GET");
+        request.removeHeader("Authorization");
+    }
+
+    @Test
+    public void testSuccess() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+        ComponentInitializationException, IOException {
+        final String clientId = uniqueClientId();
+        rpConfigureMockHttpClient(clientId);
+        final BearerAccessToken token = buildToken(clientId, List.of(clientId, anchorId));
+        request.addHeader("Authorization", token.toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(UserInfoTest.FLOW_ID, null, externalContext);
+        final UserInfoSuccessResponse response = parseSuccessResponse(result, UserInfoSuccessResponse.class);
+        Assert.assertEquals(response.getUserInfo().getSubject().getValue(), subject);
+        final UserInfo userInfo = response.getUserInfo();
+        Assert.assertNotNull(userInfo);
+        Assert.assertNull(userInfo.getEmailAddress());
+        Assert.assertNull(userInfo.getNickname());
+        Assert.assertNull(response.getUserInfoJWT());
+    }
+
+    @Test
+    public void testSuccess_resolveApi() throws Exception {
+        final String clientId = uniqueClientId();
+        rpResolveEntityConfigureMockHttpClient(clientId);
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        final BearerAccessToken token = buildToken(clientId, List.of(clientId, anchorId));
+        request.addHeader("Authorization", token.toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(UserInfoTest.FLOW_ID, null, externalContext);
+        final UserInfoSuccessResponse response = parseSuccessResponse(result, UserInfoSuccessResponse.class);
+        Assert.assertEquals(response.getUserInfo().getSubject().getValue(), subject);
+        final UserInfo userInfo = response.getUserInfo();
+        Assert.assertNotNull(userInfo);
+        Assert.assertNull(userInfo.getEmailAddress());
+        Assert.assertNull(userInfo.getNickname());
+        Assert.assertNull(response.getUserInfoJWT());
+    }
+
+    @Test
+    public void testFails_resolveApiFails_noFallback() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        final BearerAccessToken token = buildToken(clientId, List.of(clientId, anchorId));
+        request.addHeader("Authorization", token.toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(UserInfoTest.FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+        assertErrorDescriptionContains(result, "NoTrustChainsResolved");
+    }
+
+    @Test
+    public void testFails_resolveApiFails_fallbackFails() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        request.addHeader(USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION, "true");
+        final BearerAccessToken token = buildToken(clientId, List.of(clientId, anchorId));
+        request.addHeader("Authorization", token.toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(UserInfoTest.FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+        assertErrorDescriptionContains(result, "NoTrustChainsResolved");
+    }
+
+    @Test
+    public void testSuccess_resolveApiFails_fallbackSuccess() throws Exception {
+        final String clientId = uniqueClientId();
+        request.addHeader(USE_CUSTOM_RESOLVER_API_CONDITION, "true");
+        request.addHeader(USE_CUSTOM_FAILBACK_TO_LOCAL_CONDITION, "true");
+        rpConfigureMockHttpClient(clientId);
+        final BearerAccessToken token = buildToken(clientId, List.of(clientId, anchorId));
+        request.addHeader("Authorization", token.toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(UserInfoTest.FLOW_ID, null, externalContext);
+        final UserInfoSuccessResponse response = parseSuccessResponse(result, UserInfoSuccessResponse.class);
+        Assert.assertEquals(response.getUserInfo().getSubject().getValue(), subject);
+        final UserInfo userInfo = response.getUserInfo();
+        Assert.assertNotNull(userInfo);
+        Assert.assertNull(userInfo.getEmailAddress());
+        Assert.assertNull(userInfo.getNickname());
+        Assert.assertNull(response.getUserInfoJWT());
+    }
+
+    @Test
+    public void testFaoös_nonMatchingTrustChain() throws Exception {
+        final String clientId = uniqueClientId();
+        rpConfigureMockHttpClient(clientId);
+        final BearerAccessToken token = buildToken(clientId, List.of(clientId, uniqueIntermediateId()));
+        request.addHeader("Authorization", token.toAuthorizationHeader());
+        final FlowExecutionResult result = flowExecutor.launchExecution(UserInfoTest.FLOW_ID, null, externalContext);
+        assertErrorCode(result, BearerTokenError.INVALID_REQUEST.getCode());
+        assertErrorDescriptionContains(result, "NoTrustChainsResolved");
+    }
+
+    protected BearerAccessToken buildToken(final String clientId, final Object trustChain)
+            throws URISyntaxException, NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
+        final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
+                .setJWTID(idGenerator.generateIdentifier())
+                .setClientID(new ClientID(clientId))
+                .setIssuer("https://op.example.org")
+                .setPrincipal(subject)
+                .setSubject(subject)
+                .setIssuedAt(Instant.now())
+                .setExpiresAt(Instant.now().plusSeconds(30))
+                .setAuthenticationTime(Instant.now())
+                .setRedirectURI(new URI("https://example.org/cb"))
+                .setScope(Scope.parse("openid profile"))
+                .setRootTokenIdentifier(idGenerator.generateIdentifier())
+                .addCustomClaim(ClaimsSetExtensionSupport.KEY_AUTO_REGISTERED_TRUST_CHAIN, trustChain)
+                .build();
+        return new BearerAccessToken(claims.serialize(getDataSealer()));
+    }
+
+}
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
index d663f4f..1ac53f3 100644
--- a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/oidc.properties
@@ -35,7 +35,9 @@ idp.authn.Password.supportedPrincipals = \
 idp.authn.OAuth2Client.audit.format = %a|%T|%SP|%I|%s|%AF|%CV|%u|%tu|%AR|%UA|%iss|%sub|%exp|%iat|%typ
 
 idp.oidfed.authorize.automaticRegistrationCondition = shibboleth.Conditions.TRUE
+idp.oidfed.token.automaticRegistrationCondition = shibboleth.Conditions.TRUE
 idp.oidfed.par.automaticRegistrationCondition = shibboleth.Conditions.TRUE
+idp.oidfed.userinfo.automaticRegistrationCondition = shibboleth.Conditions.TRUE
 idp.oidfed.configuration.resolver.values = CustomEntityConfigurationValues
 idp.oidfed.configuration.MetadataSkaletonFile = src/test/resources/net/shibboleth/idp/module/conf/oidfed-entity-configuration-metadata.json
 idp.oidfed.trustchain.resolver.useResolverApiCondition = HeaderUseResolveApiCondition
diff --git a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
index 339ce76..a6e7cd7 100644
--- a/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
+++ b/idp-oidfed-op-impl/src/test/resources/net/shibboleth/idp/module/conf/relying-party.xml
@@ -92,6 +92,7 @@
                     <ref bean="OIDC.SSO.MDDriven" />
                     <bean parent="OAUTH2.Token.MDDriven" p:tokenEndpointAuthMethods="client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt,none"/>
                     <bean parent="OAUTH2.PAR.MDDriven" p:tokenEndpointAuthMethods="client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt,none"/>
+                    <ref bean="OIDC.UserInfo.MDDriven" />
                 </list>
             </property>
         </bean>

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


More information about the commits mailing list