[java-plugin-shibd-oidc] 03/06: WiP on logout
Codeberg
noreply at shibboleth.net
Fri Jul 3 13:33:00 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/79a65b781d9db01d7d8e7d8fe308aab7af3877da
commit 79a65b781d9db01d7d8e7d8fe308aab7af3877da
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