[java-plugin-shibd-oidc] branch main updated: Fix configuration of form_post response decoder

Codeberg noreply at shibboleth.net
Thu Apr 16 16:10:03 UTC 2026


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

codeberg pushed a commit to branch main
in repository java-plugin-shibd-oidc.

View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-oidc/commit/c0dbabf15a24132d9a3f3ca768c6cecafd30eabf

The following commit(s) were added to refs/heads/main by this push:
     new c0dbabf  Fix configuration of form_post response decoder
c0dbabf is described below

commit c0dbabf15a24132d9a3f3ca768c6cecafd30eabf
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Apr 16 16:38:33 2026 +0100

    Fix configuration of form_post response decoder
    
     - The form_post decoder was not wired up correctly. This is not the
    default response mode.
---
 .../sp/consumer/oidc/code/post/post-beans.xml      |  12 +-
 .../sp/oidc/flows/OIDCTokenConsumerFlowTest.java   | 133 ++++++++++++++++++++-
 .../shibboleth/sp/oidc/flows/TestConstants.java    |   3 +
 .../idp/module/conf/sp/oidc-test-agents.xml        |  11 ++
 .../impl/DecodeStateAsJsonObjectConsumer.java      |   2 +-
 5 files changed, 156 insertions(+), 5 deletions(-)

diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/post/post-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/post/post-beans.xml
index e5743d9..3f5e9b7 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/post/post-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/post/post-beans.xml
@@ -8,12 +8,18 @@
     default-init-method="initialize" default-destroy-method="destroy">
     
     <bean id="DecodeMessage" class="net.shibboleth.sp.profile.impl.DecodeMessage" scope="prototype"
-            p:messageType="org.opensaml.saml.saml2.core.Response">
+            p:messageType="com.nimbusds.openid.connect.sdk.AuthenticationResponse">
         <constructor-arg>
             <bean class="net.shibboleth.oidc.profile.decoding.impl.HTTPPostAuthnResponseDecoder" scope="prototype"
-                p:decodeStateAsJSON="true"
                 p:checkDuringInit="false"
-                p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier"/>
+                p:postDecodeStrategyFailureIsError="true"  
+                p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier">
+                <property name="postDecodeStrategy">
+                    <bean class="net.shibboleth.sp.oidc.profile.decoding.impl.DecodeStateAsJsonObjectConsumer"
+                        p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+                        p:unsealStatePredicate="#{getObject('%{sp.oidc.sealOAuthStatePredicate:}') ?: ((%{sp.oidc.sealOAuthState:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"/>
+                </property>
+            </bean>
         </constructor-arg>
     </bean>
 
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java
index f0414d6..479f0a2 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java
@@ -84,7 +84,9 @@ import net.shibboleth.idp.spring.IdPPropertiesApplicationContextInitializer;
 import net.shibboleth.idp.test.PreferFileSystemApplicationContextInitializer;
 import net.shibboleth.oidc.security.credential.JWKCredential;
 import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.net.URISupport;
 import net.shibboleth.sp.context.AgentRequestContext;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.flows.AbstractSPFlowTest;
@@ -191,6 +193,45 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
                 TestConstants.RESOURCE_URL);
     }
     
+    /**
+     * Test successful flow with response_mode=form_post, a signed id_token, and a plain user info response.
+     * 
+     * @throws IOException on error
+     */
+    @Test
+    public void testSuccess_Post_SignedIDToken_PlainUserInfo() throws Exception {
+        
+        mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(
+                Instant.now().plusSeconds(3600), Instant.now(), null, null), 
+                constructJSONUserInfoResponse());
+
+        final AuthenticationSuccessResponse response = 
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.FORM_POST, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                                TestConstants.AUTHENTICATION_RFP));        
+        final DDF input = buildRemotedFormPostResponse(response);
+        
+        // Add cookies
+        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
+                TestConstants.APPLICATION_ID_POST_RESPONSE_MODE,
+                TestConstants.AUTHENTICATION_RFP,
+                TestConstants.TARGET_URL,
+                TestConstants.buildAuthenticationState(null, false, null)));
+        
+        setApplicationRequest(TestConstants.APPLICATION_ID_POST_RESPONSE_MODE, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, TestConstants.FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        final DDF output = assertOutputMessageSuccess(result);
+        assert output != null;
+        System.out.println("test output: " + output.toString());
+        validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"),
+                TestConstants.RESOURCE_URL);
+    }
+    
     /**
      * Test successful flow with a signed id_token and a plain user info response. Use private_key_jwt client 
      * authentication.
@@ -705,7 +746,7 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
    
     
     /**
-     * Test successful flow.
+     * Test error response from the OP.
      * 
      * @throws IOException on error
      */
@@ -733,6 +774,67 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
         System.out.println("DDF: "+output.toString());        
         validateCookiesAreUnset(output);
     }
+    
+    /**
+     * Test error response from OP followed by a replayed success response. It should fail as the state has been
+     * replayed.
+     * 
+     * @throws IOException on error
+     */
+    //@Test
+    public void testFail_ErrorResponse_FollowedByReplayedSuccess() throws Exception {
+        final DDF input = buildRemotedQueryStringResponse(buildErrorResponse(
+                TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_RFP)));
+        
+        // Add cookies
+        input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
+                TestConstants.APPLICATION_ID,
+                TestConstants.AUTHENTICATION_RFP,
+                TestConstants.TARGET_URL,
+                TestConstants.buildAuthenticationState(null, false, null)));
+
+        setApplicationRequest(TestConstants.APPLICATION_ID, input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, TestConstants.FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        final DDF output = assertOutputMessageEvent(result, EventIds.MESSAGE_PROC_ERROR);
+        System.out.println("DDF: "+output.toString());        
+        validateCookiesAreUnset(output);
+        
+        // Now construct a valid response, as the state is re-used this should fail early, and will not need the
+        // mocked endpoints etc.
+        
+        final AuthenticationSuccessResponse response = 
+                buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, 
+                        TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN, 
+                                TestConstants.AUTHENTICATION_RFP)); 
+        final DDF inputSuccessResponse = buildRemotedQueryStringResponse(response);
+        
+        // Add cookies
+        inputSuccessResponse.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+                TestConstants.STATE_TOKEN, 
+                TestConstants.AUTHENTICATION_STATE_TOKEN,
+                TestConstants.APPLICATION_ID,
+                TestConstants.AUTHENTICATION_RFP,
+                TestConstants.TARGET_URL,
+                TestConstants.buildAuthenticationState(null, false, null)));       
+        
+        setApplicationRequest(TestConstants.APPLICATION_ID, inputSuccessResponse);
+        
+        final FlowExecutionResult resultReplayFail = 
+                flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(resultReplayFail, TestConstants.FLOW_ID);
+        assertFlowExecutionOutcome(resultReplayFail.getOutcome());
+        // TODO change to appropriate for replay error
+        assertOutputMessageEvent(resultReplayFail, EventIds.ACCESS_DENIED);
+        System.out.println("DDF: "+output.toString());    
+        validateCookiesAreUnset(output);
+        
+    }
   
     
     /**
@@ -1104,5 +1206,34 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
         return obj;
     }
     
+    /**
+     * Encodes an OIDC response into a form_post embedded in a remoted message.
+     * 
+     * @param response OIDC response to encode
+     * 
+     * @return input object suitable for token consumer flow
+     * 
+     * @throws IOException on error 
+     */
+    @Nonnull private DDF buildRemotedFormPostResponse(@Nonnull final AuthorizationResponse response) {                
+
+        final DDF obj = new DDF(null).structure();
+        obj.addmember(ConsumerConstants.BASE_URL).unsafe_string(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
+        final DDF http = obj.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
+        
+        http.addmember(RemotedHttpServletRequest.METHOD).string("POST");
+        http.addmember(RemotedHttpServletRequest.REMOTE_ADDR).string("192.168.1.1");
+        http.addmember(RemotedHttpServletRequest.REQUEST_URL).unsafe_string(
+                TestConstants.RESPONSE_URL.getBytes(StandardCharsets.UTF_8));
+        http.addmember(RemotedHttpServletRequest.CONTENT_TYPE).string("application/x-www-form-urlencoded");
+        if (response instanceof final AuthenticationSuccessResponse successResponse) {
+            http.addmember(RemotedHttpServletRequest.BODY).unsafe_string(Constraint.isNotNull(URISupport.buildQuery(
+                    CollectionSupport.listOf(new Pair<>("code", successResponse.getAuthorizationCode().getValue()), 
+                            new Pair<>("state", response.getState().getValue()))), "Query string is null").getBytes(StandardCharsets.UTF_8));
+        }
+        
+        return obj;
+    }
+    
     
 }
\ No newline at end of file
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java
index 9883156..480edda 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java
@@ -90,6 +90,9 @@ public final class TestConstants {
     
     /** Application ID used when private_key_jwt has been configured.*/
     public static final String APPLICATION_ID_PRIVATE_KEY_JWT = "test-oidc-application-with-ro-private-key-jwt";
+    
+    /** Application ID used when the form_post response mode has been configured.*/
+    public static final String APPLICATION_ID_POST_RESPONSE_MODE = "test-oidc-application-with-post-responsemode";
 
     /** Target URL encoded in cookie. */
     public static final String TARGET_URL = "https://sp.example.org/secure";
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test-agents.xml b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test-agents.xml
index 0587107..60c0576 100644
--- a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test-agents.xml
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/oidc-test-agents.xml
@@ -29,6 +29,9 @@
                     
                 <bean p:id="test-oidc-application-with-pkce" parent="shibboleth.sp.Application"
                     p:profileConfigurations-ref="test.WithPKCE"/>
+                    
+                <bean p:id="test-oidc-application-with-post-responsemode" parent="shibboleth.sp.Application"
+                    p:profileConfigurations-ref="test.WithPostResponseMode"/>
                 
                 <!-- Test using a profile that requires a request object, also override the authenticatingAuthority just to check that -->
                 <bean p:id="test-oidc-application-with-ro" parent="shibboleth.sp.Application"
@@ -67,6 +70,14 @@
         </bean>
     </util:list>
     
+    <util:list id="test.WithPostResponseMode">
+        <bean parent="OIDC.SSO" p:securityConfiguration-ref="testSecConfig" p:responseMode="form_post">
+            <property name="clientCredential">
+                  <bean parent="shibboleth.oidc.ClientSecretCredential" p:secret="secret"/>
+            </property>
+        </bean>
+    </util:list>
+    
     <util:list id="test.RequestObjectProfileConfigurations">
         <bean parent="OIDC.SSO" p:securityConfiguration-ref="testSecConfig" p:useRequestObject="true" p:signRequestObject="true" p:encryptRequestObject="false">
             <property name="clientCredential">
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/decoding/impl/DecodeStateAsJsonObjectConsumer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/decoding/impl/DecodeStateAsJsonObjectConsumer.java
index dcfa3b4..530e365 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/decoding/impl/DecodeStateAsJsonObjectConsumer.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/decoding/impl/DecodeStateAsJsonObjectConsumer.java
@@ -14,7 +14,6 @@
 
 package net.shibboleth.sp.oidc.profile.decoding.impl;
 
-import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -126,6 +125,7 @@ public class DecodeStateAsJsonObjectConsumer extends AbstractIdentifiableInitial
         }
     
         try {
+
             final String stateValue = responseState.getValue();
             assert stateValue != null;            
             Object stateObjectParsed = null;

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


More information about the commits mailing list