[java-plugin-shibd-saml] branch main updated: WIP on SAML Logout Initiator flow.
Codeberg
noreply at shibboleth.net
Tue Apr 7 19:11:50 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-plugin-shibd-saml.
View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-saml/commit/567d27aeaf415423b4d9156ef25e7da284e77476
The following commit(s) were added to refs/heads/main by this push:
new 567d27a WIP on SAML Logout Initiator flow.
567d27a is described below
commit 567d27aeaf415423b4d9156ef25e7da284e77476
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Tue Apr 7 15:11:39 2026 -0400
WIP on SAML Logout Initiator flow.
---
.../sp/saml/saml2/context/SAMLLogoutContext.java | 78 ++++++
.../sp/saml/saml2/context/SAMLTokenContext.java | 24 +-
.../config/SingleLogoutProfileConfiguration.java | 3 +
.../idp/flows/sp/initiator/saml2/saml2-beans.xml | 122 ---------
.../sp/logout/initiator/saml2/saml2-beans.xml | 31 +++
.../flows/sp/logout/initiator/saml2/saml2-flow.xml | 38 +++
.../shibboleth/idp/flows/sp/saml2-common-beans.xml | 127 +++++++++-
.../net/shibboleth/sp/service/agent/postconfig.xml | 2 +
.../flows/saml2/SAML2TokenConsumerFlowTest.java | 5 +-
.../saml/saml2/profile/impl/AddAuthnRequest.java | 1 -
.../saml/saml2/profile/impl/AddLogoutRequest.java | 282 +++++++++++++++++++++
.../saml2/profile/impl/ExtractSAMLAttributes.java | 3 +-
.../saml2/profile/impl/PrepareAgentResponse.java | 23 +-
.../impl/ProcessLogoutInitiatorRequest.java | 193 ++++++++++++++
.../profile/impl/SAMLTokenContextConsumer.java | 2 +-
.../profile/impl/PrepareAgentResponseTest.java | 15 +-
.../impl/ProcessLogoutInitiatorRequestTest.java | 170 +++++++++++++
17 files changed, 966 insertions(+), 153 deletions(-)
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLLogoutContext.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLLogoutContext.java
new file mode 100644
index 0000000..c666bb8
--- /dev/null
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLLogoutContext.java
@@ -0,0 +1,78 @@
+/*
+ * 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.saml.saml2.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+import org.opensaml.saml.saml2.core.NameID;
+
+/**
+ * Manages state for SAML logout flows.
+ */
+public class SAMLLogoutContext extends BaseContext {
+
+ /** NameID from original assertion. */
+ @Nullable private NameID nameID;
+
+ /** Session index. */
+ @Nullable private String sessionIndex;
+
+ /**
+ * Get the SAML {@link NameID} issued with the session.
+ *
+ * @return SAML {@link NameID}
+ */
+ @Nullable public NameID getNameID() {
+ return nameID;
+ }
+
+ /**
+ * Set the SAML {@link NameID} issued with the session.
+ *
+ * @param n the SAML {@link NameID}
+ *
+ * @return this context
+ */
+ @Nonnull public SAMLLogoutContext setNameID(@Nullable final NameID n) {
+ nameID = n;
+
+ return this;
+ }
+
+ /**
+ * Get the SAML SessionIndex value issued with the session.
+ *
+ * @return session index
+ */
+ @Nullable public String getSessionIndex() {
+ return sessionIndex;
+ }
+
+ /**
+ * Set the SAML SessionIndex value issued with the session.
+ *
+ * @param index the index
+ *
+ * @return this context
+ */
+ @Nonnull public SAMLLogoutContext setSessionIndex(@Nullable final String index) {
+ sessionIndex = index;
+
+ return this;
+ }
+
+}
\ No newline at end of file
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java
index ffad84d..496f707 100644
--- a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/context/SAMLTokenContext.java
@@ -18,38 +18,38 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.opensaml.messaging.context.BaseContext;
+import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.AuthnStatement;
-import org.opensaml.saml.saml2.core.Subject;
/**
* Manages state for SAML token consumer flow during final assertion processing.
*/
public class SAMLTokenContext extends BaseContext {
- /** Subject of assertion used to authenticate. */
- @Nullable private Subject subject;
+ /** Assertion used to authenticate. */
+ @Nullable private Assertion assertion;
/** Authentication statement. */
@Nullable private AuthnStatement authnStatement;
/**
- * Get the SAML {@link Subject} from the authentication.
+ * Get the SAML {@link Assertion} from the authentication.
*
- * @return SAML {@link Subject}
+ * @return SAML {@link Assertion}
*/
- @Nullable public Subject getSubject() {
- return subject;
+ @Nullable public Assertion getAssertion() {
+ return assertion;
}
/**
- * Set the SAML {@link Subject} from the authentication.
- *
- * @param sub the SAML {@link Subject}
+ * Set the SAML {@link Assertion} from the authentication.
*
+ * @param a the SAML {@link Assertion}
+ *
* @return this context
*/
- @Nonnull public SAMLTokenContext setSubject(@Nullable final Subject sub) {
- subject = sub;
+ @Nonnull public SAMLTokenContext setAssertion(@Nullable final Assertion a) {
+ assertion = a;
return this;
}
diff --git a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java
index 4a1d087..13dc5c2 100644
--- a/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java
+++ b/sp-saml-api/src/main/java/net/shibboleth/sp/saml/saml2/profile/config/SingleLogoutProfileConfiguration.java
@@ -19,6 +19,8 @@ import javax.annotation.Nullable;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.saml.ext.saml2aslo.Asynchronous;
+import net.shibboleth.shared.annotation.ConfigurationSetting;
+
/** Configuration support for SP SAML 2.0 Single Logout. */
public interface SingleLogoutProfileConfiguration
extends net.shibboleth.saml.saml2.profile.config.SingleLogoutProfileConfiguration {
@@ -32,6 +34,7 @@ public interface SingleLogoutProfileConfiguration
*
* @return true iff the extension should be included in requests
*/
+ @ConfigurationSetting(name="asynchronous")
boolean isAsynchronous(@Nullable final ProfileRequestContext profileRequestContext);
}
\ No newline at end of file
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
index 3ca6f1e..0a111f9 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/saml2/saml2-beans.xml
@@ -18,56 +18,9 @@
p:requireDiscoveryURL="false"
p:requireRelyingPartyId="true" />
- <bean id="PrepareInboundMessageContext"
- class="net.shibboleth.idp.saml.session.impl.PrepareInboundMessageContext" scope="prototype"
- p:relyingPartyLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple" />
-
- <bean id="InitializeOutboundMessageContext"
- class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" scope="prototype"
- p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple" />
-
- <bean id="InitializeMessageChannelSecurityContext"
- class="org.opensaml.profile.action.impl.StaticMessageChannelSecurity" scope="prototype"
- p:confidentialityActive="false"
- p:integrityActive="false" />
-
<util:constant id="shibboleth.EndpointType"
static-field="org.opensaml.saml.saml2.metadata.SingleSignOnService.DEFAULT_ELEMENT_NAME" />
- <util:list id="OutgoingSAML2SPRequestBindings">
- <ref bean="shibboleth.Binding.SAML2Redirect" />
- <ref bean="shibboleth.Binding.SAML2POST" />
- <ref bean="shibboleth.Binding.SAML2POSTSimpleSign" />
- </util:list>
-
- <bean id="OutgoingSAML2SPRequestBindingsStrategy" parent="shibboleth.Functions.Constant"
- c:_0-ref="OutgoingSAML2SPRequestBindings" />
-
- <bean id="PopulateBindingAndEndpointContexts"
- class="net.shibboleth.idp.saml.profile.impl.PopulateBindingAndEndpointContexts" scope="prototype"
- p:endpointResolver-ref="shibboleth.EndpointResolver"
- p:endpointType-ref="shibboleth.EndpointType"
- p:bindingDescriptorsLookupStrategy-ref="OutgoingSAML2SPRequestBindingsStrategy"
- p:artifactImpliesSecureChannel="%{sp.artifact.secureChannel:true}" />
-
- <bean id="PopulateRequestSignatureSigningParameters"
- 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.SignRequestsPredicate"
- p:honorMetadata="%{sp.saml.honorWantAuthnRequestsSigned:true}" />
- </property>
- </bean>
-
- <bean id="PopulateEncryptionParameters"
- class="net.shibboleth.idp.saml.saml2.profile.impl.PopulateEncryptionParameters" scope="prototype"
- p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
- p:encryptionParametersResolver-ref="shibboleth.EncryptionParametersResolver"
- p:protocol-ref="shibboleth.MetadataLookup.Protocol"
- p:role-ref="shibboleth.MetadataLookup.Role" />
-
<util:map id="InboundSAML2BindingMap">
<entry key="POST">
<util:constant static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML2_POST_BINDING_URI" />
@@ -90,55 +43,6 @@
p:defaultIdentifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy" />
</property>
</bean>
-
- <!-- Default formats not to encrypt. -->
- <util:set id="DefaultPlaintextNameIDFormats">
- <util:constant static-field="org.opensaml.saml.saml2.core.NameIDType.ENTITY" />
- </util:set>
-
- <bean id="EncryptNameIDs"
- class="org.opensaml.saml.saml2.profile.impl.EncryptNameIDs" scope="prototype"
- p:excludedFormats="#{getObject('shibboleth.PlaintextNameIDFormats') ?: getObject('DefaultPlaintextNameIDFormats')}"
- p:recipientLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
- <property name="encryptionContextLookupStrategy">
- <bean parent="shibboleth.Functions.Compose"
- c:g-ref="shibboleth.ChildLookup.EncryptionParameters"
- c:f-ref="shibboleth.ChildLookup.RelyingParty" />
- </property>
- </bean>
-
- <bean id="HandleOutboundMessage"
- class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
- c:messageHandler-ref="PreEncodeMessageHandler"
- c:executionDirection="OUTBOUND">
- <property name="errorEvent">
- <util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
- </property>
- </bean>
-
- <bean id="PreEncodeMessageHandler"
- class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
- <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.impl.SAMLOutboundDestinationHandler" scope="prototype"/>
- <bean class="org.opensaml.saml.common.binding.security.impl.EndpointURLSchemeSecurityHandler" scope="prototype"/>
- <bean class="org.opensaml.saml.common.binding.security.impl.SAMLOutboundProtocolMessageSigningHandler" scope="prototype">
- <property name="activationCondition">
- <bean parent="shibboleth.Conditions.NOT">
- <constructor-arg>
- <bean class="org.opensaml.saml.common.messaging.logic.SignatureCapableBindingPredicate" />
- </constructor-arg>
- </bean>
- </property>
- </bean>
- </list>
- </property>
- </bean>
<bean id="IssueCorrelationCookie" class="net.shibboleth.sp.profile.impl.IssueCorrelationCookie" scope="prototype"
p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
@@ -158,30 +62,4 @@
</constructor-arg>
</bean>
- <bean id="messageEncoderFactory" class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageEncoderFactory" />
-
- <bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
- p:createOutputObjects="true"
- p:messageEncoderFactory-ref="messageEncoderFactory" />
-
- <!-- Override IdP's encoders to supply the DDF-backed servlet supplier. -->
-
- <bean id="shibboleth.Encoders.SAML2RedirectEncoder"
- class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPRedirectDeflateEncoder" scope="prototype" init-method=""
- p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier" />
-
- <bean id="shibboleth.Encoders.SAML2PostEncoder"
- class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostEncoder" scope="prototype" init-method=""
- p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
- p:velocityEngine-ref="shibboleth.VelocityEngine"
- p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
- p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
-
- <bean id="shibboleth.Encoders.SAML2PostSimpleSignEncoder"
- class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostSimpleSignEncoder" scope="prototype" init-method=""
- p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
- p:velocityEngine-ref="shibboleth.VelocityEngine"
- p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
- p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
-
</beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-beans.xml
new file mode 100644
index 0000000..d3caea4
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-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/saml2-common-beans.xml" />
+
+ <bean id="ProcessLogoutInitiatorRequest"
+ class="net.shibboleth.sp.saml.saml2.profile.impl.ProcessLogoutInitiatorRequest" scope="prototype"
+ p:parserPool-ref="shibboleth.ParserPool" />
+
+ <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-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-flow.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-flow.xml
new file mode 100644
index 0000000..18f438f
--- /dev/null
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout/initiator/saml2/saml2-flow.xml
@@ -0,0 +1,38 @@
+<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="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/initiator/saml2/saml2-beans.xml" />
+
+</flow>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
index 75423c6..a56652c 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/saml2-common-beans.xml
@@ -21,6 +21,10 @@
</property>
</bean>
+ <bean id="PrepareInboundMessageContext"
+ class="net.shibboleth.idp.saml.session.impl.PrepareInboundMessageContext" scope="prototype"
+ p:relyingPartyLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple" />
+
<util:constant id="shibboleth.MetadataLookup.Protocol"
static-field="org.opensaml.saml.common.xml.SAMLConstants.SAML20P_NS" />
@@ -45,6 +49,32 @@
</constructor-arg>
</bean>
+ <bean id="InitializeOutboundMessageContext"
+ class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" scope="prototype"
+ p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple" />
+
+ <bean id="InitializeMessageChannelSecurityContext"
+ class="org.opensaml.profile.action.impl.StaticMessageChannelSecurity" scope="prototype"
+ p:confidentialityActive="false"
+ p:integrityActive="false" />
+
+ <util:list id="OutgoingSAML2SPRequestBindings">
+ <ref bean="shibboleth.Binding.SAML2Redirect" />
+ <ref bean="shibboleth.Binding.SAML2POST" />
+ <ref bean="shibboleth.Binding.SAML2POSTSimpleSign" />
+ </util:list>
+
+ <bean id="OutgoingSAML2SPRequestBindingsStrategy" parent="shibboleth.Functions.Constant"
+ c:_0-ref="OutgoingSAML2SPRequestBindings" />
+
+ <bean id="PopulateBindingAndEndpointContexts"
+ class="net.shibboleth.idp.saml.profile.impl.PopulateBindingAndEndpointContexts" scope="prototype"
+ p:endpointResolver-ref="shibboleth.EndpointResolver"
+ p:endpointType-ref="shibboleth.EndpointType"
+ p:bindingDescriptorsLookupStrategy-ref="OutgoingSAML2SPRequestBindingsStrategy"
+ p:artifactImpliesSecureChannel="%{sp.artifact.secureChannel:true}" />
+
+
<bean id="InitializeRelyingPartyContextFromSAMLPeer"
class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" scope="prototype" />
@@ -55,6 +85,27 @@
class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
p:profileId-ref="shiibboleth.sp.ProfileId" />
+ <bean id="PopulateInboundMessageContextWithSAMLSelf"
+ class="net.shibboleth.idp.saml.profile.impl.PopulateInboundMessageContextWithSAMLSelf" scope="prototype" />
+
+ <bean id="PopulateRequestSignatureSigningParameters"
+ 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.SignRequestsPredicate"
+ p:honorMetadata="%{sp.saml.honorWantAuthnRequestsSigned:true}" />
+ </property>
+ </bean>
+
+ <bean id="PopulateEncryptionParameters"
+ class="net.shibboleth.idp.saml.saml2.profile.impl.PopulateEncryptionParameters" scope="prototype"
+ p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
+ p:encryptionParametersResolver-ref="shibboleth.EncryptionParametersResolver"
+ p:protocol-ref="shibboleth.MetadataLookup.Protocol"
+ p:role-ref="shibboleth.MetadataLookup.Role" />
+
<bean id="PopulateSignatureValidationParameters"
class="org.opensaml.profile.action.impl.PopulateSignatureValidationParameters" scope="prototype"
p:configurationLookupStrategy-ref="shibboleth.SignatureValidationConfigurationLookup"
@@ -65,7 +116,79 @@
p:configurationLookupStrategy-ref="shibboleth.ClientTLSValidationConfigurationLookup"
p:clientTLSValidationParametersResolver-ref="shibboleth.ClientTLSValidationParametersResolver" />
- <bean id="PopulateInboundMessageContextWithSAMLSelf"
- class="net.shibboleth.idp.saml.profile.impl.PopulateInboundMessageContextWithSAMLSelf" scope="prototype" />
+ <!-- Default formats not to encrypt. -->
+ <util:set id="DefaultPlaintextNameIDFormats">
+ <util:constant static-field="org.opensaml.saml.saml2.core.NameIDType.ENTITY" />
+ </util:set>
+ <bean id="EncryptNameIDs"
+ class="org.opensaml.saml.saml2.profile.impl.EncryptNameIDs" scope="prototype"
+ p:excludedFormats="#{getObject('shibboleth.PlaintextNameIDFormats') ?: getObject('DefaultPlaintextNameIDFormats')}"
+ p:recipientLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
+ <property name="encryptionContextLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookup.EncryptionParameters"
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+ </property>
+ </bean>
+
+ <bean id="HandleOutboundMessage"
+ class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+ c:messageHandler-ref="PreEncodeMessageHandler"
+ c:executionDirection="OUTBOUND">
+ <property name="errorEvent">
+ <util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
+ </property>
+ </bean>
+
+ <bean id="PreEncodeMessageHandler"
+ class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain" scope="prototype">
+ <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.impl.SAMLOutboundDestinationHandler" scope="prototype"/>
+ <bean class="org.opensaml.saml.common.binding.security.impl.EndpointURLSchemeSecurityHandler" scope="prototype"/>
+ <bean class="org.opensaml.saml.common.binding.security.impl.SAMLOutboundProtocolMessageSigningHandler" scope="prototype">
+ <property name="activationCondition">
+ <bean parent="shibboleth.Conditions.NOT">
+ <constructor-arg>
+ <bean class="org.opensaml.saml.common.messaging.logic.SignatureCapableBindingPredicate" />
+ </constructor-arg>
+ </bean>
+ </property>
+ </bean>
+ </list>
+ </property>
+ </bean>
+
+ <bean id="messageEncoderFactory" class="net.shibboleth.idp.saml.profile.impl.SpringAwareMessageEncoderFactory" />
+
+ <bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
+ p:createOutputObjects="true"
+ p:messageEncoderFactory-ref="messageEncoderFactory" />
+
+ <!-- Override IdP's encoders to supply the DDF-backed servlet supplier. -->
+
+ <bean id="shibboleth.Encoders.SAML2RedirectEncoder"
+ class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPRedirectDeflateEncoder" scope="prototype" init-method=""
+ p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier" />
+
+ <bean id="shibboleth.Encoders.SAML2PostEncoder"
+ class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostEncoder" scope="prototype" init-method=""
+ p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
+ p:velocityEngine-ref="shibboleth.VelocityEngine"
+ p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
+ p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
+
+ <bean id="shibboleth.Encoders.SAML2PostSimpleSignEncoder"
+ class="org.opensaml.saml.saml2.binding.encoding.impl.HTTPPostSimpleSignEncoder" scope="prototype" init-method=""
+ p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
+ p:velocityEngine-ref="shibboleth.VelocityEngine"
+ p:cSPDigester="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
+ p:cSPNonceGenerator="#{%{sp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
+
</beans>
diff --git a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
index 0e2882b..a7fbdcd 100644
--- a/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
+++ b/sp-saml-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
@@ -22,6 +22,7 @@
p:order="%{sp.saml.relativeOrder:1}"
p:metadataResolver-ref="shibboleth.MetadataResolverService"
p:sessionInitiators="saml2"
+ p:logoutInitiators="saml2"
p:tokenConsumers="#{{ 'saml2/post', 'saml2/post-simplesign', 'saml2/artifact' }}">
<property name="id">
<util:constant static-field="net.shibboleth.sp.saml.saml2.SAML2ProtocolSupportService.PROTOCOL_ID" />
@@ -29,6 +30,7 @@
<property name="defaultProfileConfigurations">
<list>
<ref bean="SAML2.SSO" />
+ <ref bean="SAML2.Logout" />
</list>
</property>
<property name="metadataDrivenDefaultProfileConfigurations">
diff --git a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
index b23b78b..a589534 100644
--- a/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
+++ b/sp-saml-conf-impl/src/test/java/net/shibboleth/sp/saml/flows/saml2/SAML2TokenConsumerFlowTest.java
@@ -82,6 +82,7 @@ import net.shibboleth.sp.flows.AbstractSPFlowTest;
import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
import net.shibboleth.sp.messaging.RemotedHttpServletResponse;
import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.saml.saml2.profile.impl.PrepareAgentResponse;
/**
* Unit test for the SP session-initiator flow.
@@ -616,7 +617,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
final DDF s = output.getmember(ConsumerConstants.SESSION_OPAQUE);
assert s != null;
Assert.assertTrue(s.isstruct());
- final DDF nameIdDdf = s.getmember("nameID");
+ final DDF nameIdDdf = s.getmember(PrepareAgentResponse.NAMEID_PARAM);
Assert.assertTrue(nameIdDdf.isstring());
final String nameIdString = nameIdDdf.string();
assert nameIdString != null;
@@ -626,7 +627,7 @@ public class SAML2TokenConsumerFlowTest extends AbstractSPFlowTest {
if (obj instanceof final NameID nameID) {
Assert.assertEquals(nameID.getValue(), "jdoe at example.org");
Assert.assertEquals(nameID.getFormat(), NameIDType.EMAIL);
- Assert.assertEquals(nameID.getSPProvidedID(), sessionIndex);
+ Assert.assertEquals(nameID.getSPProvidedID(), ISSUER + "!!" + sessionIndex);
} else {
Assert.fail("Session data was not a NameID");
}
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
index 66d030a..055c45f 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddAuthnRequest.java
@@ -586,7 +586,6 @@ public class AddAuthnRequest extends AbstractApplicationAction {
*/
@Nullable private Extensions buildExtensions(@Nonnull final ProfileRequestContext profileRequestContext) {
- assert profileConfiguration!=null;
final Collection<RequestedAttribute> attrs = profileConfiguration.getRequestedAttributes(profileRequestContext);
if (!attrs.isEmpty()) {
final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddLogoutRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddLogoutRequest.java
new file mode 100644
index 0000000..011a05b
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/AddLogoutRequest.java
@@ -0,0 +1,282 @@
+/*
+ * 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.saml.saml2.profile.impl;
+
+import java.time.Instant;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.profile.config.navigate.IdentifierGenerationStrategyLookupFunction;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.SPConstants;
+import net.shibboleth.sp.saml.saml2.context.SAMLLogoutContext;
+import net.shibboleth.sp.saml.saml2.profile.config.SingleLogoutProfileConfiguration;
+
+import org.opensaml.core.xml.XMLObjectBuilderFactory;
+import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport;
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.core.xml.io.UnmarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.saml.common.SAMLObjectBuilder;
+import org.opensaml.saml.common.SAMLVersion;
+import org.opensaml.saml.common.binding.SAMLBindingSupport;
+import org.opensaml.saml.ext.saml2aslo.Asynchronous;
+import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.Extensions;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.SessionIndex;
+import org.slf4j.Logger;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action that creates an {@link LogoutRequest} and sets it as the message returned by
+ * {@link ProfileRequestContext#getOutboundMessageContext()}.
+ *
+ * <p>If an issuer value is returned via a lookup strategy, then it's set as the Issuer of the message.</p>
+ *
+ * <p>A {@link SAMLLogoutContext} must be present to provide the necessary information to populate into
+ * the request.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#IO_ERROR}
+ * @event {@link EventIds#INVALID_MSG_CTX}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ *
+ * @pre ProfileRequestContext.getSubcontext(SAMLLogoutContext.class) != null
+ * @post ProfileRequestContext.getOutboundMessageContext().getMessage() != null
+ */
+public class AddLogoutRequest extends AbstractApplicationAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(AddLogoutRequest.class);
+
+ /** Overwrite an existing message? */
+ private boolean overwriteExisting;
+
+ /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
+ @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+
+ /** Strategy used to obtain the request issuer value. */
+ @Nullable private Function<ProfileRequestContext,String> issuerLookupStrategy;
+
+ /** The generator to use. */
+ @NonnullBeforeExec private IdentifierGenerationStrategy idGenerator;
+
+ /** Applicable profile configuration. */
+ @NonnullBeforeExec private SingleLogoutProfileConfiguration profileConfiguration;
+
+ /** Cached logout context. */
+ @NonnullBeforeExec private SAMLLogoutContext logoutContext;
+
+ /** EntityID to populate into Issuer element. */
+ @Nullable private String issuerId;
+
+ /** Constructor. */
+ public AddLogoutRequest() {
+ // Default strategy is a 16-byte secure random source.
+ idGeneratorLookupStrategy = new IdentifierGenerationStrategyLookupFunction();
+
+ issuerLookupStrategy = new IssuerLookupFunction();
+ }
+
+ /**
+ * Set whether to overwrite an existing message.
+ *
+ * @param flag flag to set
+ */
+ public void setOverwriteExisting(final boolean flag) {
+ checkSetterPreconditions();
+ overwriteExisting = flag;
+ }
+
+ /**
+ * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setIdentifierGeneratorLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,IdentifierGenerationStrategy> strategy) {
+ checkSetterPreconditions();
+ idGeneratorLookupStrategy =
+ Constraint.isNotNull(strategy, "IdentifierGenerationStrategy lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the issuer value to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setIssuerLookupStrategy(@Nullable final Function<ProfileRequestContext,String> strategy) {
+ checkSetterPreconditions();
+ issuerLookupStrategy = strategy;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ logoutContext = profileRequestContext.getSubcontext(SAMLLogoutContext.class);
+ if (logoutContext == null || logoutContext.getNameID() == null) {
+ log.error("{} No populated SAMLLogoutContext available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ final RelyingPartyContext rpCtx = profileRequestContext.getSubcontext(RelyingPartyContext.class);
+ if (rpCtx != null && rpCtx.getProfileConfig() instanceof SingleLogoutProfileConfiguration slo) {
+ profileConfiguration = slo;
+ }
+ if (profileConfiguration == null) {
+ log.error("{} SingleLogoutProfileConfiguration not found", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+
+ final MessageContext outboundMessageCtx = profileRequestContext.getOutboundMessageContext();
+ if (outboundMessageCtx == null) {
+ log.debug("{} No outbound message context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ } else if (!overwriteExisting && outboundMessageCtx.getMessage() != null) {
+ log.debug("{} Outbound message context already contains a message", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
+ return false;
+ }
+
+ idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+ if (idGenerator == null) {
+ log.debug("{} No identifier generation strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ if (issuerLookupStrategy != null) {
+ issuerId = issuerLookupStrategy.apply(profileRequestContext);
+ }
+
+ outboundMessageCtx.setMessage(null);
+
+ return true;
+ }
+
+// Checkstyle: MethodLength OFF
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+ final SAMLObjectBuilder<LogoutRequest> requestBuilder =
+ (SAMLObjectBuilder<LogoutRequest>) bf.<LogoutRequest>ensureBuilder(
+ LogoutRequest.DEFAULT_ELEMENT_NAME);
+
+ final LogoutRequest object = requestBuilder.buildObject();
+ object.setID(idGenerator.generateIdentifier());
+ object.setIssueInstant(Instant.now());
+ object.setVersion(SAMLVersion.VERSION_20);
+
+ if (issuerId != null) {
+ log.debug("{} Setting Issuer to {}", getLogPrefix(), issuerId);
+ final SAMLObjectBuilder<Issuer> issuerBuilder =
+ (SAMLObjectBuilder<Issuer>) bf.<Issuer>ensureBuilder(Issuer.DEFAULT_ELEMENT_NAME);
+ final Issuer issuer = issuerBuilder.buildObject();
+ issuer.setValue(issuerId);
+ object.setIssuer(issuer);
+ } else {
+ log.debug("{} No issuer value available, leaving Issuer unset", getLogPrefix());
+ }
+
+ final NameID original = logoutContext.getNameID();
+ assert original != null;
+ try {
+ final NameID cloned = XMLObjectSupport.cloneXMLObject(original);
+ object.setNameID(cloned);
+ log.debug("{} Populating LogoutRequest with NameID '{}', Format '{}', SessionIndex '{}'", getLogPrefix(),
+ cloned.getValue(), cloned.getFormat(), logoutContext.getSessionIndex());
+
+ } catch (final MarshallingException | UnmarshallingException e) {
+ log.error("{} Error cloning NameID for insertion", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+ return;
+ }
+
+ if (logoutContext.getSessionIndex() != null) {
+ final SAMLObjectBuilder<SessionIndex> indexBuilder =
+ (SAMLObjectBuilder<SessionIndex>) bf.<SessionIndex>ensureBuilder(
+ SessionIndex.DEFAULT_ELEMENT_NAME);
+ final SessionIndex index = indexBuilder.buildObject();
+ index.setValue(logoutContext.getSessionIndex());
+ object.getSessionIndexes().add(index);
+ }
+
+ object.setExtensions(buildExtensions(profileRequestContext));
+
+ profileRequestContext.ensureOutboundMessageContext().setMessage(object);
+
+ // Check for RelayState.
+ final DDF input = ensureAgentRequestContext().getInput();
+ final String relayState = input != null ? input.getmember(SPConstants.STATE).string() : null;
+ if (relayState != null) {
+ SAMLBindingSupport.setRelayState(profileRequestContext.ensureOutboundMessageContext(), relayState);
+ }
+
+ log.info("{} Generated LogoutRequest with ID {} from {}", getLogPrefix(), object.getID(), issuerId);
+ }
+
+ /**
+ * Build {@link Asynchronous} extension if required.
+ *
+ * @param profileRequestContext current profile request context
+ *
+ * @return extension or null
+ */
+ @Nullable private Extensions buildExtensions(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (profileConfiguration.isAsynchronous(profileRequestContext)) {
+ final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory();
+ final SAMLObjectBuilder<Extensions> extBuilder =
+ (SAMLObjectBuilder<Extensions>) bf.<Extensions>ensureBuilder(
+ Extensions.DEFAULT_ELEMENT_NAME);
+ final SAMLObjectBuilder<Asynchronous> asyncBuilder =
+ (SAMLObjectBuilder<Asynchronous>) bf.<Asynchronous>ensureBuilder(
+ Asynchronous.DEFAULT_ELEMENT_NAME);
+
+ final Extensions ext = extBuilder.buildObject();
+ ext.getUnknownXMLObjects().add(asyncBuilder.buildObject());
+ return ext;
+ }
+
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
index 7a327df..f5d8676 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ExtractSAMLAttributes.java
@@ -362,7 +362,8 @@ public class ExtractSAMLAttributes extends AbstractApplicationAction {
final Multimap<String,IdPAttribute> mapped = HashMultimap.create();
assert mapped != null;
- final Subject subject = samlTokenContext.getSubject();
+ final Assertion primary = samlTokenContext.getAssertion();
+ final Subject subject = primary != null ? primary.getSubject() : null;
final NameID nameID = subject != null ? subject.getNameID() : null;
final AuthnStatement theStatement = samlTokenContext.getAuthnStatement();
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java
index 68e3a93..be30ad1 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponse.java
@@ -27,13 +27,16 @@ 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.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.AuthnStatement;
+import org.opensaml.saml.saml2.core.Issuer;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.Subject;
import org.slf4j.Logger;
import net.shibboleth.idp.attribute.context.AttributeContext;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.codec.Base64Support;
import net.shibboleth.shared.codec.EncodingException;
import net.shibboleth.shared.collection.CollectionSupport;
@@ -60,6 +63,9 @@ import net.shibboleth.sp.saml.saml2.context.SAMLTokenContext;
*/
public class PrepareAgentResponse extends AbstractTokenConsumerResponseAction {
+ /** Parameter for accessing NameID from opaque data. */
+ @Nonnull @NotEmpty public static final String NAMEID_PARAM = "NameID";
+
/** DOM configuration parameters used by LSSerializer to exclude XML declaration. */
@Nonnull private static final Map<String, Object> NO_XML_DECL_PARAMS;
@@ -115,23 +121,28 @@ public class PrepareAgentResponse extends AbstractTokenConsumerResponseAction {
@Override
@Nullable protected DDF getSessionData(@Nonnull final ProfileRequestContext profileRequestContext) {
- final Subject subject = samlTokenContext.getSubject();
+ final Assertion assertion = samlTokenContext.getAssertion();
+ final Issuer issuer = assertion != null ? assertion.getIssuer() : null;
+ final Subject subject = assertion != null ? assertion.getSubject() : null;
final NameID nameID = subject != null ? subject.getNameID() : null;
- if (nameID == null) {
- log.debug("{} No NameID found in assertion, no session data to attach", getLogPrefix());
+
+ if (issuer == null || issuer.getValue() == null || nameID == null || nameID.getValue() == null) {
+ log.debug("{} No Issuer or NameID found in Assertion, no session data to attach", getLogPrefix());
return null;
}
final AuthnStatement statement = samlTokenContext.getAuthnStatement();
final String sessionIndex = statement != null ? statement.getSessionIndex() : null;
+ // If you tell anyone I did this, I will be sad.
if (sessionIndex != null) {
- // If you tell anyone I did this, I will be sad.
- nameID.setSPProvidedID(sessionIndex);
+ nameID.setSPProvidedID(issuer.getValue() + "!!" + sessionIndex);
+ } else {
+ nameID.setSPProvidedID(issuer.getValue());
}
try {
final String xml = SerializeSupport.nodeToString(XMLObjectSupport.marshall(nameID), NO_XML_DECL_PARAMS);
- return new DDF("nameID").string(Base64Support.encodeURLSafe(xml.getBytes(StandardCharsets.UTF_8)));
+ return new DDF(NAMEID_PARAM).string(Base64Support.encodeURLSafe(xml.getBytes(StandardCharsets.UTF_8)));
} catch (final MarshallingException | EncodingException e) {
log.error("{} Error marshalling and encoding NameID", getLogPrefix(), e);
}
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutInitiatorRequest.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutInitiatorRequest.java
new file mode 100644
index 0000000..a707ff9
--- /dev/null
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutInitiatorRequest.java
@@ -0,0 +1,193 @@
+/*
+ * 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.saml.saml2.profile.impl;
+
+import java.io.StringReader;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.core.xml.io.UnmarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+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.opensaml.saml.saml2.core.NameID;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.xml.ParserPool;
+import net.shibboleth.shared.xml.XMLParserException;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.saml.saml2.context.SAMLLogoutContext;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Processes a request to "possibly" initiate a SAML 2.0 logout by examining the input to recover
+ * the required information from the opaque portion of the session created by the token consumer
+ * flow.
+ *
+ * <p>Assuming the opaque data is present and sufficient, it creates a {@link RelyingPartyContext}
+ * based on the identity of the original IdP that issued the token that led to the session.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @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);
+
+ /** Parser machinery. */
+ @NonnullAfterInit private ParserPool parserPool;
+
+ /** Creation strategy for {@link RelyingPartyContext}. */
+ @Nonnull private Function<ProfileRequestContext,RelyingPartyContext> relyingPartyContextCreationStrategy;
+
+ /** The relying party name to base the inbound context on. */
+ @NonnullBeforeExec private String relyingPartyId;
+
+ /** NameID from session. */
+ @NonnullBeforeExec private NameID nameID;
+
+ /** Session index from session. */
+ @Nullable private String sessionIndex;
+
+ /** Constructor. */
+ public ProcessLogoutInitiatorRequest() {
+ relyingPartyContextCreationStrategy = new ChildContextLookup<>(RelyingPartyContext.class, true);
+ }
+
+ /**
+ * Sets the {@link ParserPool} to parse session data with.
+ *
+ * @param pool parser pool
+ */
+ public void setParserPool(@Nonnull final ParserPool pool) {
+ checkSetterPreconditions();
+
+ parserPool = pool;
+ }
+
+ /**
+ * Set an optional lookup strategy to identify the relying party name, as a substitute for the session/logout
+ * assumptions made by the action otherwise.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRelyingPartyContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext,RelyingPartyContext> strategy) {
+ checkSetterPreconditions();
+
+ relyingPartyContextCreationStrategy =
+ Constraint.isNotNull(strategy, "RelyingPartyContext creation strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (parserPool == null) {
+ throw new ComponentInitializationException("ParserPool 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 saml2 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;
+ }
+
+ final String pickled = input.getmember(ConsumerConstants.SESSION_OPAQUE).getmember(PrepareAgentResponse.NAMEID_PARAM).string();
+ if (pickled == null) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_DECODE);
+ log.debug("{} No encoded NameID found in input message", getLogPrefix());
+ return false;
+ }
+
+ // Decode NameID from session data from Agent and unpack the buried information.
+ try {
+ final XMLObject xmlObject = XMLObjectSupport.unmarshallFromReader(parserPool, new StringReader(pickled));
+ if (xmlObject instanceof NameID n) {
+ final String buriedData = n.getSPProvidedID();
+ if (buriedData != null) {
+ final int index = buriedData.indexOf("!!");
+ if (index > 0) {
+ relyingPartyId = buriedData.substring(0, index);
+ sessionIndex = buriedData.substring(index + 2);
+ } else {
+ relyingPartyId = buriedData;
+ }
+ nameID = n;
+ } else {
+ throw new XMLParserException("Decoded NameID did not contain required data for logout.");
+ }
+ } else {
+ throw new XMLParserException("Decoded object was of unexpected type.");
+ }
+ } catch (final XMLParserException | UnmarshallingException e) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_DECODE);
+ log.error("{} Failed to decode session information", getLogPrefix(), e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final SAMLLogoutContext logoutContext = profileRequestContext.ensureSubcontext(SAMLLogoutContext.class);
+ nameID.setSPProvidedID(null);
+ logoutContext.setNameID(nameID);
+ logoutContext.setSessionIndex(sessionIndex);
+
+ 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);
+ }
+
+}
\ No newline at end of file
diff --git a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java
index 6a2ab23..9f35ffe 100644
--- a/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java
+++ b/sp-saml-impl/src/main/java/net/shibboleth/sp/saml/saml2/profile/impl/SAMLTokenContextConsumer.java
@@ -48,7 +48,7 @@ public class SAMLTokenContextConsumer implements BiConsumer<ProfileRequestContex
tokenContext.setAuthnStatement(statement);
if (statement.getParent() instanceof Assertion assertion) {
- tokenContext.setSubject(assertion.getSubject());
+ tokenContext.setAssertion(assertion);
}
}
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponseTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponseTest.java
index 9e26644..d4a3064 100644
--- a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponseTest.java
+++ b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/PrepareAgentResponseTest.java
@@ -145,7 +145,7 @@ public class PrepareAgentResponseTest extends BaseAgplicationActionTest {
final DDF state = out.getmember(ConsumerConstants.SESSION_OPAQUE);
Assert.assertTrue(state.isstruct());
- final DDF nameId = state.getmember("nameID");
+ final DDF nameId = state.getmember(PrepareAgentResponse.NAMEID_PARAM);
Assert.assertTrue(nameId.isstring());
final String encoded = nameId.string();
@@ -160,7 +160,7 @@ public class PrepareAgentResponseTest extends BaseAgplicationActionTest {
parserPool.destroy();
if (xmlobj instanceof final NameID nameID) {
Assert.assertEquals(nameID.getValue(), "jdoe");
- Assert.assertEquals(nameID.getSPProvidedID(), "foo");
+ Assert.assertEquals(nameID.getSPProvidedID(), "foo!!foo");
} else {
Assert.fail("XMLObject stored in NameID field was not a NameID");
}
@@ -279,17 +279,20 @@ public class PrepareAgentResponseTest extends BaseAgplicationActionTest {
* Adds mock assertion content to the SAMLTokenContext.
*/
private void buildAssertion() {
- final Subject subject = SAML2ActionTestingSupport.buildSubject("jdoe");
- samlTokenContext.setSubject(subject);
-
final AuthnStatement statement = SAML2ActionTestingSupport.buildAuthnStatement();
statement.setSessionIndex("foo");
samlTokenContext.setAuthnStatement(statement);
final Assertion assertion = SAML2ActionTestingSupport.buildAssertion();
- assertion.setSubject(subject);
assertion.getAuthnStatements().add(statement);
+
+ assertion.setIssuer(SAML2ActionTestingSupport.buildIssuer("foo"));
+
+ final Subject subject = SAML2ActionTestingSupport.buildSubject("jdoe");
+ assertion.setSubject(subject);
+
+ samlTokenContext.setAssertion(assertion);
}
}
\ No newline at end of file
diff --git a/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutInitiatorRequestTest.java b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutInitiatorRequestTest.java
new file mode 100644
index 0000000..a9d4f25
--- /dev/null
+++ b/sp-saml-impl/src/test/java/net/shibboleth/sp/saml/saml2/profile/impl/ProcessLogoutInitiatorRequestTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.saml.saml2.profile.impl;
+
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.saml.saml2.core.NameID;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.xml.impl.BasicParserPool;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.ConsumerConstants;
+import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
+import net.shibboleth.sp.saml.saml2.context.SAMLLogoutContext;
+
+/**
+ * Unit test for {@link ProcessLogoutInitiatorRequest} action.
+ */
+ at SuppressWarnings("javadoc")
+public class ProcessLogoutInitiatorRequestTest extends BaseAgplicationActionTest {
+
+ private ProcessLogoutInitiatorRequest action;
+
+ /**
+ * Set up test.
+ *
+ * @throws ComponentInitializationException
+ */
+ @BeforeMethod
+ public void setUp() throws ComponentInitializationException {
+ super.beforeMethod();
+ prc.removeSubcontext(RelyingPartyContext.class);
+
+ final BasicParserPool pool = new BasicParserPool();
+ pool.initialize();
+
+ action = new ProcessLogoutInitiatorRequest();
+ action.setParserPool(pool);
+ action.initialize();
+ }
+
+ /**
+ * Tear down test.
+ */
+ @AfterMethod
+ public void tearDown() {
+ action.destroy();
+ }
+
+
+ @Test(expectedExceptions=ComponentInitializationException.class)
+ public void testNoParserPool() throws ComponentInitializationException {
+ new ProcessLogoutInitiatorRequest().initialize();
+ }
+
+ @Test
+ public void testNoInput() throws ComponentInitializationException {
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, EventIds.INVALID_MESSAGE);
+ Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+ }
+
+ @Test
+ public void testNoSessionData() throws ComponentInitializationException {
+ final DDF input = new DDF(null).structure();
+ arc.setInput(input);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, EventIds.UNABLE_TO_DECODE);
+ Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+ }
+
+ @Test
+ public void testMisnamedSessionData() throws ComponentInitializationException {
+ final DDF input = new DDF(null).structure();
+ input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember("foo").string("bar");
+ arc.setInput(input);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, EventIds.UNABLE_TO_DECODE);
+ Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+ }
+
+ @Test
+ public void testInvalidSessionData() throws ComponentInitializationException {
+ final DDF input = new DDF(null).structure();
+ input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string("bar");
+ arc.setInput(input);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, EventIds.UNABLE_TO_DECODE);
+ Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+ }
+
+ @Test
+ public void testIncompleteSessionData() throws ComponentInitializationException {
+ final DDF input = new DDF(null).structure();
+ input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(
+ "<NameID xmlns='urn:oasis:names:tc:SAML:2.0:assertion' Format='bar'>foo</NameID>");
+ arc.setInput(input);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, EventIds.UNABLE_TO_DECODE);
+ Assert.assertNull(prc.getSubcontext(RelyingPartyContext.class));
+ }
+
+ @Test
+ public void testNoIndex() throws ComponentInitializationException {
+ final DDF input = new DDF(null).structure();
+ input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(
+ "<NameID xmlns='urn:oasis:names:tc:SAML:2.0:assertion' Format='bar' SPProvidedID='https://idp.example.org/idp'>foo</NameID>");
+ arc.setInput(input);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(prc.ensureSubcontext(RelyingPartyContext.class).getRelyingPartyId(), "https://idp.example.org/idp");
+
+ final SAMLLogoutContext logoutContext = prc.getSubcontext(SAMLLogoutContext.class);
+ assert logoutContext != null;
+ Assert.assertNull(logoutContext.getSessionIndex());
+
+ final NameID nameID = logoutContext.getNameID();
+ assert nameID != null;
+ Assert.assertEquals(nameID.getValue(), "foo");
+ Assert.assertEquals(nameID.getFormat(), "bar");
+ Assert.assertNull(nameID.getSPProvidedID());
+ }
+
+ @Test
+ public void testIndex() throws ComponentInitializationException {
+ final DDF input = new DDF(null).structure();
+ input.addmember(ConsumerConstants.SESSION_OPAQUE).addmember(PrepareAgentResponse.NAMEID_PARAM).string(
+ "<NameID xmlns='urn:oasis:names:tc:SAML:2.0:assertion' Format='bar' SPProvidedID='https://idp.example.org/idp!!12345'>foo</NameID>");
+ arc.setInput(input);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertEquals(prc.ensureSubcontext(RelyingPartyContext.class).getRelyingPartyId(), "https://idp.example.org/idp");
+
+ final SAMLLogoutContext logoutContext = prc.getSubcontext(SAMLLogoutContext.class);
+ assert logoutContext != null;
+ Assert.assertEquals(logoutContext.getSessionIndex(), "12345");
+
+ final NameID nameID = logoutContext.getNameID();
+ assert nameID != null;
+ Assert.assertEquals(nameID.getValue(), "foo");
+ Assert.assertEquals(nameID.getFormat(), "bar");
+ Assert.assertNull(nameID.getSPProvidedID());
+ }
+
+}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list