[java-plugin-shibd-oidc] 03/09: WiP on logout

Codeberg noreply at shibboleth.net
Mon Jul 13 16:24:18 UTC 2026


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

codeberg pushed a commit to branch dev/JSHIBDOIDC-28
in repository java-plugin-shibd-oidc.

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

commit 013fcee2e6bdaf0d98491e5c13596a759cae3fb2
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Jun 19 13:35:14 2026 +0100

    WiP on logout
---
 .../sp/oidc/context/OIDCLogoutContext.java         |  50 +++++
 .../shibboleth/sp/oidc/profile/OIDCConstants.java  |   8 +-
 .../shibboleth/sp/oidc/testing/TestConstants.java  |   2 +-
 .../net/shibboleth/sp/oidc/testing/TestHelper.java |  38 ++++
 .../META-INF/net.shibboleth.idp/postconfig.xml     |  34 ++++
 .../flows/sp/logout/consumer/oidc/oidc-beans.xml   | 207 +++++++++++++++++++++
 .../flows/sp/logout/consumer/oidc/oidc-flow.xml    | 101 ++++++++++
 .../consumer/oidc/redirect/redirect-beans.xml      |  23 +++
 .../consumer/oidc/redirect/redirect-flow.xml       |   9 +
 .../flows/sp/logout/initiator/oidc/oidc-beans.xml  |  31 +++
 .../flows/sp/logout/initiator/oidc/oidc-flow.xml   |  39 ++++
 .../net/shibboleth/sp/service/agent/postconfig.xml |   5 +-
 .../sp/oidc/flows/OIDCLogoutInitiatorFlowTest.java | 175 +++++++++++++++++
 ...DCEnvironmentApplicationContextInitializer.java |   1 +
 .../src/test/resources/logback-flow-test.xml       |  29 +++
 .../src/test/resources/logback-test.xml            |   2 +-
 .../impl/ProcessLogoutInitiatorRequest.java        | 201 ++++++++++++++++++++
 .../profile/impl/BaseOIDCAuthenticationTest.java   |   3 +-
 .../impl/ProcessLogoutInitiatorRequestTest.java    | 206 ++++++++++++++++++++
 .../sp/oidc/profile/impl/SealerKeyStore.jks        | Bin 0 -> 984 bytes
 .../sp/oidc/profile/impl/SealerKeyStore.kver       |   1 +
 21 files changed, 1154 insertions(+), 11 deletions(-)

diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/OIDCLogoutContext.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/OIDCLogoutContext.java
new file mode 100644
index 0000000..0c023e5
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/OIDCLogoutContext.java
@@ -0,0 +1,50 @@
+/*
+ * 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.sp.oidc.context;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import com.nimbusds.jwt.JWT;
+
+/**
+ * Context to handle state for OIDC logout flows.
+ */
+public class OIDCLogoutContext extends BaseContext {
+    
+    /** The id_token recovered from the session.*/
+    @Nullable private JWT idToken;
+
+    /**
+     * Set the id_token recovered from the session.
+     * 
+     * @param token the id_token
+     */
+    public OIDCLogoutContext setIdToken(@Nullable final JWT token) {
+        idToken = token;   
+        return this;
+    }
+    
+    /**
+     * Get the id_token recovered from the session.
+     * 
+     * @return the id_token.
+     */
+    @Nullable public JWT getIdToken() {
+        return idToken;
+    }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCConstants.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCConstants.java
index e4783d4..7ada8fd 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCConstants.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCConstants.java
@@ -27,13 +27,7 @@ public final class OIDCConstants {
      * Name of the state field to add to the state JSON object. The state field represents the current 'state'
      * value of the DDF.
      */
-    @Nonnull @NotEmpty public static final String STATE_FIELD = "state";
-    
-    /** Name of the authentication state token field that references the authentication state in the token manager.*/
-    @Nonnull @NotEmpty public static final String AUTHN_STATE_FIELD = "authnState";
-    
-    /** Name of the Request Forgery Protection (nonce) field.*/
-    @Nonnull @NotEmpty public static final String RFP_FIELD = "rfp";    
+    @Nonnull @NotEmpty public static final String STATE_FIELD = "state"; 
     
     /** Private constructor. */
     private OIDCConstants() {
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestConstants.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestConstants.java
index 3c88442..10849a3 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestConstants.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestConstants.java
@@ -46,7 +46,7 @@ public final class TestConstants {
     /** State token string used in cookie names. */
     public static final String STATE_TOKEN = "1761316967710_1622a5c726da8f7b36e24f19eed82aea";    
 
-    /** Application ID used in cookie names. */
+    /** Application ID. */
     public static final String APPLICATION_ID = "test-oidc-application-with-ro";
     
     /** Application ID used when private_key_jwt has been configured.*/
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestHelper.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestHelper.java
index 123e635..f901793 100644
--- a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestHelper.java
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/testing/TestHelper.java
@@ -211,6 +211,28 @@ public final class TestHelper {
         return type.cast(defaultValue);
     }
     
+    /**
+     * Simplified method to create a signed JWT (Id Token) using a MAC.
+     * 
+     * @param issuer the issuer
+     * @param subject the subject
+     * @param macCredential the shared secret credential used for the MAC
+     * 
+     * @return a signed JWT
+     * 
+     * @throws Exception on error
+     */
+    public static JWT createSignedIDToken(@Nullable final String issuer, @Nullable final String subject, 
+            @Nullable final Credential macCredential) 
+                throws Exception {
+        final JWTClaimsSet claims = new JWTClaimsSet.Builder()
+                .issuer(issuer)
+                .subject(subject)
+                .expirationTime(new Date(System.currentTimeMillis() + 60000))
+                .build();
+        return createJWT(claims, JWSAlgorithm.HS256, null, null, macCredential, null);        
+    }
+    
     /**
      * Create a JWT from the given payload. The JWT can either be plain, or signed and encrypted. If encrypted, it must
      * be signed. 
@@ -547,5 +569,21 @@ public final class TestHelper {
             ResponseMode.QUERY);
     
     }
+    
+    
+    /**
+     * Build a basic DDF with session data that contains an id_token.
+     * 
+     * @param idToken the token to add to the DDF
+     * @param tokenParam the name of the token param
+     * @return the constructed DDF
+     */
+    @Nonnull public static DDF buildLogoutInitiatorDDFInput(@Nonnull final String idToken, 
+            @Nonnull final String tokenParam) {
+        final DDF root = new DDF(null).structure();
+        final DDF session = root.addmember(ConsumerConstants.SESSION_OPAQUE);
+        session.addmember(tokenParam).string(idToken);
+        return root;
+    }
 
 }
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 2b2cdb3..4256b8b 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -88,6 +88,40 @@
             </set>
         </property>
     </bean>
+    
+     <!-- Logout consumer descriptors for OIDC -->
+
+<!--     <bean id="shibboleth.sp.TokenParameterPrefixCondition"
+        class="net.shibboleth.sp.profile.context.logic.InputStartsWithPredicate"
+        c:_0="token" c:_1="SAML:" /> -->
+    
+    <bean p:id="sp/logout/consumer/oidc/redirect" class="net.shibboleth.sp.profile.BasicLogoutConsumerFlowDescriptor">
+       <!--  <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.OR">
+                <constructor-arg>
+                    <list>
+                        <ref bean="shibboletg.sp.TokenParameterPrefixCondition" />
+                        <bean class="net.shibboleth.sp.profile.context.logic.HttpServletRequestPredicate"
+                                p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier">
+                            <property name="httpServletRequestValidator">
+                                <bean class="net.shibboleth.shared.servlet.impl.ChainingHttpServletRequestValidator">
+                                    <property name="validators">
+                                        <list>
+                                            <bean class="net.shibboleth.shared.servlet.impl.BasicHttpServletRequestMethodValidator"
+                                                p:allowedMethods="GET" />
+                                            <ref bean="shibboleth.HttpServletRequestValidator.BasicParams.SAML2.Redirect.RequestOrResponse" />
+                                        </list>
+                                    </property>
+                                </bean>
+                            </property>
+                        </bean>
+                    </list>
+                </constructor-arg>
+            </bean>
+        </property> -->
+    </bean>
+    
+    <!-- Other global beans -->
 
     <bean id="shibboleth.ProviderMetadataResolverService"
         class="net.shibboleth.shared.spring.service.ReloadableSpringService"
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/oidc-beans.xml
new file mode 100644
index 0000000..c0154e6
--- /dev/null
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/oidc-beans.xml
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <util:constant id="shiibboleth.sp.ProfileId"
+        static-field="net.shibboleth.saml.saml2.profile.config.SingleLogoutProfileConfiguration.PROFILE_ID" />
+
+    <util:constant id="shibboleth.EndpointType"
+        static-field="org.opensaml.saml.saml2.metadata.SingleLogoutService.DEFAULT_ELEMENT_NAME" />
+
+   <!--  <import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml" />
+
+    Dummy manager used to generate/consume the "token" operation parameter.
+    <bean id="PassthroughStateManager"
+            class="net.shibboleth.sp.state.impl.PassthroughStateManager"
+            p:dataSealer-ref="shibboleth.DataSealer"
+            p:objectMapper-ref="shibboleth.JSONObjectMapper"
+            p:expiration="PT30M">
+        <property name="prefix">
+            <util:constant static-field="net.shibboleth.sp.saml.saml2.profile.impl.ProcessLogoutRequest.TOKEN_PREFIX" />
+        </property>
+    </bean>
+
+    <bean id="ProcessLogoutTokenRequest"
+        class="net.shibboleth.sp.saml.saml2.profile.impl.ProcessLogoutTokenRequest" scope="prototype"
+        p:stateManager-ref="PassthroughStateManager" />
+
+    Only applied to LogoutResponse messages.
+    
+    <bean id="CreateServletResponseForLogoutResponse"
+            class="net.shibboleth.sp.profile.impl.CreateOutputMessage" scope="prototype"
+            p:createServletResponse="true">
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.NOT" c:_0-ref="IsLogoutRequest" />
+        </property>
+    </bean>
+    
+    <bean id="RecoverStateDataForLogoutResponse"
+            class="net.shibboleth.sp.profile.impl.RecoverStateData" scope="prototype"
+            p:stateTokenLookupStrategy-ref="RelayStateLookup"
+            p:stateDataClass="net.shibboleth.sp.saml.saml2.SAMLStateData">
+        <property name="activationCondition">
+            <bean parent="shibboleth.Conditions.NOT" c:_0-ref="IsLogoutRequest" />
+        </property>
+    </bean>
+
+    <bean id="RelayStateLookup" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <bean parent="shibboleth.Functions.Expression"
+                c:_0="T(org.opensaml.saml.common.binding.SAMLBindingSupport).getRelayState(#input)" />
+        </constructor-arg>
+        <constructor-arg name="f" ref="shibboleth.MessageContextLookup.Inbound"/>
+    </bean>
+    
+    <bean id="IsLogoutRequest" parent="shibboleth.Conditions.Expression"
+        c:_0="#input.ensureInboundMessageContext().getMessage() instanceof T(org.opensaml.saml.saml2.core.LogoutRequest)" />
+
+    <bean id="CheckDestinationAndIssuerHandler" class="net.shibboleth.sp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="INBOUND">
+        <constructor-arg>
+            <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
+                <property name="handlers">
+                    <list>
+                        <bean class="net.shibboleth.sp.saml.saml2.messaging.impl.CheckDestinationAndIssuerHandler" scope="prototype"
+                            p:checkDuringInit="false"
+                            p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier" />
+                    </list>
+                </property>
+             </bean>
+        </constructor-arg>
+        <property name="errorEvent">
+            <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MESSAGE" />
+        </property>
+    </bean>
+
+    <bean id="HandleMessageEarly" class="net.shibboleth.sp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="INBOUND">
+        <constructor-arg>
+            <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
+                <property name="handlers">
+                    <list>
+                        <bean class="org.opensaml.messaging.handler.impl.FunctionMessageHandler" scope="prototype">
+                            <property name="functionLookupStrategy">
+                                <bean class="net.shibboleth.saml.profile.config.navigate.messaging.MessageHandlerLookupFunction" />
+                            </property>
+                        </bean>
+                        <bean class="org.opensaml.saml.common.binding.security.impl.ReceivedEndpointSecurityHandler" scope="prototype"
+                            p:checkDuringInit="false"
+                            p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier" />
+                        <bean class="org.opensaml.saml.common.binding.security.impl.MessageReplaySecurityHandler" scope="prototype"
+                            p:replayCache-ref="shibboleth.ReplayCache"
+                            p:expires="%{sp.policy.messageLifetime:PT3M}" />
+                        <bean class="org.opensaml.saml.common.binding.security.impl.MessageLifetimeSecurityHandler" scope="prototype"
+                            p:messageLifetime="%{sp.policy.messageLifetime:PT3M}"
+                            p:clockSkew="%{sp.policy.clockSkew:PT3M}" />
+                    </list>
+                </property>
+             </bean>
+        </constructor-arg>
+        <property name="errorEvent">
+            <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MESSAGE" />
+        </property>
+    </bean>
+
+    <bean id="NotIgnoreRequestSignaturesPredicate" parent="shibboleth.Conditions.NOT">
+        <constructor-arg>
+            <bean class="net.shibboleth.saml.saml2.profile.config.logic.IgnoreRequestSignaturesPredicate" />
+        </constructor-arg>
+    </bean>
+
+    <bean id="CheckSignature" class="net.shibboleth.sp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="INBOUND"
+            p:activationCondition-ref="NotIgnoreRequestSignaturesPredicate">
+        <constructor-arg>
+            <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
+                <property name="handlers">
+                    <list>
+                        <bean class="org.opensaml.saml.saml2.binding.security.impl.SAML2HTTPRedirectDeflateSignatureSecurityHandler" scope="prototype"
+                            p:checkDuringInit="false"
+                            p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier" />
+                        <bean class="org.opensaml.saml.common.binding.security.impl.SAMLProtocolMessageXMLSignatureSecurityHandler" scope="prototype" />
+                        <bean class="org.opensaml.saml.saml2.binding.security.impl.SAML2HTTPPostSimpleSignSecurityHandler" scope="prototype"
+                            p:checkDuringInit="false"
+                            p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier"
+                            p:parser-ref="shibboleth.ParserPool"
+                            p:keyInfoResolver-ref="shibboleth.KeyInfoCredentialResolver" />
+                    </list>
+                </property>
+             </bean>
+        </constructor-arg>
+        <property name="errorEvent">
+            <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MESSAGE" />
+        </property>
+    </bean>
+
+    <bean id="InboundEntityIDLookup" parent="shibboleth.Functions.Compose">
+        <constructor-arg name="g">
+            <bean class="org.opensaml.saml.common.messaging.context.navigate.SAMLEntityIDFunction" />
+        </constructor-arg>
+        <constructor-arg name="f">
+            <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+                  c:type="#{ T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext) }" />
+        </constructor-arg>
+    </bean>
+
+    <bean id="HandleMessageLate" class="net.shibboleth.sp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+            c:executionDirection="INBOUND">
+        <constructor-arg>
+            <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
+                <property name="handlers">
+                    <list>
+                        <bean class="org.opensaml.messaging.handler.impl.CheckMandatoryIssuer" scope="prototype"
+                            p:issuerLookupStrategy-ref="InboundEntityIDLookup" />
+                        <bean class="org.opensaml.messaging.handler.impl.CheckMandatoryAuthentication" scope="prototype"
+                                p:activationCondition="%{sp.saml.logout.authenticated:true}">
+                            <property name="authenticationLookupStrategy">
+                                <bean class="org.opensaml.saml.common.messaging.context.navigate.SAMLMessageContextAuthenticationFunction" />
+                            </property>
+                        </bean>
+                    </list>
+                </property>
+             </bean>
+        </constructor-arg>
+        <property name="errorEvent">
+            <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MESSAGE" />
+        </property>
+    </bean>
+
+    <bean id="PopulateDecryptionParameters"
+        class="org.opensaml.profile.action.impl.PopulateDecryptionParameters" scope="prototype"
+        p:recipientLookupStrategy-ref="shibboleth.IssuerLookup.Simple"
+        p:configurationLookupStrategy-ref="shibboleth.DecryptionConfigurationLookup"
+        p:decryptionParametersResolver-ref="shibboleth.DecryptionParametersResolver" />
+
+    <bean id="DecryptNameIDs" class="org.opensaml.saml.saml2.profile.impl.DecryptNameIDs" scope="prototype" />
+        
+    <bean id="ProcessLogoutRequest"
+        class="net.shibboleth.sp.saml.saml2.profile.impl.ProcessLogoutRequest" scope="prototype"
+        p:parserPool-ref="shibboleth.ParserPool"
+        p:stateManager-ref="PassthroughStateManager" />
+    
+    <bean id="ProcessLogoutResponse"
+        class="net.shibboleth.sp.saml.saml2.profile.impl.ProcessLogoutResponse" scope="prototype" />
+        
+    <bean id="PopulateResponseSignatureSigningParameters"
+            class="org.opensaml.saml.common.profile.impl.PopulateSignatureSigningParameters" scope="prototype"
+            p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+            p:signatureSigningParametersResolver-ref="shibboleth.SignatureSigningParametersResolver"
+            p:noResultIsError="false">
+        <property name="activationCondition">
+            <bean class="net.shibboleth.saml.profile.config.logic.SignResponsesPredicate" />
+        </property>
+    </bean>
+    
+    <bean id="CreateServletResponse"
+        class="net.shibboleth.sp.profile.impl.CreateOutputMessage" scope="prototype"
+        p:createServletResponse="true" />
+    
+    <bean id="AddLogoutResponse"
+        class="net.shibboleth.sp.saml.saml2.profile.impl.AddLogoutResponse" scope="prototype" /> -->
+    
+</beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/oidc-flow.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/oidc-flow.xml
new file mode 100644
index 0000000..24dfc46
--- /dev/null
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/oidc-flow.xml
@@ -0,0 +1,101 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" 
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    abstract="true" parent="sp/logout/consumer">
+
+    <decision-state id="CheckForToken">
+        <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.sp.context.AgentRequestContext)).getInput().getmember('http').isstruct()"
+            then="DoMessage"
+            else="IssueResponse" />
+    </decision-state>
+
+    <!-- This branch consumes a SAML message. -->
+    <action-state id="DoMessage">
+        <evaluate expression="ValidateHttpServletRequest" />
+        <evaluate expression="DecodeMessage" />
+<!--        <evaluate expression="PostDecodePopulateAuditContext" />-->
+
+        <!-- Only runs for LogoutResponse messages but early enough to head off errors. -->
+        <evaluate expression="CreateServletResponseForLogoutResponse" />
+        <evaluate expression="RecoverStateDataForLogoutResponse" />
+        
+        <evaluate expression="CheckMessageVersion" />
+        
+        <evaluate expression="SAMLProtocolAndRole" />
+        <evaluate expression="CheckDestinationAndIssuerHandler" />
+        <evaluate expression="SAMLMetadataLookup" />
+
+        <evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
+        <evaluate expression="SelectRelyingPartyConfiguration" />
+<!--        <evaluate expression="PostLookupPopulateAuditContext" />-->
+        <evaluate expression="SelectProfileConfiguration" />
+
+        <evaluate expression="PopulateSignatureValidationParameters" />
+        <evaluate expression="PopulateClientTLSValidationParameters" />
+        
+        <evaluate expression="HandleMessageEarly" />
+        <evaluate expression="CheckSignature" />
+        <evaluate expression="HandleMessageLate" />
+<!--        <evaluate expression="PostResponsePopulateAuditContext" />-->        
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="PreProcessLogoutMessage" />
+    </action-state>
+    
+    <!-- Dispatch by message type. -->
+    <decision-state id="PreProcessLogoutMessage">
+        <if test="IsLogoutRequest.test(opensamlProfileRequestContext)" then="DoLogoutRequest" else="DoLogoutResponse"/>
+    </decision-state>
+    
+    <!-- For a LogoutRequest, we process against the "session.opaque" input and produce a "token" for the Agent. -->
+    <action-state id="DoLogoutRequest">
+        <evaluate expression="PopulateDecryptionParameters" />
+        <evaluate expression="DecryptNameIDs" />
+        <evaluate expression="ProcessLogoutRequest" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="proceed" />
+    </action-state>
+
+    <!-- For a LogoutResponse, we just extract some info for the Agent, simplest case by far. -->
+    <action-state id="DoLogoutResponse">
+        <evaluate expression="ProcessLogoutResponse" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="proceed" />
+    </action-state>
+    
+    <!-- This branch uses the "token" from the Agent to generate a LogoutResponse outbound. -->
+    <action-state id="IssueResponse">
+        <evaluate expression="ProcessLogoutTokenRequest" />
+        <evaluate expression="PrepareInboundMessageContext" />
+        <evaluate expression="SAMLProtocolAndRole" />
+        <evaluate expression="SAMLMetadataLookup" />
+        <evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
+        <evaluate expression="SelectRelyingPartyConfiguration" />
+        <evaluate expression="SelectProfileConfiguration" />
+        <evaluate expression="InitializeOutboundMessageContext" />
+        <evaluate expression="InitializeMessageChannelSecurityContext" />
+        <evaluate expression="PopulateBindingAndEndpointContexts" />
+        <evaluate expression="PopulateResponseSignatureSigningParameters" />
+        <evaluate expression="CreateServletResponse" />
+        <evaluate expression="AddLogoutResponse" />
+        <evaluate expression="HandleOutboundMessage" />
+        <evaluate expression="EncodeMessage" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="proceed" />
+    </action-state>
+    
+    <end-state id="EndpointResolutionFailed" />
+    
+    <global-transitions>
+        <transition on="EndpointResolutionFailed" to="EndpointResolutionFailed" />
+        <!-- Remap some internal action errors. -->
+        <transition on="InvalidCredentials" to="InvalidMessage" />
+    </global-transitions>
+    
+    <!-- The file really exists in this directory, but it's referenced from extending flow-directories -->
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/saml2/saml2-beans.xml" />
+
+</flow>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/redirect/redirect-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/redirect/redirect-beans.xml
new file mode 100644
index 0000000..729013a
--- /dev/null
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/redirect/redirect-beans.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <alias alias="shibboleth.HttpServletRequestValidator" name="shibboleth.HttpServletRequestValidator.BasicParams.SAML2.Redirect.RequestOrResponse" />
+
+    <bean id="DecodeMessage" class="net.shibboleth.sp.profile.impl.DecodeMessage" scope="prototype"
+            p:messageType="org.opensaml.saml.saml2.core.LogoutMessage">
+        <constructor-arg>
+            <bean class="org.opensaml.saml.saml2.binding.decoding.impl.HTTPRedirectDeflateDecoder" scope="prototype"
+                p:checkDuringInit="false"
+                p:parserPool-ref="shibboleth.ParserPool"
+                p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier"
+                p:bindingDescriptor-ref="shibboleth.Binding.SAML2Redirect" />
+        </constructor-arg>
+    </bean>
+
+</beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/redirect/redirect-flow.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/redirect/redirect-flow.xml
new file mode 100644
index 0000000..c049c3b
--- /dev/null
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/redirect/redirect-flow.xml
@@ -0,0 +1,9 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" 
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="sp/logout/consumer/oidc">
+
+    <!-- The file really exists in this directory, but it's referenced from extending flow-directories -->
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/logout/consumer/oidc/redirect/redirect-beans.xml" />
+
+</flow>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/oidc/oidc-beans.xml
new file mode 100644
index 0000000..b4f14c4
--- /dev/null
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/oidc/oidc-beans.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize" default-destroy-method="destroy">
+
+    <util:constant id="shiibboleth.sp.ProfileId"
+        static-field="net.shibboleth.saml.saml2.profile.config.SingleLogoutProfileConfiguration.PROFILE_ID" />
+
+    <import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml" />
+
+    <bean id="ProcessLogoutInitiatorRequest"
+        class="net.shibboleth.sp.oidc.profile.impl.ProcessLogoutInitiatorRequest" scope="prototype"
+        p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"/>
+
+    <!-- <util:constant id="shibboleth.EndpointType"
+        static-field="org.opensaml.saml.saml2.metadata.SingleLogoutService.DEFAULT_ELEMENT_NAME" />
+
+    <bean id="AddLogoutRequest"
+            class="net.shibboleth.sp.saml.saml2.profile.impl.AddLogoutRequest" scope="prototype"
+            p:overwriteExisting="true">
+        <property name="identifierGeneratorLookupStrategy">
+            <bean class="net.shibboleth.profile.config.navigate.IdentifierGenerationStrategyLookupFunction"
+                p:defaultIdentifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy" />
+        </property>
+    </bean> -->
+    
+</beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/oidc/oidc-flow.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/oidc/oidc-flow.xml
new file mode 100644
index 0000000..fb24083
--- /dev/null
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/oidc/oidc-flow.xml
@@ -0,0 +1,39 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" 
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="sp/logout/initiator">
+
+    <action-state id="SAML2LogoutInitiator">
+        <evaluate expression="ProcessLogoutInitiatorRequest" />
+        <!-- <evaluate expression="PrepareInboundMessageContext" />
+        <evaluate expression="SAMLProtocolAndRole" />
+        <evaluate expression="SAMLMetadataLookup" />
+        
+        <evaluate expression="InitializeRelyingPartyContextFromSAMLPeer" />
+        <evaluate expression="SelectRelyingPartyConfiguration" />
+        <evaluate expression="SelectProfileConfiguration" />
+        
+        <evaluate expression="InitializeOutboundMessageContext" />
+        <evaluate expression="InitializeMessageChannelSecurityContext" />
+        <evaluate expression="PopulateBindingAndEndpointContexts" />
+
+        <evaluate expression="PopulateRequestSignatureSigningParameters" />
+        <evaluate expression="PopulateEncryptionParameters" />
+        
+        <evaluate expression="AddLogoutRequest" />
+        <evaluate expression="EncryptNameIDs" />
+
+        <evaluate expression="HandleOutboundMessage" />
+        <evaluate expression="PreserveRelayState" />
+        <evaluate expression="EncodeMessage" /> -->
+        <evaluate expression="'proceed'" />
+
+        <transition on="proceed" to="proceed" />
+        <!-- Remap any other events into a fall-through to the next flow. -->
+        <transition to="ReselectFlow" />
+    </action-state>
+    
+    <!-- The file really exists in this directory, but it's referenced from extending flow-directories -->
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/oidc/oidc-beans.xml" />
+
+</flow>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
index fcb58b8..eeb0406 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
@@ -22,11 +22,14 @@
         p:id="OIDC"
         p:order="%{sp.oidc.relativeOrder:1}"
         p:sessionInitiators="oidc"
+        p:logoutInitiators="oidc"
         p:metadataResolver-ref="shibboleth.ProviderMetadataResolverService"
-        p:tokenConsumers="#{{'oidc/code/query', 'oidc/code/post'}}">
+        p:tokenConsumers="#{{'oidc/code/query', 'oidc/code/post'}}"
+        p:logoutConsumers="#{{ 'oidc/redirect' }}">
         <property name="defaultProfileConfigurations">
             <list>
                 <ref bean="OIDC.SSO" />
+                <!-- <ref bean="OIDC.Logout" /> -->
             </list>
         </property>
         <property name="metadataDrivenDefaultProfileConfigurations">
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCLogoutInitiatorFlowTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCLogoutInitiatorFlowTest.java
new file mode 100644
index 0000000..672dbf7
--- /dev/null
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCLogoutInitiatorFlowTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.sp.oidc.flows;
+
+import static org.testng.Assert.fail;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.Date;
+
+import javax.annotation.Nonnull;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.idp.spring.IdPPropertiesApplicationContextInitializer;
+import net.shibboleth.idp.test.PreferFileSystemApplicationContextInitializer;
+import net.shibboleth.oidc.security.credential.JWKCredential;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.flows.AbstractSPFlowTest;
+import net.shibboleth.sp.oidc.profile.impl.PrepareAgentResponse;
+import net.shibboleth.sp.oidc.testing.TestConstants;
+import net.shibboleth.sp.oidc.testing.TestHelper;
+
+/**
+ * Unit test for the OIDC-SP RP-Initiated logout flow.
+ */
+ at ContextConfiguration(
+        locations = {
+                "classpath:/net/shibboleth/sp/oidc-test-beans.xml",
+                },
+        initializers = {
+                TestSPOIDCEnvironmentApplicationContextInitializer.class,
+                PreferFileSystemApplicationContextInitializer.class,
+                IdPPropertiesApplicationContextInitializer.class
+                },
+        inheritInitializers = false
+        )
+ at WebAppConfiguration
+public class OIDCLogoutInitiatorFlowTest extends AbstractSPFlowTest {
+    
+    /** Flow ID. */
+    @Nonnull public static final String FLOW_ID = "sp/logout-initiator";
+    
+    /** Dummy signing key of the dummy OP we are testing against. */
+    @Autowired @Qualifier("dummy.op.signing.Credential") private JWKCredential opSigningCredential;
+    
+    /** DataSealer used to seal session data as input to the logout flow. */
+    @Autowired @Qualifier("shibboleth.DataSealer") private DataSealer sealer;
+    
+    /** The mocked HttpClient to use when responding to Token and UserInfo requests.*/
+    private HttpClient httpClient;
+    
+    /** The OP metadata to use.*/
+    private OIDCProviderMetadata metadata;
+    
+    /** A signed id_token for adding to the input DDF.*/
+    private JWT signedIdToken;
+    
+    /** The sealed version of the compact JWT serialisation of the id_token.*/
+    private String sealedIdToken;
+    
+    /** Constructor. */
+    protected OIDCLogoutInitiatorFlowTest() {
+        super(FLOW_ID);
+    }
+    
+
+    /** 
+     * Pre-test work.
+     *  
+     * @throws Exception on error
+     */
+    @BeforeMethod
+    public void beforeMethod() throws Exception {
+        setDefaultAuth();
+        final ApplicationContext applicationContext2 = applicationContext;
+        if (applicationContext2 != null) {
+            httpClient = applicationContext2.getBean("Mock.HttpClient", HttpClient.class);
+        } else {
+            fail("Mocked Http Client could not be found");
+        }
+        if (httpClient == null) {
+            fail("Mocked Http Client could not be found");
+        }
+        // Add a default metadata response
+        final var metadataFromFile = new ClassPathResource("metadata/openid-configuration.json");
+        final String json = new String(metadataFromFile.getInputStream().readAllBytes(), StandardCharsets.UTF_8);        
+        metadata = OIDCProviderMetadata.parse(JSONObjectUtils.parse(json));        
+        mockProviderMetadataEndpoint(metadata);
+        
+        final JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder()
+                .subject("jdoe")
+                .issuer(TestConstants.ISSUER)
+                .audience(TestConstants.CLIENT_ID)
+                .expirationTime(Date.from(Instant.now().plusSeconds(60)))
+                    .issueTime(Date.from(Instant.now()));
+        
+        signedIdToken = 
+                TestHelper.createJWT(claimsSetBuilder.build(), JWSAlgorithm.RS256, null, null, 
+                        opSigningCredential, null);
+        
+        sealedIdToken = sealer.wrap(signedIdToken.serialize());
+    }
+    
+    @Test
+    public void testRPInitiated() throws Exception {
+        setDefaultAuth();
+        
+        final DDF input = 
+                TestHelper.buildLogoutInitiatorDDFInput(sealedIdToken, PrepareAgentResponse.ID_TOKEN_PARAM);
+        setApplicationRequest(TestConstants.APPLICATION_ID, input);
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        final var output = assertOutputMessageSuccess(result);
+        assert output != null;
+        System.out.println("test output: " + output.toString());
+        
+        
+    }
+    
+    /**
+     * Mock OpenID Provider configuration (metadata) endpoint.
+     * 
+     * @param tokenResponse the token response
+     * @param userInfoResponse the user info response
+     * 
+     * @throws IOException on error.
+     */
+    @SuppressWarnings("unchecked")
+    private void mockProviderMetadataEndpoint(final OIDCProviderMetadata metadata) throws IOException {
+        
+        Mockito.when(httpClient.execute(
+                Mockito.argThat(req -> req != null && req.getRequestUri().toString()
+                .contains(".well-known/openid-configuration")),
+                Mockito.any(HttpContext.class),
+                Mockito.any(HttpClientResponseHandler.class)))
+            .thenReturn(metadata);
+
+    }
+
+}
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
index 9474543..6633acc 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
@@ -38,6 +38,7 @@ public class TestSPOIDCEnvironmentApplicationContextInitializer extends TestSPEn
         final MockPropertySource mock = new MockPropertySource();
         mock.setProperty("idp.home", "classpath:/net/shibboleth/idp/module");
         mock.setProperty("idp.webflows", "classpath*:/flows");
+        mock.setProperty("idp.service.logging.resource", "/logback-flow-test.xml");
         mock.setProperty("sp.service.agents.resources", "test.sp.oidc.AgentResolverResources");
         // Use cookie based state management
         mock.setProperty("sp.stateToken.Manager","shibboleth.sp.CookieStateManager");
diff --git a/sp-oidc-conf-impl/src/test/resources/logback-flow-test.xml b/sp-oidc-conf-impl/src/test/resources/logback-flow-test.xml
new file mode 100644
index 0000000..0b70e92
--- /dev/null
+++ b/sp-oidc-conf-impl/src/test/resources/logback-flow-test.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<configuration>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%level [%logger:%line] - %msg%n</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <root>
+        <level value="WARN" />
+        <appender-ref ref="STDOUT" />
+    </root>
+    
+    <logger name="net.shibboleth.sp" level="TRACE" additivity="false">
+        <appender-ref ref="STDOUT" />
+    </logger>
+    
+    <logger name="net.shibboleth.idp" level="DEBUG" additivity="false">
+        <appender-ref ref="STDOUT" />
+    </logger>
+     
+    <logger name="org.springframework.webflow" level="INFO" additivity="false">
+        <appender-ref ref="STDOUT" />
+    </logger>
+    
+</configuration>
\ No newline at end of file
diff --git a/sp-oidc-conf-impl/src/test/resources/logback-test.xml b/sp-oidc-conf-impl/src/test/resources/logback-test.xml
index 55f1d6e..a70b94e 100644
--- a/sp-oidc-conf-impl/src/test/resources/logback-test.xml
+++ b/sp-oidc-conf-impl/src/test/resources/logback-test.xml
@@ -9,7 +9,7 @@
         </encoder>
     </appender>
 
-    <root level="WARN">
+    <root level="INFO">
         <appender-ref ref="STDOUT" />
     </root>
     
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessLogoutInitiatorRequest.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessLogoutInitiatorRequest.java
new file mode 100644
index 0000000..8928ea5
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessLogoutInitiatorRequest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.sp.oidc.profile.impl;
+
+import java.text.ParseException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.JWTParser;
+
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.oidc.context.OIDCLogoutContext;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.ConsumerConstants;
+
+/**
+ * Processes a request to potentially initiate a OIDC RP-Initiated logout by examining the input to recover
+ * the required id_token information from the opaque portion of the session created by the token consumer
+ * flow and store in a newly created {@link OIDCLogoutContext}.
+ * 
+ * <p>Assuming the opaque data is present and sufficient, it also creates a {@link RelyingPartyContext}
+ * based on the identity of the original OP that issued the id_token that led to the session.</p>
+ * 
+ * <p>If the data sealer is enabled, the the id_token is first unsealed.</p>
+ *
+ * @post <pre>profileRequestContext.getSubcontext(OIDCLogoutContext.class) != null</pre>
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#UNABLE_TO_DECODE}
+ * @event {@link IdPEventIds#INVALID_RELYING_PARTY_CTX}
+ */
+public class ProcessLogoutInitiatorRequest extends AbstractApplicationAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ProcessLogoutInitiatorRequest.class);
+    
+    /** Creation strategy for {@link RelyingPartyContext}. */
+    @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextCreationStrategy;
+    
+    /** The id_token take from the data in the session.*/
+    @NonnullBeforeExec private JWT idToken;
+    
+    /** The relying party name to base the inbound context on. */
+    @NonnullBeforeExec private String relyingPartyId;
+    
+    /** Optional data sealer to use. */
+    @Nullable private DataSealer dataSealer;
+    
+    /** Constructor. */
+    public ProcessLogoutInitiatorRequest() {
+        relyingPartyContextCreationStrategy = new ChildContextLookup<>(RelyingPartyContext.class, true);
+    }
+    
+    /**
+     * Sets {@link DataSealer} to use.
+     * 
+     * @param sealer data sealer
+     */
+    public void setDataSealer(@Nullable final DataSealer sealer) {
+        checkSetterPreconditions();
+        
+        dataSealer = sealer;
+    }
+    
+    /**
+     * Set strategy to create the {@link RelyingPartyContext}.
+     * 
+     * @param strategy creation strategy
+     */
+    public void setRelyingPartyContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+        checkSetterPreconditions();
+        
+        relyingPartyContextCreationStrategy =
+                Constraint.isNotNull(strategy, "RelyingPartyContext creation strategy cannot be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        log.debug("{} Evaluating applicability of request to oidc logout initiator flow", getLogPrefix());
+        
+        final DDF input = ensureAgentRequestContext().getInput();
+        if (input == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            log.error("{} No input message from agent", getLogPrefix());
+            return false;
+        }
+        
+        // The ID Token should be in JWT compact form, but should be sealed at this stage
+        final String pickled = input.getmember(ConsumerConstants.SESSION_OPAQUE)
+                .getmember(PrepareAgentResponse.ID_TOKEN_PARAM).string();
+        if (pickled == null) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_DECODE);
+            log.info("{} No encoded id_token in input message", getLogPrefix());
+            return false;
+        }
+        
+        // Attempt to unseal the stored JWT
+        String compactIdToken = pickled;
+        final var localDataSealer = dataSealer;
+        if (localDataSealer != null) {
+            try {
+                compactIdToken = localDataSealer.unwrap(pickled);
+            } catch (final DataSealerException e) {
+                ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_DECODE);
+                log.warn("{} id_token could not be unsealed", getLogPrefix());
+                return false; 
+            }
+        }
+        
+        try {
+            idToken = JWTParser.parse(compactIdToken);
+            if (idToken instanceof EncryptedJWT) {
+                // This should not happen, we are in control of what goes into session data
+                ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_DECODE);
+                log.warn("{} id_token was still encrypted in session data", getLogPrefix());
+                return false;               
+            }
+            final var localIdToken  = idToken;
+            if (localIdToken != null) {
+                // Extract out the identity of the issuer of the token. This is safe, we've already validated the token 
+                // before we added it to the session, and the token should be sealed in storage.
+                final JWTClaimsSet claims = localIdToken.getJWTClaimsSet();
+                if (claims != null) {
+                    relyingPartyId = claims.getIssuer();
+                }
+                if (relyingPartyId == null) {
+                    ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_DECODE);
+                    log.info("{} Could not extract issuer (OP) from id_token", getLogPrefix());
+                    return false;
+                }                
+            } else {
+                ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_DECODE);
+                log.info("{} Could not extract id_token from session", getLogPrefix());
+                return false;
+            }
+        } catch (final ParseException e) {
+            ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_DECODE);
+            log.error("{} Could not parse encoded id_token in input message", getLogPrefix());
+            return false;
+        }
+        
+        return true;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final OIDCLogoutContext logoutContext = profileRequestContext.ensureSubcontext(OIDCLogoutContext.class);
+        logoutContext.setIdToken(idToken);
+        
+        final RelyingPartyContext rpContext = relyingPartyContextCreationStrategy.apply(profileRequestContext);
+        if (rpContext == null) {
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
+            log.error("{} Unable to create RelyingPartyContext", getLogPrefix());
+            return;
+        }
+
+        rpContext.setRelyingPartyId(relyingPartyId);
+        log.debug("{} Initialized RelyingPartyContext for {}", getLogPrefix(), relyingPartyId);
+    
+    }
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BaseOIDCAuthenticationTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BaseOIDCAuthenticationTest.java
index 442e412..80af108 100644
--- a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BaseOIDCAuthenticationTest.java
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/BaseOIDCAuthenticationTest.java
@@ -149,6 +149,7 @@ public abstract class BaseOIDCAuthenticationTest extends BaseApplicationActionTe
         assert outboundMsgCtx != null;
         return outboundMsgCtx;
     }
-    
+
+        
 
 }
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ProcessLogoutInitiatorRequestTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ProcessLogoutInitiatorRequestTest.java
new file mode 100644
index 0000000..7c97620
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ProcessLogoutInitiatorRequestTest.java
@@ -0,0 +1,206 @@
+/*
+ * 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.sp.oidc.profile.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.fail;
+
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.security.credential.BasicCredential;
+import org.opensaml.security.credential.Credential;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWT;
+
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.impl.BasicKeystoreKeyStrategy;
+import net.shibboleth.shared.spring.resource.ResourceHelper;
+import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.oidc.testing.TestHelper;
+import net.shibboleth.sp.profile.ConsumerConstants;
+
+/**
+ * Tests for {@link ProcessLogoutInitiatorRequest}.
+ */
+public class ProcessLogoutInitiatorRequestTest extends BaseOIDCAuthenticationTest {
+
+    /** Issuer to use.*/
+    private static final String ISS = "https://op.example.com";
+    
+    /** Action to test.*/
+    private ProcessLogoutInitiatorRequest action;
+    
+    /** Create a basic shared secret credential to create a MAC.*/
+    private Credential macCredential;
+    
+    /** Data sealer to use if required.*/
+    private DataSealer sealer;
+    
+    @SuppressWarnings("null")
+    @Override
+    @BeforeMethod
+    public void beforeMethod() throws ComponentInitializationException {
+        super.beforeMethod();
+        
+        action = new ProcessLogoutInitiatorRequest();
+
+        KeyGenerator keyGen;
+        try {
+            keyGen = KeyGenerator.getInstance("HmacSHA256");
+        } catch (final NoSuchAlgorithmException e) {
+            throw new ComponentInitializationException("Can not create MAC key for token signing");
+        }
+        keyGen.init(256);        
+        final SecretKey key = keyGen.generateKey();
+        macCredential = new BasicCredential(key);        
+        
+        final ClassPathResource keystoreResource = 
+                new ClassPathResource("/net/shibboleth/sp/oidc/profile/impl/SealerKeyStore.jks");
+        final ClassPathResource versionResource = 
+                new ClassPathResource("/net/shibboleth/sp/oidc/profile/impl/SealerKeyStore.kver");
+        
+        final BasicKeystoreKeyStrategy strategy = new BasicKeystoreKeyStrategy();
+        strategy.setKeyAlias("secret");
+        strategy.setKeyPassword("kpassword");
+        strategy.setKeystorePassword("password");
+        strategy.setKeystoreResource(ResourceHelper.of(keystoreResource));
+        strategy.setKeyVersionResource(ResourceHelper.of(versionResource));
+
+        sealer = new DataSealer();
+        sealer.setKeyStrategy(strategy);
+
+        try {
+            strategy.initialize();
+            sealer.initialize();
+        } catch (final ComponentInitializationException e) {
+            fail(e.getMessage());
+        }
+    }
+
+
+    @Test
+    public void testValidIdToken() throws Exception {
+        action.initialize();
+        final JWT signedIdToken = 
+                TestHelper.createSignedIDToken(ISS, "jdoe", macCredential);
+        
+        final DDF input = TestHelper.buildLogoutInitiatorDDFInput(signedIdToken.serialize(), 
+                PrepareAgentResponse.ID_TOKEN_PARAM);
+
+        final var agentCtx = prc.ensureSubcontext(AgentRequestContext.class);
+        agentCtx.setInput(input);
+
+        final Event event = action.execute(src);
+        assertNull(event);
+        final RelyingPartyContext rpc = prc.getSubcontext(RelyingPartyContext.class);
+        assertNotNull(rpc);
+        assert rpc != null;
+        assertEquals(rpc.getRelyingPartyId(), ISS);
+    }
+    
+    @Test
+    public void testValidSealedToken() throws Exception {
+        action.setDataSealer(sealer);
+        action.initialize();
+        final JWT signedIdToken = 
+                TestHelper.createSignedIDToken(ISS, "jdoe", macCredential);
+        final String idTokenSerialised = signedIdToken.serialize();        
+        final DDF input = TestHelper.buildLogoutInitiatorDDFInput(sealer.wrap(idTokenSerialised), 
+                PrepareAgentResponse.ID_TOKEN_PARAM);
+
+        final var agentCtx = prc.ensureSubcontext(AgentRequestContext.class);
+        agentCtx.setInput(input);
+
+        final Event event = action.execute(src);
+        assertNull(event);
+        final RelyingPartyContext rpc = prc.getSubcontext(RelyingPartyContext.class);
+        assertNotNull(rpc);
+        assert rpc != null;
+        assertEquals(rpc.getRelyingPartyId(), ISS);
+    }
+
+    @Test
+    public void testMissingIdToken() throws ComponentInitializationException {
+        action.initialize();
+        final DDF root = new DDF(null).structure();
+        root.addmember(ConsumerConstants.SESSION_OPAQUE);
+
+        final var agentCtx = prc.ensureSubcontext(AgentRequestContext.class);
+        agentCtx.setInput(root);
+
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assert event != null;
+        assertEquals(event.getId(), EventIds.UNABLE_TO_DECODE);
+    }
+
+    @Test
+    public void testInvalidTokenFormat() throws ComponentInitializationException {
+        action.initialize();
+        final String invalidToken = "not.a.jwt";
+
+        final DDF input = TestHelper.buildLogoutInitiatorDDFInput(invalidToken,  PrepareAgentResponse.ID_TOKEN_PARAM);
+
+        final var agentCtx = prc.ensureSubcontext(AgentRequestContext.class);
+        agentCtx.setInput(input);
+
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assert event != null;
+        assertEquals(event.getId(), EventIds.UNABLE_TO_DECODE);
+    }
+
+    @Test
+    public void testNoIssuer() throws Exception {
+        action.initialize();
+        final JWT signedIdToken = 
+                TestHelper.createSignedIDToken(null, "jdoe", macCredential);
+
+        final DDF input = TestHelper.buildLogoutInitiatorDDFInput(signedIdToken.serialize()
+                , PrepareAgentResponse.ID_TOKEN_PARAM);
+
+        final var agentCtx = prc.ensureSubcontext(AgentRequestContext.class);
+        agentCtx.setInput(input);
+
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assert event != null;
+        assertEquals(event.getId(), EventIds.UNABLE_TO_DECODE);
+    }
+    
+    @Test
+    public void testNoDDF() throws Exception {
+        action.initialize();
+        final Event event = action.execute(src);
+        assertNotNull(event);
+        assert event != null;
+        assertEquals(event.getId(), EventIds.INVALID_MESSAGE);
+    }
+
+}
+
diff --git a/sp-oidc-impl/src/test/resources/net/shibboleth/sp/oidc/profile/impl/SealerKeyStore.jks b/sp-oidc-impl/src/test/resources/net/shibboleth/sp/oidc/profile/impl/SealerKeyStore.jks
new file mode 100644
index 0000000..147d92b
Binary files /dev/null and b/sp-oidc-impl/src/test/resources/net/shibboleth/sp/oidc/profile/impl/SealerKeyStore.jks differ
diff --git a/sp-oidc-impl/src/test/resources/net/shibboleth/sp/oidc/profile/impl/SealerKeyStore.kver b/sp-oidc-impl/src/test/resources/net/shibboleth/sp/oidc/profile/impl/SealerKeyStore.kver
new file mode 100644
index 0000000..c232bd3
--- /dev/null
+++ b/sp-oidc-impl/src/test/resources/net/shibboleth/sp/oidc/profile/impl/SealerKeyStore.kver
@@ -0,0 +1 @@
+CurrentVersion = 2

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


More information about the commits mailing list