[java-idp-oidc] 11/14: JOIDC-13 - Support for OIDC Logout

Henri Mikkonen henri.mikkonen at iki.fi
Wed Jan 3 14:07:18 UTC 2024


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

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

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=412d81a1e81a9a64de907895a7a8e12218599d95

commit 412d81a1e81a9a64de907895a7a8e12218599d95
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Nov 24 18:17:37 2023 +0200

    JOIDC-13 - Support for OIDC Logout
    
    https://shibboleth.atlassian.net/browse/JOIDC-13
    
    Improved the logout propagation action hierarchy and added unit tests.
---
 .../impl/AbstractOIDCLogoutPropagationAction.java  |  66 +++++++-
 .../impl/ExecuteBackChannelLogoutRequest.java      |  55 +-----
 .../impl/FormOutboundLogoutRequestMessage.java     |  39 +----
 .../impl/PrepareBackChannelLogoutRequest.java      |  57 +++----
 .../op/logout/profile/impl/RevokeTokenChain.java   |  44 +----
 .../profile/impl/SetFrontChannelLogoutSuccess.java |  58 +------
 ...OIDCBackChannelLogoutPropagationActionTest.java |  51 ++++++
 .../impl/BaseOIDCLogoutPropagationActionTest.java  |  98 +++++++++++
 .../impl/ExecuteBackChannelLogoutRequestTest.java  | 185 +++++++++++++++++++++
 .../impl/FormOutboundLogoutRequestMessageTest.java | 111 +++++++++++++
 .../impl/PrepareBackChannelLogoutRequestTest.java  |  81 +++++++++
 .../logout/profile/impl/RevokeTokenChainTest.java  | 108 ++++++++++++
 .../impl/SetFrontChannelLogoutSuccessTest.java     |  47 ++++++
 13 files changed, 788 insertions(+), 212 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/AbstractOIDCLogoutPropagationAction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/AbstractOIDCLogoutPropagationAction.java
index e36d8e26..2be84f24 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/AbstractOIDCLogoutPropagationAction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/AbstractOIDCLogoutPropagationAction.java
@@ -16,13 +16,19 @@ package net.shibboleth.idp.plugin.oidc.op.logout.profile.impl;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCLogoutPropagationContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.session.context.LogoutPropagationContext;
+import net.shibboleth.shared.logic.Constraint;
 
+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 org.slf4j.LoggerFactory;
 
+import java.util.function.Function;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
@@ -34,9 +40,36 @@ public class AbstractOIDCLogoutPropagationAction extends AbstractOIDCResponseAct
     /** Class logger. */
     @Nonnull private static final Logger log = LoggerFactory.getLogger(AbstractOIDCLogoutPropagationAction.class);
 
+    /** The lookup strategy for the logout propagation context. */
+    @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> logoutPropagationContextLookupStrategy;
+
     /** OIDC logout propagation context. */
     @Nullable private OIDCLogoutPropagationContext oidcLogoutContext;
 
+    /** The logout propagation context to operate on. */
+    @Nullable private LogoutPropagationContext logoutPropagationContext;
+
+    /** The OIDC RP session containing the data for logout propagation. */
+    @Nullable private OIDCRPSession oidcRPSession;
+
+    /**
+     * Constructor.
+     */
+    public AbstractOIDCLogoutPropagationAction() {
+        logoutPropagationContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
+    }
+
+    /**
+     * Set the lookup strategy for the logout propagation context.
+     * @param strategy What to set
+     */
+    public void setLogoutPropagationContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, LogoutPropagationContext> strategy) {
+        checkSetterPreconditions();
+        logoutPropagationContextLookupStrategy = Constraint.isNotNull(strategy,
+                "LogoutPropagationContext lookup strategy cannot be null");
+    }
+
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull ProfileRequestContext profileRequestContext) {
@@ -51,6 +84,22 @@ public class AbstractOIDCLogoutPropagationAction extends AbstractOIDCResponseAct
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
             return false;
         }
+
+        logoutPropagationContext = logoutPropagationContextLookupStrategy.apply(profileRequestContext);
+        if (logoutPropagationContext == null) {
+            log.error("{} No logout propagation context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        if (logoutPropagationContext.getSession() instanceof OIDCRPSession rpSession) {
+            oidcRPSession = rpSession;
+        } else {
+            log.error("{} Logout propagation context did not contain a OIDCRPSession", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+
         return true;
     }
 
@@ -58,8 +107,23 @@ public class AbstractOIDCLogoutPropagationAction extends AbstractOIDCResponseAct
      * Get the OIDC logout propagation context.
      * @return The OIDC logout propagation context.
      */
-    @Nullable public OIDCLogoutPropagationContext getOidcLogoutContext() {
+    @Nullable protected OIDCLogoutPropagationContext getOidcLogoutContext() {
         return oidcLogoutContext;
     }
 
+    /**
+     * Get the protocol-independent logout propagation context.
+     * @return The protocol-independent logout propagation context.
+     */
+    @Nullable protected LogoutPropagationContext getLogoutPropagationContext() {
+        return logoutPropagationContext;
+    }
+
+    /**
+     * Get the OIDC RP session.
+     * @return The OIDC RP session.
+     */
+    @Nullable protected OIDCRPSession getOidcRPSession() {
+        return oidcRPSession;
+    }
 }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ExecuteBackChannelLogoutRequest.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ExecuteBackChannelLogoutRequest.java
index 20d39129..da7a232f 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ExecuteBackChannelLogoutRequest.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ExecuteBackChannelLogoutRequest.java
@@ -18,7 +18,6 @@ import com.nimbusds.common.contenttype.ContentType;
 import com.nimbusds.jwt.JWT;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 
-import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
 import net.shibboleth.idp.session.context.LogoutPropagationContext;
 import net.shibboleth.oidc.profile.config.navigate.ClientInformationStringValueLookupFunction;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
@@ -35,7 +34,6 @@ import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.NameValuePair;
 import org.apache.hc.core5.http.message.BasicNameValuePair;
-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;
@@ -69,18 +67,9 @@ public class ExecuteBackChannelLogoutRequest extends AbstractOIDCBackChannelLogo
     /** HTTP client security parameters. */
     @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
 
-    /** The lookup strategy for the logout propagation context. */
-    @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> logoutPropagationContextLookupStrategy;
-
     /** The lookup strategy for back-channel logout endpoint. */
     @Nonnull private final Function<OIDCClientInformation, String> oidcBackChannelLogoutURILookupFunction;
 
-    /** The logout propagation context to operate on. */
-    @Nullable private LogoutPropagationContext logoutPropagationContext;
-
-    /** The OIDC RP session resolved from the logout propagation context. */
-    @Nullable private OIDCRPSession oidcRPSession;
-
     /** The back-channel logout URI for which to send the back-channel logout request. */
     @Nullable private URI backChannelLogoutURI;
 
@@ -93,7 +82,6 @@ public class ExecuteBackChannelLogoutRequest extends AbstractOIDCBackChannelLogo
     public ExecuteBackChannelLogoutRequest() {
         oidcBackChannelLogoutURILookupFunction =
                 new ClientInformationStringValueLookupFunction("backchannel_logout_uri");
-        logoutPropagationContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
     }
 
     /**
@@ -114,17 +102,6 @@ public class ExecuteBackChannelLogoutRequest extends AbstractOIDCBackChannelLogo
         httpClientSecurityParameters = params;
     }
 
-    /**
-     * Set the lookup strategy for logout propagation context.
-     * @param strategy What to set
-     */
-    public void setLogoutPropagationContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, LogoutPropagationContext> strategy) {
-        checkSetterPreconditions();
-        logoutPropagationContextLookupStrategy = Constraint.isNotNull(strategy,
-                "LogoutPropagationContext lookup strategy cannot be null");
-    }
-
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -139,24 +116,6 @@ public class ExecuteBackChannelLogoutRequest extends AbstractOIDCBackChannelLogo
             return false;
         }
 
-        logoutPropagationContext = logoutPropagationContextLookupStrategy.apply(profileRequestContext);
-        if (logoutPropagationContext == null) {
-            log.error("{} No bclogout propagation context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        } else if (!(logoutPropagationContext.getSession() instanceof OIDCRPSession)) {
-            log.error("{} Logout propagation context did not contain a OIDCRPSession", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        }
-
-        oidcRPSession = (OIDCRPSession) logoutPropagationContext.getSession();
-        if (oidcRPSession == null) {
-            log.error("{} No OIDCRPSession available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        }
-
         if (getMetadataContext() == null) {
             log.error("{} No OIDC metadata context available", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
@@ -193,9 +152,9 @@ public class ExecuteBackChannelLogoutRequest extends AbstractOIDCBackChannelLogo
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
-        final String relyingPartyId = oidcRPSession.getId();
-        final String subject = oidcRPSession.getSubject();
-        final String sessionId = oidcRPSession.getSessionIdentifier();
+        final String relyingPartyId = getOidcRPSession().getId();
+        final String subject = getOidcRPSession().getSubject();
+        final String sessionId = getOidcRPSession().getSessionIdentifier();
 
         final HttpPost httpRequest = new HttpPost(backChannelLogoutURI);
 
@@ -215,24 +174,24 @@ public class ExecuteBackChannelLogoutRequest extends AbstractOIDCBackChannelLogo
 
             if (response.getCode() == HttpStatus.SC_OK || response.getCode() == HttpStatus.SC_NO_CONTENT) {
                 if (!getOidcLogoutContext().hasRevocationError()) {
-                    logoutPropagationContext.setResult(LogoutPropagationContext.Result.Success);
+                    getLogoutPropagationContext().setResult(LogoutPropagationContext.Result.Success);
                     log.debug("{} back-channel logout for rp '{}', sub '{}' and sid '{}' succeeded.", getLogPrefix(),
                             relyingPartyId, subject, sessionId);
                 } else {
-                    logoutPropagationContext.setResult(LogoutPropagationContext.Result.Failure);
+                    getLogoutPropagationContext().setResult(LogoutPropagationContext.Result.Failure);
                     log.debug("{} back-channel logout for rp '{}', sub '{}' and sid '{}' succeeded, but overall " +
                             "result remains 'failure' due to previous error",
                             getLogPrefix(), relyingPartyId, subject, sessionId);
                 }
             } else {
-                logoutPropagationContext.setResult(LogoutPropagationContext.Result.Failure);
+                getLogoutPropagationContext().setResult(LogoutPropagationContext.Result.Failure);
                 log.error("{} back-channel logout for rp '{}', sub '{}' and sid '{}' failed. HTTP code: {}",
                         getLogPrefix(), relyingPartyId, subject, sessionId, response.getCode());
             }
         } catch (final IOException | URISyntaxException e) {
             log.error("{} back-channel logout for rp '{}' with token '{}' failed.", getLogPrefix(), relyingPartyId,
                     logoutToken.serialize(), e);
-            logoutPropagationContext.setResult(LogoutPropagationContext.Result.Failure);
+            getLogoutPropagationContext().setResult(LogoutPropagationContext.Result.Failure);
         }
     }
 
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormOutboundLogoutRequestMessage.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormOutboundLogoutRequestMessage.java
index 3f1be412..d57fd6ad 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormOutboundLogoutRequestMessage.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormOutboundLogoutRequestMessage.java
@@ -20,9 +20,7 @@ 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 org.slf4j.LoggerFactory;
@@ -30,10 +28,7 @@ import org.slf4j.LoggerFactory;
 import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.impl.FrontChannelLogoutPropagationResponse;
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
-import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
 import net.shibboleth.idp.profile.IdPEventIds;
-import net.shibboleth.idp.session.context.LogoutPropagationContext;
 import net.shibboleth.profile.context.navigate.IssuerLookupFunction;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.StringSupport;
@@ -42,7 +37,7 @@ import net.shibboleth.shared.primitive.StringSupport;
  * Action that forms outbound message based on request and response context. Formed message is set to
  * {@link ProfileRequestContext#getOutboundMessageContext()}.
  */
-public class FormOutboundLogoutRequestMessage extends AbstractOIDCResponseAction {
+public class FormOutboundLogoutRequestMessage extends AbstractOIDCLogoutPropagationAction {
 
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(FormOutboundLogoutRequestMessage.class);
@@ -50,15 +45,9 @@ public class FormOutboundLogoutRequestMessage extends AbstractOIDCResponseAction
     /** Strategy used to obtain the response issuer value. */
     @Nonnull private Function<ProfileRequestContext, String> issuerLookupStrategy;
 
-    /** The lookup strategy for the logout propagation context. */
-    @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> logoutPropagationContextLookupStrategy;
-
     /** Issuer value to included in the response message, if configured to be included. */
     @Nullable private String issuer;
 
-    /** The logout propagation context to operate on. */
-    @Nullable private LogoutPropagationContext logoutPropagationContext;
-
     /** The RP metadata where the front-channel logout related data is fetched. */
     @Nullable private OIDCClientInformation clientInformation;
 
@@ -70,7 +59,6 @@ public class FormOutboundLogoutRequestMessage extends AbstractOIDCResponseAction
      */
     public FormOutboundLogoutRequestMessage() {
         issuerLookupStrategy = new IssuerLookupFunction();
-        logoutPropagationContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
     }
 
     /**
@@ -83,18 +71,6 @@ public class FormOutboundLogoutRequestMessage extends AbstractOIDCResponseAction
         issuerLookupStrategy = Constraint.isNotNull(strategy, "IssuerLookupStrategy lookup strategy cannot be null");
     }
 
-    /**
-     * Set the strategy used to locate logout propagation context to use.
-     * 
-     * @param strategy What to set
-     */
-    public void setLogoutPropagationContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, LogoutPropagationContext> strategy) {
-        checkSetterPreconditions();
-        logoutPropagationContextLookupStrategy = Constraint.isNotNull(strategy,
-                "LogoutPropagationContext lookup strategy cannot be null");
-    }
-
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -109,17 +85,6 @@ public class FormOutboundLogoutRequestMessage extends AbstractOIDCResponseAction
             return false;
         }
 
-        logoutPropagationContext = logoutPropagationContextLookupStrategy.apply(profileRequestContext);
-        if (logoutPropagationContext == null) {
-            log.error("{} No bclogout propagation context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        } else if (!(logoutPropagationContext.getSession() instanceof OIDCRPSession)) {
-            log.error("{} Logout propagation context did not contain a OIDCRPSession", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        }
-
         if (getMetadataContext() == null || getMetadataContext().getClientInformation() == null) {
             log.error("{} Could not find OIDC metadata", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
@@ -144,7 +109,7 @@ public class FormOutboundLogoutRequestMessage extends AbstractOIDCResponseAction
         final FrontChannelLogoutPropagationResponse response;
         if (clientInformation.getOIDCMetadata().requiresFrontChannelLogoutSession()) {
             response = new FrontChannelLogoutPropagationResponse(frontChannelLogoutUri.toString(), issuer,
-                    ((OIDCRPSession) logoutPropagationContext.getSession()).getSessionIdentifier());
+                    getOidcRPSession().getSessionIdentifier());
         } else {
             response = new FrontChannelLogoutPropagationResponse(frontChannelLogoutUri.toString());
         }
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PrepareBackChannelLogoutRequest.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PrepareBackChannelLogoutRequest.java
index 93f98e00..4fe46c9d 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PrepareBackChannelLogoutRequest.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PrepareBackChannelLogoutRequest.java
@@ -22,13 +22,11 @@ import com.nimbusds.openid.connect.sdk.claims.LogoutTokenClaimsSet;
 import com.nimbusds.openid.connect.sdk.claims.SessionID;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCBackChannelLogoutPropagationContext;
-import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
-import net.shibboleth.idp.session.context.LogoutPropagationContext;
 import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.shared.security.impl.SecureRandomIdentifierGenerationStrategy;
 
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.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;
@@ -36,6 +34,8 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 import java.util.Calendar;
 import java.util.Collections;
 import java.util.Date;
@@ -51,29 +51,29 @@ public class PrepareBackChannelLogoutRequest extends AbstractOIDCBackChannelLogo
     /** Class logger. */
     @Nonnull private static final Logger log = LoggerFactory.getLogger(PrepareBackChannelLogoutRequest.class);
 
-    /** The lookup strategy for fetching logout propagation context containing the OIDC RP session. */
-    @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> logoutPropagationContextLookupStrategy;
+    /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
+    @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
 
-    /** The OIDC RP session containing the data for back-channel logout. */
-    @Nullable private OIDCRPSession oidcRPSession;
+    /** The generator to use. */
+    @Nullable private IdentifierGenerationStrategy idGenerator;
 
     /**
      * Constructor.
      */
     public PrepareBackChannelLogoutRequest() {
-        logoutPropagationContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
+        idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
     }
 
     /**
-     * Set the lookup strategy for fetching logout propagation context containing the OIDC RP session.
+     * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
      * 
-     * @param strategy What to set
+     * @param strategy lookup strategy
      */
-    public void setLogoutPropagationContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, LogoutPropagationContext> strategy) {
+    public void setIdentifierGeneratorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,IdentifierGenerationStrategy> strategy) {
         checkSetterPreconditions();
-        logoutPropagationContextLookupStrategy = Constraint.isNotNull(strategy,
-                "LogoutPropagationContext lookup strategy cannot be null");
+        idGeneratorLookupStrategy =
+                Constraint.isNotNull(strategy, "Identifier generation strategy cannot be null");
     }
 
     /** {@inheritDoc} */
@@ -83,32 +83,25 @@ public class PrepareBackChannelLogoutRequest extends AbstractOIDCBackChannelLogo
             return false;
         }
 
-        final LogoutPropagationContext logoutPropagationContext =
-                logoutPropagationContextLookupStrategy.apply(profileRequestContext);
-        if (logoutPropagationContext == null) {
-            log.error("{} No bclogout propagation context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        } else if (!(logoutPropagationContext.getSession() instanceof OIDCRPSession)) {
-            log.error("{} Logout propagation context did not contain a OIDCRPSession", getLogPrefix());
+        idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+        if (idGenerator == null) {
+            log.error("{} No identifier generation strategy", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
             return false;
         }
 
-        oidcRPSession = (OIDCRPSession) logoutPropagationContext.getSession();
-
         return true;
     }
 
     /** {@inheritDoc} */
     @Override
-    protected void doExecute(@NotNull ProfileRequestContext profileRequestContext) {
-        final Issuer iss = new Issuer(oidcRPSession.getIssuer());
-        final Subject sub = new Subject(oidcRPSession.getSubject());
-        final List<Audience> aud = Collections.singletonList(new Audience(oidcRPSession.getId()));
+    protected void doExecute(@Nonnull ProfileRequestContext profileRequestContext) {
+        final Issuer iss = new Issuer(getOidcRPSession().getIssuer());
+        final Subject sub = new Subject(getOidcRPSession().getSubject());
+        final List<Audience> aud = Collections.singletonList(new Audience(getOidcRPSession().getId()));
         final Date iat = Calendar.getInstance().getTime();
-        final JWTID jti = new JWTID(oidcRPSession.getRootTokenIdentifier());
-        final SessionID sid = new SessionID(oidcRPSession.getSessionIdentifier());
+        final JWTID jti = new JWTID(idGenerator.generateIdentifier());
+        final SessionID sid = new SessionID(getOidcRPSession().getSessionIdentifier());
 
         final LogoutTokenClaimsSet logoutTokenClaimsSet = new LogoutTokenClaimsSet(iss, sub, aud, iat, jti, sid);
         getOidcBackChannelLogoutContext().setLogoutTokenClaimsSet(logoutTokenClaimsSet);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/RevokeTokenChain.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/RevokeTokenChain.java
index 4e8c88d2..6c7b922d 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/RevokeTokenChain.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/RevokeTokenChain.java
@@ -15,24 +15,20 @@
 package net.shibboleth.idp.plugin.oidc.op.logout.profile.impl;
 
 import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultChainRevocationLifetimeLookupStrategy;
-import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
 import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
 import net.shibboleth.idp.profile.IdPEventIds;
-import net.shibboleth.idp.session.context.LogoutPropagationContext;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.logic.Constraint;
 
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.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.opensaml.storage.RevocationCache;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 import java.time.Duration;
 import java.util.function.Function;
 
@@ -47,9 +43,6 @@ public class RevokeTokenChain extends AbstractOIDCLogoutPropagationAction {
     /** Token revocation cache instance to use. */
     @NonnullAfterInit private RevocationCache revocationCache;
 
-    /** Lookup function to logout propagation context. */
-    @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> logoutPropagationContextLookupStrategy;
-
     /** Lookup function to supply chain revocation lifetime. */
     @Nonnull private Function<ProfileRequestContext,Duration> chainRevocationLifetimeLookupStrategy;
 
@@ -64,7 +57,6 @@ public class RevokeTokenChain extends AbstractOIDCLogoutPropagationAction {
      */
     public RevokeTokenChain() {
         chainRevocationLifetimeLookupStrategy = new DefaultChainRevocationLifetimeLookupStrategy();
-        logoutPropagationContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
     }
 
     /**
@@ -73,7 +65,7 @@ public class RevokeTokenChain extends AbstractOIDCLogoutPropagationAction {
      * @param cache What to set.
      */
     public void setRevocationCache(@Nonnull final RevocationCache cache) {
-        ifInitializedThrowUnmodifiabledComponentException();
+        checkSetterPreconditions();
         revocationCache = Constraint.isNotNull(cache, "RevocationCache cannot be null");
     }
 
@@ -84,19 +76,8 @@ public class RevokeTokenChain extends AbstractOIDCLogoutPropagationAction {
      */
     public void setChainRevocationLifetimeLookupStrategy(
             @Nullable final Function<ProfileRequestContext,Duration> strategy) {
-        chainRevocationLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
-    }
-
-    /**
-     * Set the lookup strategy for logout propagation context.
-     * 
-     * @param strategy What to set
-     */
-    public void setLogoutPropagationContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, LogoutPropagationContext> strategy) {
         checkSetterPreconditions();
-        logoutPropagationContextLookupStrategy = Constraint.isNotNull(strategy,
-                "LogoutPropagationContext lookup strategy cannot be null");
+        chainRevocationLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
     }
 
     /**
@@ -115,20 +96,7 @@ public class RevokeTokenChain extends AbstractOIDCLogoutPropagationAction {
             return false;
         }
 
-        final LogoutPropagationContext logoutPropagationContext =
-                logoutPropagationContextLookupStrategy.apply(profileRequestContext);
-        if (logoutPropagationContext == null) {
-            log.error("{} No bclogout propagation context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        }
-        if (!(logoutPropagationContext.getSession() instanceof OIDCRPSession)) {
-            log.error("{} Logout propagation context did not contain a OIDCRPSession", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        }
-
-        rootTokenIdentifier = ((OIDCRPSession) logoutPropagationContext.getSession()).getRootTokenIdentifier();
+        rootTokenIdentifier = getOidcRPSession().getRootTokenIdentifier();
         if (rootTokenIdentifier == null) {
             log.debug("{} OIDCRPSession context did not contain a root jti, nothing to revoke.", getLogPrefix());
             return false;
@@ -141,7 +109,7 @@ public class RevokeTokenChain extends AbstractOIDCLogoutPropagationAction {
      * {@inheritDoc}
      */
     @Override
-    protected void doExecute(@NotNull ProfileRequestContext profileRequestContext) {
+    protected void doExecute(@Nonnull ProfileRequestContext profileRequestContext) {
         if (revocationCache.revoke(
                 RevocationCacheContexts.AUTHORIZATION_CODE, rootTokenIdentifier, revocationLifetime)) {
             getOidcLogoutContext().setRevocationError(false);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/SetFrontChannelLogoutSuccess.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/SetFrontChannelLogoutSuccess.java
index f80a2d30..9b14d804 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/SetFrontChannelLogoutSuccess.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/SetFrontChannelLogoutSuccess.java
@@ -14,81 +14,27 @@
 
 package net.shibboleth.idp.plugin.oidc.op.logout.profile.impl;
 
-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 org.slf4j.LoggerFactory;
 
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
-import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
 import net.shibboleth.idp.session.context.LogoutPropagationContext;
 import net.shibboleth.idp.session.context.LogoutPropagationContext.Result;
-import net.shibboleth.shared.logic.Constraint;
 
 /**
  * Action that sets the {@link LogoutPropagationContext#setResult(Result)} as 'success'.
  */
-public class SetFrontChannelLogoutSuccess extends AbstractOIDCResponseAction {
+public class SetFrontChannelLogoutSuccess extends AbstractOIDCLogoutPropagationAction {
 
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(SetFrontChannelLogoutSuccess.class);
 
-    /** The lookup strategy for the logout propagation context. */
-    @Nonnull private Function<ProfileRequestContext, LogoutPropagationContext> logoutPropagationContextLookupStrategy;
-
-    /** The logout propagation context to operate on. */
-    @Nullable private LogoutPropagationContext logoutPropagationContext;
-
-    /**
-     * Constructor.
-     */
-    public SetFrontChannelLogoutSuccess() {
-        logoutPropagationContextLookupStrategy = new ChildContextLookup<>(LogoutPropagationContext.class);
-    }
-
-    /**
-     * Set the lookup strategy for the logout propagation context.
-     * @param strategy What to set
-     */
-    public void setLogoutPropagationContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext, LogoutPropagationContext> strategy) {
-        checkSetterPreconditions();
-        logoutPropagationContextLookupStrategy = Constraint.isNotNull(strategy,
-                "LogoutPropagationContext lookup strategy cannot be null");
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
-        if (!super.doPreExecute(profileRequestContext)) {
-            return false;
-        }
-
-        logoutPropagationContext = logoutPropagationContextLookupStrategy.apply(profileRequestContext);
-        if (logoutPropagationContext == null) {
-            log.error("{} No bclogout propagation context", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        } else if (!(logoutPropagationContext.getSession() instanceof OIDCRPSession)) {
-            log.error("{} Logout propagation context did not contain a OIDCRPSession", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            return false;
-        }
-
-        return true;
-    }
-
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         log.debug("{} Set front-channel logout propagation result 'success'", getLogPrefix());
-        logoutPropagationContext.setResult(Result.Success);
+        getLogoutPropagationContext().setResult(Result.Success);
     }
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/BaseOIDCBackChannelLogoutPropagationActionTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/BaseOIDCBackChannelLogoutPropagationActionTest.java
new file mode 100644
index 00000000..f61588df
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/BaseOIDCBackChannelLogoutPropagationActionTest.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.logout.profile.impl;
+
+import org.opensaml.profile.action.EventIds;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCBackChannelLogoutPropagationContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCLogoutPropagationContext;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+
+/**
+ * Base unit test class for actions extending {@link AbstractOIDCBackChannelLogoutPropagationAction}.
+ *
+ * @param <T>
+ */
+public abstract class BaseOIDCBackChannelLogoutPropagationActionTest<T extends AbstractOIDCBackChannelLogoutPropagationAction>
+        extends BaseOIDCLogoutPropagationActionTest<AbstractOIDCBackChannelLogoutPropagationAction> {
+
+    protected OIDCBackChannelLogoutPropagationContext backChannelLogoutPropagationContext;
+
+    @BeforeMethod
+    protected void setup() throws Exception {
+        super.setup();
+        backChannelLogoutPropagationContext = new OIDCBackChannelLogoutPropagationContext();
+        profileRequestContext.getOutboundMessageContext().getSubcontext(OIDCLogoutPropagationContext.class)
+              .addSubcontext(backChannelLogoutPropagationContext);
+    }
+
+    @Test
+    public void testMissingOidcBackChannelLogoutPropagationContext() {
+        action = constructAction();
+        profileRequestContext.getOutboundMessageContext().getSubcontext(OIDCLogoutPropagationContext.class)
+            .removeSubcontext(OIDCBackChannelLogoutPropagationContext.class);
+        ActionTestingSupport.assertEvent(action.execute(requestContext), EventIds.INVALID_MSG_CTX);
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/BaseOIDCLogoutPropagationActionTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/BaseOIDCLogoutPropagationActionTest.java
new file mode 100644
index 00000000..9c004f79
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/BaseOIDCLogoutPropagationActionTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.logout.profile.impl;
+
+import java.time.Instant;
+
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCLogoutPropagationContext;
+import net.shibboleth.idp.plugin.oidc.op.session.OIDCRPSession;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.idp.session.context.LogoutPropagationContext;
+import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
+
+/**
+ * Base unit test class for actions extending {@link AbstractOIDCLogoutPropagationAction}.
+ *
+ * @param <T>
+ */
+public abstract class BaseOIDCLogoutPropagationActionTest<T extends AbstractOIDCLogoutPropagationAction> {
+
+    protected T action;
+    protected RequestContext requestContext;
+    protected ProfileRequestContext profileRequestContext;
+    protected LogoutPropagationContext logoutPropagationContext;
+    protected OIDCLogoutPropagationContext oidcLogoutPropagationContext;
+    protected OIDCRPSession oidcRpSession;
+    protected OIDCMetadataContext oidcMetadataContext;
+    protected OIDCAuthenticationResponseContext oidcResponseContext;
+
+    protected abstract T constructAction();
+
+    @BeforeMethod
+    protected void setup() throws Exception {
+        action = constructAction();
+        requestContext = new RequestContextBuilder().buildRequestContext();
+        profileRequestContext = new WebflowRequestContextProfileRequestContextLookup().apply(requestContext);
+        oidcMetadataContext = new OIDCMetadataContext();
+        profileRequestContext.getInboundMessageContext().addSubcontext(oidcMetadataContext);
+        oidcResponseContext = new OIDCAuthenticationResponseContext();
+        profileRequestContext.getOutboundMessageContext().addSubcontext(oidcResponseContext);
+        oidcLogoutPropagationContext = new OIDCLogoutPropagationContext();
+        profileRequestContext.getOutboundMessageContext().addSubcontext(oidcLogoutPropagationContext);
+        logoutPropagationContext = new LogoutPropagationContext();
+        oidcRpSession = new OIDCRPSession.Builder()
+                .creationInstant(Instant.now())
+                .expirationInstant(Instant.now().plusSeconds(60))
+                .issuer("mockIssuer")
+                .rootTokenIdentifier("mockRootTokenIdentifier")
+                .serviceId("mockClientId")
+                .sessionIdentifier("mockSessionIdentifier")
+                .subject("mockSubject")
+                .supportLogoutPropagation(true)
+                .build();
+        logoutPropagationContext.setSession(oidcRpSession);
+        profileRequestContext.addSubcontext(logoutPropagationContext);
+    }
+
+    @Test
+    public void testMissingOidcLogoutPropagationContext() {
+        action = constructAction();
+        profileRequestContext.getOutboundMessageContext().removeSubcontext(OIDCLogoutPropagationContext.class);
+        ActionTestingSupport.assertEvent(action.execute(requestContext), EventIds.INVALID_MSG_CTX);
+    }
+
+    @Test
+    public void testMissingLogoutPropagationContext() {
+        action = constructAction();
+        profileRequestContext.removeSubcontext(LogoutPropagationContext.class);
+        ActionTestingSupport.assertEvent(action.execute(requestContext), EventIds.INVALID_PROFILE_CTX);
+    }
+
+    public void testMissingOidcRpSession() {
+        action = constructAction();
+        logoutPropagationContext.setSession(null);
+        ActionTestingSupport.assertEvent(action.execute(requestContext), EventIds.INVALID_PROFILE_CTX);
+        
+    }
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ExecuteBackChannelLogoutRequestTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ExecuteBackChannelLogoutRequestTest.java
new file mode 100644
index 00000000..bfee9222
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/ExecuteBackChannelLogoutRequestTest.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.logout.profile.impl;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.mockito.Mockito;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.security.credential.Credential;
+import org.springframework.core.io.ClassPathResource;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.session.context.LogoutPropagationContext;
+import net.shibboleth.oidc.security.credential.BasicJWKCredentialFactoryBean;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link ExecuteBackChannelLogoutRequest}.
+ */
+public class ExecuteBackChannelLogoutRequestTest
+        extends BaseOIDCBackChannelLogoutPropagationActionTest<ExecuteBackChannelLogoutRequest> {
+    
+    HttpClient mockClient = Mockito.mock(HttpClient.class);
+    Credential credential;
+
+    @BeforeClass
+    protected void setupCredential() {
+        final BasicJWKCredentialFactoryBean factory = new BasicJWKCredentialFactoryBean();
+        factory.setResource(new ClassPathResource("credentials/idp-signing-rs.jwk"));
+        try {
+            factory.afterPropertiesSet();
+            credential = factory.getObject();
+        } catch (Exception e) {
+            Assert.fail("Could not setup the signing credential", e);
+        }
+
+    }
+    protected ExecuteBackChannelLogoutRequest constructAction() {
+        final ExecuteBackChannelLogoutRequest action = new ExecuteBackChannelLogoutRequest();
+        action.setHttpClient(mockClient);
+        try {
+            action.initialize();
+        } catch (final ComponentInitializationException e) {
+            Assert.fail();
+        }
+        return action;
+    }
+
+    @Test
+    public void testMissingBackChannelLogoutUri() {
+        ActionTestingSupport.assertEvent(action.execute(requestContext), EventIds.INVALID_PROFILE_CTX);
+    }
+
+    @Test
+    public void testMissingBackChannelLogoutToken() {
+        populateMetadataContext();
+        ActionTestingSupport.assertEvent(action.execute(requestContext), EventIds.INVALID_MSG_CTX);
+    }
+
+    @Test
+    public void testHttpClientThrowingException() throws IOException {
+        populateMetadataContext();
+        populateResponseContext();
+        Mockito.when(mockClient.executeOpen(Mockito.any(), (ClassicHttpRequest)Mockito.any(),
+                Mockito.any())).thenThrow(new IOException("mock"));
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        Assert.assertEquals(logoutPropagationContext.getResult(), LogoutPropagationContext.Result.Failure);
+    }
+
+    @Test
+    public void testHttpClientReturningOK_noRevocationError() throws IOException {
+        populateMetadataContext();
+        populateResponseContext();
+        initializeMockClient(HttpStatus.SC_OK);
+        oidcLogoutPropagationContext.setRevocationError(false);
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        Assert.assertEquals(logoutPropagationContext.getResult(), LogoutPropagationContext.Result.Success);
+    }
+
+    @Test
+    public void testHttpClientReturningOK_withRevocationError() throws IOException {
+        populateMetadataContext();
+        populateResponseContext();
+        initializeMockClient(HttpStatus.SC_OK);
+        oidcLogoutPropagationContext.setRevocationError(true);
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        Assert.assertEquals(logoutPropagationContext.getResult(), LogoutPropagationContext.Result.Failure);
+    }
+
+    @Test
+    public void testHttpClientReturningNoContent_noRevocationError() throws IOException {
+        populateMetadataContext();
+        populateResponseContext();
+        initializeMockClient(HttpStatus.SC_NO_CONTENT);
+        oidcLogoutPropagationContext.setRevocationError(false);
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        Assert.assertEquals(logoutPropagationContext.getResult(), LogoutPropagationContext.Result.Success);
+    }
+
+    @Test
+    public void testHttpClientReturningNoContent_withRevocationError() throws IOException {
+        populateMetadataContext();
+        populateResponseContext();
+        initializeMockClient(HttpStatus.SC_NO_CONTENT);
+        oidcLogoutPropagationContext.setRevocationError(true);
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        Assert.assertEquals(logoutPropagationContext.getResult(), LogoutPropagationContext.Result.Failure);
+    }
+
+    @Test
+    public void testHttpClientReturningUnexpectedCode() throws IOException {
+        populateMetadataContext();
+        populateResponseContext();
+        initializeMockClient(HttpStatus.SC_BAD_GATEWAY);
+        oidcLogoutPropagationContext.setRevocationError(false);
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        Assert.assertEquals(logoutPropagationContext.getResult(), LogoutPropagationContext.Result.Failure);
+    }
+
+    protected void populateMetadataContext() {
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        try {
+            metadata.setBackChannelLogoutURI(new URI("https://example.org/backchannel"));
+        } catch (URISyntaxException e) {
+            Assert.fail();
+        }
+        oidcMetadataContext.setClientInformation(new OIDCClientInformation(new ClientID("mockClientId"), metadata));
+    }
+
+    protected void populateResponseContext() {
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder().audience("mockAudience").issuer("mockIssuer").build();
+        final SignedJWT jwt = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).keyID("id").build(),
+                claimsSet);
+        try {
+            jwt.sign(new RSASSASigner(credential.getPrivateKey()));
+        } catch (JOSEException e) {
+            Assert.fail("Could not sign the JWT", e);
+        }
+        oidcResponseContext.setProcessedToken(jwt);
+    }
+    
+    protected void initializeMockClient(final int statusCode) {
+        final ClassicHttpResponse httpResponse = Mockito.mock(ClassicHttpResponse.class);
+        Mockito.when(httpResponse.getCode()).thenReturn(statusCode);
+        try {
+            Mockito.when(mockClient.executeOpen(Mockito.any(), (ClassicHttpRequest)Mockito.any(), 
+                    (HttpContext)Mockito.any())).thenReturn(httpResponse);
+        } catch (IOException e) {
+            Assert.fail("Could not initialize the mock HTTP client", e);
+        }
+
+    }
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormOutboundLogoutRequestMessageTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormOutboundLogoutRequestMessageTest.java
new file mode 100644
index 00000000..bf876a5f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/FormOutboundLogoutRequestMessageTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.logout.profile.impl;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.function.Function;
+
+import org.mockito.Mockito;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientInformation;
+import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.impl.FrontChannelLogoutPropagationResponse;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link FormOutboundLogoutRequestMessage}.
+ */
+public class FormOutboundLogoutRequestMessageTest
+        extends BaseOIDCLogoutPropagationActionTest<FormOutboundLogoutRequestMessage> {
+    
+    @SuppressWarnings("unchecked")
+    Function<ProfileRequestContext, String> issuerLookupStrategy = Mockito.mock(Function.class);
+    
+    String frontChannelUri = "https://example.org/frontchannel";
+
+    @Override
+    protected FormOutboundLogoutRequestMessage constructAction() {
+        final FormOutboundLogoutRequestMessage action = new FormOutboundLogoutRequestMessage();
+        action.setIssuerLookupStrategy(issuerLookupStrategy);
+        try {
+            action.initialize();
+        } catch (final ComponentInitializationException e) {
+            Assert.fail();
+        }
+        return action;
+    }
+
+    @Test
+    public void testWithNoIssuer() {
+        Mockito.when(issuerLookupStrategy.apply(Mockito.any())).thenReturn(null);
+        ActionTestingSupport.assertEvent(action.execute(requestContext), IdPEventIds.INVALID_RELYING_PARTY_CTX);
+    }
+
+    @Test
+    public void testWithNoMetadata() {
+        Mockito.when(issuerLookupStrategy.apply(Mockito.any())).thenReturn("mockIssuer");
+        ActionTestingSupport.assertEvent(action.execute(requestContext), IdPEventIds.INVALID_RELYING_PARTY_CTX);
+    }
+
+    @Test
+    public void testSuccessWithoutSessionDetails() {
+        populateMetadataContext(false);
+        Mockito.when(issuerLookupStrategy.apply(Mockito.any())).thenReturn("mockIssuer");
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        final Object response = profileRequestContext.getOutboundMessageContext().getMessage();
+        Assert.assertNotNull(response);
+        Assert.assertTrue(response instanceof FrontChannelLogoutPropagationResponse);
+        final FrontChannelLogoutPropagationResponse logoutResponse = (FrontChannelLogoutPropagationResponse) response;
+        Assert.assertTrue(logoutResponse.indicatesSuccess());
+        Assert.assertEquals(logoutResponse.toHTTPResponse().getLocation().toString(), frontChannelUri);
+    }
+
+    @Test
+    public void testSuccessWithSessionDetails() {
+        populateMetadataContext(true);
+        Mockito.when(issuerLookupStrategy.apply(Mockito.any())).thenReturn("mockIssuer");
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        final Object response = profileRequestContext.getOutboundMessageContext().getMessage();
+        Assert.assertNotNull(response);
+        Assert.assertTrue(response instanceof FrontChannelLogoutPropagationResponse);
+        final FrontChannelLogoutPropagationResponse logoutResponse = (FrontChannelLogoutPropagationResponse) response;
+        Assert.assertTrue(logoutResponse.indicatesSuccess());
+        final String uri = logoutResponse.toHTTPResponse().getLocation().toString();
+        Assert.assertTrue(uri.startsWith(frontChannelUri));
+        Assert.assertTrue(uri.contains("iss=mockIssuer"));
+        Assert.assertTrue(uri.contains("sid=mockSessionIdentifier"));
+
+    }
+
+    protected void populateMetadataContext(final boolean addSession) {
+        final OIDCClientMetadata metadata = new OIDCClientMetadata();
+        try {
+            metadata.setFrontChannelLogoutURI(new URI(frontChannelUri));
+            metadata.requiresFrontChannelLogoutSession(addSession);
+        } catch (URISyntaxException e) {
+            Assert.fail();
+        }
+        oidcMetadataContext.setClientInformation(new OIDCClientInformation(new ClientID("mockClientId"), metadata));
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PrepareBackChannelLogoutRequestTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PrepareBackChannelLogoutRequestTest.java
new file mode 100644
index 00000000..e2ca1568
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/PrepareBackChannelLogoutRequestTest.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.logout.profile.impl;
+
+import org.opensaml.profile.action.EventIds;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.id.Audience;
+import com.nimbusds.openid.connect.sdk.claims.LogoutTokenClaimsSet;
+
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+
+/**
+ * Unit tests for {@link PrepareBackChannelLogoutRequest}.
+ */
+public class PrepareBackChannelLogoutRequestTest
+        extends BaseOIDCBackChannelLogoutPropagationActionTest<PrepareBackChannelLogoutRequest> {
+
+    @Override
+    protected PrepareBackChannelLogoutRequest constructAction() {
+        return constructAction(new IdentifierGenerationStrategy() {
+
+            @Override
+            public String generateIdentifier() {
+                return "mockId";
+            }
+
+            @Override
+            public String generateIdentifier(boolean xmlSafe) {
+                return "mockId";
+            }
+
+        });
+    }
+
+    protected PrepareBackChannelLogoutRequest constructAction(final IdentifierGenerationStrategy strategy) {
+        final PrepareBackChannelLogoutRequest action = new PrepareBackChannelLogoutRequest();
+        action.setIdentifierGeneratorLookupStrategy(prc -> strategy);
+        try {
+            action.initialize();
+        } catch (final ComponentInitializationException e) {
+            Assert.fail();
+        }
+        return action;
+    }
+
+    @Test
+    public void testMissingIdGenerationStrategy() {
+        action = constructAction(null);
+        ActionTestingSupport.assertEvent(action.execute(requestContext), EventIds.INVALID_PROFILE_CTX);
+    }
+
+    @Test
+    public void testSuccess() {
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        final LogoutTokenClaimsSet claimsSet = backChannelLogoutPropagationContext.getLogoutTokenClaimsSet();
+        Assert.assertNotNull(claimsSet);
+        Assert.assertEquals(claimsSet.getAudience().size(), 1);
+        Assert.assertTrue(claimsSet.getAudience().contains(new Audience(oidcRpSession.getId())));
+        Assert.assertEquals(claimsSet.getIssuer().getValue(), "mockIssuer");
+        Assert.assertEquals(claimsSet.getSubject().getValue(), "mockSubject");
+        Assert.assertEquals(claimsSet.getSessionID().getValue(), "mockSessionIdentifier");
+        Assert.assertEquals(claimsSet.getJWTID().getValue(), "mockId");
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/RevokeTokenChainTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/RevokeTokenChainTest.java
new file mode 100644
index 00000000..086abd70
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/RevokeTokenChainTest.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.logout.profile.impl;
+
+import java.time.Duration;
+import java.util.function.Function;
+
+import org.mockito.Mockito;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.RevocationCache;
+import org.opensaml.storage.impl.MemoryStorageService;
+import org.opensaml.storage.impl.StorageServiceRevocationCache;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link RevokeTokenChain}.
+ */
+public class RevokeTokenChainTest extends BaseOIDCLogoutPropagationActionTest<RevokeTokenChain> {
+
+    MemoryStorageService storageService;
+    StorageServiceRevocationCache revocationCache;
+    @SuppressWarnings("unchecked")
+    Function<ProfileRequestContext,Duration> revocationLifetimeStrategy = Mockito.mock(Function.class);
+    
+    @Override
+    protected RevokeTokenChain constructAction() {
+        try {
+        storageService = new MemoryStorageService();
+        storageService.setId("test");
+        storageService.initialize();
+
+        revocationCache = new StorageServiceRevocationCache();
+        revocationCache.setId("mockCache");
+        revocationCache.setEntryExpiration(Duration.ofHours(1));
+        revocationCache.setStorage(storageService);
+        revocationCache.initialize();
+        } catch (final ComponentInitializationException e) {
+            Assert.fail();
+        }
+        return constructAction(revocationCache);
+    }
+
+    protected RevokeTokenChain constructAction(final RevocationCache revocationCache) {
+        final RevokeTokenChain action = new RevokeTokenChain();
+        action.setChainRevocationLifetimeLookupStrategy(prc -> Duration.ofHours(2));
+        action.setRevocationCache(revocationCache);
+        action.setChainRevocationLifetimeLookupStrategy(revocationLifetimeStrategy);
+        try {
+            action.initialize();
+        } catch (final ComponentInitializationException e) {
+            Assert.fail();
+        }
+        return action;        
+    }
+    
+    @Test
+    public void testWithNullLifetime() {
+        Mockito.when(revocationLifetimeStrategy.apply(Mockito.any())).thenReturn(null);
+        ActionTestingSupport.assertEvent(action.execute(requestContext), IdPEventIds.INVALID_PROFILE_CONFIG);
+    }
+
+    @Test
+    public void testWithZeroLifetime() {
+        Mockito.when(revocationLifetimeStrategy.apply(Mockito.any())).thenReturn(Duration.ZERO);
+        ActionTestingSupport.assertEvent(action.execute(requestContext), IdPEventIds.INVALID_PROFILE_CONFIG);
+    }
+
+    @Test
+    public void testWithPositiveLifetime() {
+        Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE,
+                "mockRootTokenIdentifier"));
+        Mockito.when(revocationLifetimeStrategy.apply(Mockito.any())).thenReturn(Duration.ofHours(2));
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        Assert.assertFalse(oidcLogoutPropagationContext.hasRevocationError());
+        Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE,
+                "mockRootTokenIdentifier"));
+    }
+
+    @Test
+    public void testWithPositiveLifetimeRevocationFailed() {
+        RevocationCache revocationCache = Mockito.mock(RevocationCache.class);
+        Mockito.when(revocationCache.revoke(Mockito.anyString(), Mockito.anyString(), Mockito.any(Duration.class)))
+            .thenReturn(false);
+        action = constructAction(revocationCache);
+        Mockito.when(revocationLifetimeStrategy.apply(Mockito.any())).thenReturn(Duration.ofHours(2));
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        Assert.assertTrue(oidcLogoutPropagationContext.hasRevocationError());
+    }
+
+}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/SetFrontChannelLogoutSuccessTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/SetFrontChannelLogoutSuccessTest.java
new file mode 100644
index 00000000..87c0ff14
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/logout/profile/impl/SetFrontChannelLogoutSuccessTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.logout.profile.impl;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.session.context.LogoutPropagationContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link SetFrontChannelLogoutSuccess}.
+ */
+public class SetFrontChannelLogoutSuccessTest
+        extends BaseOIDCLogoutPropagationActionTest<SetFrontChannelLogoutSuccess> {
+
+    @Override
+    protected SetFrontChannelLogoutSuccess constructAction() {
+        final SetFrontChannelLogoutSuccess action = new SetFrontChannelLogoutSuccess();
+        try {
+            action.initialize();
+        } catch (final ComponentInitializationException e) {
+            Assert.fail();
+        }
+        return action;
+    }
+
+    @Test
+    public void test() {
+        ActionTestingSupport.assertProceedEvent(action.execute(requestContext));
+        Assert.assertEquals(logoutPropagationContext.getResult(), LogoutPropagationContext.Result.Success);
+    }
+    
+}

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


More information about the commits mailing list